Starnus Public API — Reference Documentation

Version: 1.0.0

Base URL:https://api.starnus.com/v1

WebSocket URL:wss://0vxd7s295f.execute-api.eu-central-1.amazonaws.com/prod

OpenAPI spec: GET /v1/openapi.json — no auth required, import into Postman or Swagger UI


1. Quickstart

Get from zero to a running AI execution in under 5 minutes.

Step 1 — Install the SDK (recommended)

bashCopy

pip install starnus

Step 2 — Get your API key

Open the Starnus dashboard → Dev Tools → API Keys → Create Key. Copy the sk_live_... value — it is shown only once.

Step 3 — Run your first execution

pythonCopy

import starnus

client = starnus.Starnus(api_key="sk_live_your_key_here")

# Create a project (or reuse an existing project_id)
project = client.projects.create(name="My First Project")

# Run a task and stream results in real time
execution = client.executions.create(
    project_id=project.id,
    prompt="Find 5 CEOs of software companies in the Netherlands and return their names and LinkedIn URLs.",
)

for event in execution.stream():
    if event.type == "supervisor_update":
        print(f"[supervisor] {event.message}")
    elif event.type == "plan":
        print(f"[plan]       {event.message}")
    elif event.type == "component_update":
        print(f"[agent]      {event.message}")
    elif event.type == "result":
        print(f"\n✅ Done ({event.status})\n{event.message}")
        break

Step 4 — Retrieve the artifact

After the execution completes, your results are stored as an artifact:

pythonCopy

artifacts = client.artifacts.list()
for art in artifacts["artifacts"]:
    print(art["name"], art["type"])

# Download as Excel
result = client.artifacts.export(artifact_id=art["id"], format="xlsx")
import requests, pathlib
data = requests.get(result["download_url"]).content
pathlib.Path("leads.xlsx").write_bytes(data)
print("Saved leads.xlsx")

No SDK? Use raw HTTP + WebSocket. See §20 WebSocket Streaming and §21 Code Examples.


2. Authentication

Every REST request must include your API key in the Authorization header:

httpCopy

Authorization: Bearer sk_live_your_api_key_here

For the WebSocket, pass the key as a query parameter:

wss://0vxd7s295f.execute-api.eu-central-1.amazonaws.com/prod?api_key=sk_live_your_key_here

API keys are created in Dev Tools → API Keys or via POST /v1/api-keys. Each key is shown once — store it securely in an environment variable, never in source code.

Authentication error — HTTP 401:

jsonCopy

{
  "error": {
    "code":    "authentication_error",
    "message": "Invalid API key",
    "status":  401
  }
}

3. Errors & Status Codes

All error responses use a consistent envelope:

jsonCopy

{
  "error": {
    "code":    "validation_error",
    "message": "Field 'name' is required",
    "status":  400
  }
}
HTTP Status Code When it occurs
400 validation_error Missing or invalid field in the request body
400 invalid_request Request is structurally valid but logically wrong
400 invalid_json Request body is not valid JSON
401 authentication_error Missing, invalid, or revoked API key
403 forbidden Key is valid but the resource belongs to another account
404 not_found Resource does not exist or you do not have access
405 method_not_allowed HTTP method not supported on this route
429 rate_limit_exceeded Too many requests — see §4 Rate Limits
500 internal_error Unexpected server-side failure — safe to retry with exponential backoff

4. Rate Limits

Requests are counted per API key per rolling minute:

Tier Requests / minute
Free / Trial 60
Pro 100
Pro+ 300
Ultra 300

Every response includes these headers:

Header Description
X-RateLimit-Limit Maximum requests allowed per minute for your tier
X-RateLimit-Remaining Requests remaining in the current window
X-RateLimit-Reset Unix timestamp when the window resets

When rate-limited, HTTP 429 is returned. Back off and retry after the X-RateLimit-Reset timestamp.


5. Security Model

The Starnus Public API is designed so that you never touch AWS resources directly and sensitive internal identifiers never reach you.

What is protected How
Your Cognito user_id Injected server-side from your API key — never returned in any response field
Internal S3 paths s3_key, s3_uri, bucket stripped from all responses
OAuth tokens connected_account_id, config_id, api_key for integrations stripped
Stripe IDs stripe_customer_id, stripe_subscription_id stripped
Agent internals Entire tools array (ARNs, component IDs, token refs) stripped from thread responses
API key hashes api_key_hash stripped

Ownership is verified at the API layer. Every read and write on a single resource (GET, PATCH, DELETE) checks that it belongs to your account. A 403 is returned if it does not.

Whitelist sanitization. Every response passes through a whitelist filter — only explicitly allowed fields are returned. Unknown or internal fields are dropped automatically.


6. Profile

GET /v1/me

Returns your account profile.

Response 200:

jsonCopy

{
  "profile": {
    "email":                    "john.doe@acme.com",
    "first_name":               "John",
    "last_name":                "Doe",
    "tier":                     "pro+ monthly",
    "subscription_status":      "active",
    "credits_remained":         180000.0,
    "credits_limit":            350000.0,
    "phone":                    "+31 20 123 4567",
    "bio":                      "Head of Growth at ACME Corp.",
    "work_info":                { "company": "ACME Corp", "role": "Head of Growth" },
    "external_links":           { "linkedin": "https://linkedin.com/in/johndoe" },
    "notification_preferences": { "email": true, "push": false }
  }
}
Field Type Description
email string Read-only — cannot be changed via the API
first_name string First name
last_name string Last name
tier string free, trial, pro monthly, pro+ monthly, ultra monthly (and yearly variants)
subscription_status string active, trialing, canceled, past_due
credits_remained number Credits available this billing period
credits_limit number Total credits allocated this billing period
phone string Phone number (if set)
bio string Short biography (if set)
work_info object Structured work information (if set)
external_links object External profile links (if set)
notification_preferences object Notification settings (if set)

PATCH /v1/me

Update editable profile fields. Email and tier cannot be changed via the API.

Request body (all fields optional; provide at least one):

jsonCopy

{
  "first_name":               "John",
  "last_name":                "Doe",
  "phone":                    "+31 20 123 4567",
  "bio":                      "Head of Growth at ACME Corp, building AI-powered sales tools.",
  "work_info":                { "company": "ACME Corp", "role": "Head of Growth" },
  "external_links":           { "linkedin": "https://linkedin.com/in/johndoe" },
  "notification_preferences": { "email": true, "push": false }
}
Field Type Max length Description
first_name string 1,000 First name
last_name string 1,000 Last name
phone string 1,000 Phone number
bio string 1,000 Short biography
work_info object — Arbitrary structured work information
external_links object — External links
notification_preferences object — {"email": true, "push": false}

Response 200: Same structure as GET /v1/me.

Errors:

  • 400 invalid_request — No valid fields provided, or a string field exceeds 1,000 characters
  • 400 invalid_request — A dict field received a non-object value

7. API Keys

GET /v1/api-keys

List all API keys on your account. The plaintext key is never returned after creation.

Response 200:

jsonCopy

{
  "keys": [\
    {\
      "key_id":       "3bb67544-bcdb-496f-9ab2-7b9802f710a9",\
      "key_prefix":   "sk_live_NBAYNDv",\
      "label":        "Production integration",\
      "status":       "active",\
      "created_at":   "2026-01-15T10:00:00+00:00",\
      "updated_at":   "2026-01-15T10:00:00+00:00",\
      "last_used_at": "2026-03-20T14:32:00+00:00",\
      "usage_count":  42\
    }\
  ],
  "count": 1
}
Field Type Description
key_id string (UUID) Opaque identifier — use to revoke the key
key_prefix string First characters of the key — safe to display in a UI
label string Human-readable label
status string active or revoked
last_used_at string (ISO 8601) Last usage timestamp
usage_count integer Total API calls made with this key

POST /v1/api-keys

Create a new API key. The plaintext_key is returned once only — it cannot be retrieved again.

Maximum: 10 active keys per account.

Request body:

jsonCopy

{
  "label": "Production integration"
}
Field Type Required Description
label string ✅ Human-readable name. Max 100 characters.

Response 201:

jsonCopy

{
  "key": {
    "key_id":        "3bb67544-bcdb-496f-9ab2-7b9802f710a9",
    "key_prefix":    "sk_live_NBAYNDv",
    "plaintext_key": "sk_live_NBAYNDvXXXXXXXXXXXXXXXXXXXXXXX",
    "label":         "Production integration",
    "status":        "active",
    "created_at":    "2026-03-22T15:16:30+00:00",
    "last_used_at":  null,
    "usage_count":   0
  }
}

Security: Store plaintext_key immediately and securely (environment variable / secrets manager). It will not be shown again.

Errors:

  • 400 invalid_request — label is missing or blank
  • 400 invalid_request — label exceeds 100 characters
  • 400 invalid_request — 10 active keys already exist

DELETE /v1/api-keys/{key_id}

Permanently revoke an API key. Revoked keys cannot be re-activated.

Path parameter:

Parameter Description
key_id From GET /v1/api-keys → key_id field

Response 200:

jsonCopy

{
  "message": "API key revoked successfully",
  "key_id":  "3bb67544-bcdb-496f-9ab2-7b9802f710a9"
}

8. Projects

Projects are workspaces that group tasks, threads, files, and artifacts together.

Space Types

Every project is one of two space types, identifiable by its id field:

Space type id prefix Description
Free Space Regular UUID (e.g. df8a2d7c-...) A standard project you create manually. You drive all prompts and tasks.
Autopilot Outbound auto_outbound_<uuid> An AI-managed outbound campaign. A 4-week execution plan is generated at creation. The AI supervisor runs goals autonomously on schedule, including a recurring Weekly Progress Report every Monday at 00:01 (local time).
Autopilot Inbound auto_inbound_<uuid> Reserved for future AI-managed inbound workflows (not yet generally available).

Creating Autopilot Spaces: Autopilot spaces are created from the Starnus platform dashboard, not via POST /v1/projects. The API POST /v1/projects endpoint always creates a Free Space.

Accessing Autopilot Spaces via API: All standard project endpoints (GET, PATCH, DELETE) work on Autopilot spaces. Their tasks, threads, and artifacts are fully accessible.


GET /v1/projects

List all your projects.

Response 200:

jsonCopy

{
  "projects": [\
    {\
      "id":          "df8a2d7c-2eef-4250-99ae-1d167c5e125a",\
      "name":        "Q1 Lead Generation",\
      "description": "Find and qualify B2B leads for ACME Corp's Q1 outreach.",\
      "created_at":  "2026-01-10T09:00:00+00:00",\
      "updated_at":  "2026-03-22T14:00:00+00:00"\
    }\
  ]
}
Field Type Description
id string (UUID) Project identifier — use in all project-scoped endpoints
name string Display name
description string Optional description
created_at string (ISO 8601) Creation timestamp
updated_at string (ISO 8601) Last modification timestamp

POST /v1/projects

Create a new project.

Request body:

jsonCopy

{
  "name":        "Q1 Lead Generation",
  "description": "Find and qualify B2B leads for ACME Corp's Q1 outreach."
}
Field Type Required Description
name string ✅ Project name. Must be non-empty.
description string ❌ Optional description.

Response 201: Returns {"project": {...}} with the same fields as the list.

Errors:

  • 400 validation_error — name is missing or blank

GET /v1/projects/{project_id}

Get a single project.

Response 200: Returns {"project": {...}}.

Errors:

  • 403 forbidden — Project belongs to another account
  • 404 not_found — Project not found

PATCH /v1/projects/{project_id}

Update a project's name or description.

Request body (at least one field required):

jsonCopy

{
  "name":        "Q1 Lead Generation (updated)",
  "description": "Updated description."
}

Response 200: Returns {"project": {...}}.


DELETE /v1/projects/{project_id}

Delete a project permanently.

Response 204: No content.


9. Tasks

Tasks are scheduled or on-demand instructions for the AI to execute. They are separate from real-time executions (which use WebSocket). Use tasks for recurring work, scheduled jobs, or queue-based workflows.

GET /v1/projects/{project_id}/tasks

List tasks in a project. Returns the most recent tasks first.

Query parameters:

Parameter Type Description
status string Filter by status: created, in_progress, done

Response 200:

jsonCopy

{
  "tasks": [\
    {\
      "id":                   "goal_4f3a1b2c5d6e",\
      "request_id":           "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",\
      "project_id":           "df8a2d7c-2eef-4250-99ae-1d167c5e125a",\
      "description":          "Find 50 B2B leads in Berlin",\
      "status":               "done",\
      "due_date":             "2026-04-01T00:00:00+00:00",\
      "recurring":            false,\
      "created_at":           "2026-03-22T08:00:00+00:00"\
    }\
  ]
}
Field Type Description
id string Task identifier. Autopilot weekly report tasks start with goal_wrep_.
request_id string (UUID) Auto-generated correlation ID — matches WebSocket streaming events for this execution
project_id string Project this task belongs to
description string The task prompt
status string created (queued), in_progress, done
due_date string (ISO 8601) When the task should run (for scheduled tasks)
recurring boolean Whether this task repeats
recurrence_interval string Recurrence pattern when recurring is true (e.g. "weekly", "daily")
created_at string (ISO 8601) Creation timestamp
updated_at string (ISO 8601) Last modification timestamp

POST /v1/projects/{project_id}/tasks

Create a new task. The server auto-generates a request_id — you do not supply one.

Request body:

jsonCopy

{
  "description":         "Find 50 B2B leads in Berlin and add them to the Q1 Leads database",
  "due_date":            "2026-04-01T00:00:00+00:00",
  "files":               ["acme_prospects_a1b2c3d4_uuid.csv"],
  "recurring":           true,
  "recurrence_interval": "weekly"
}
Field Type Required Description
description string ✅ The task prompt — what the agent should do
due_date string (ISO 8601) ❌ When to execute. Omit for immediate execution.
files array of strings ❌ File IDs from POST /v1/files/confirm to attach as context
recurring boolean ❌ Set true with a recurrence_interval for recurring tasks
recurrence_interval string ❌ Recurrence pattern: e.g. "daily", "weekly". Alternatively recurrence_time_span (integer minutes) is also accepted.

Response 201:

jsonCopy

{
  "task": {
    "id":          "goal_4f3a1b2c5d6e",
    "request_id":  "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
    "project_id":  "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
    "description": "Find 50 B2B leads in Berlin and add them to the Q1 Leads database",
    "status":      "created",
    "created_at":  "2026-03-22T15:00:00+00:00"
  }
}

request_id is auto-generated. The server creates a UUID for every task. Use it to correlate WebSocket streaming events for this task's execution.

Errors:

  • 400 validation_error — description is missing or blank

PATCH /v1/projects/{project_id}/tasks/{task_id}

Update a task's description, due date, or status.

Request body (at least one field required):

jsonCopy

{
  "description": "Find 50 B2B leads in Amsterdam instead",
  "due_date":    "2026-04-15T00:00:00+00:00",
  "status":      "created"
}

Response 200: Returns {"task": {...}}.


DELETE /v1/projects/{project_id}/tasks/{task_id}

Delete a task.

Response 204: No content.


10. Threads (Execution History)

Threads are the persisted records of each AI execution — the prompt that was sent and the response that was returned. Every WebSocket execution creates a thread.

GET /v1/projects/{project_id}/threads

List execution threads for a project, newest first.

Query parameters:

Parameter Type Default Description
limit integer 50 Max threads to return (max 100)

Response 200:

jsonCopy

{
  "threads": [\
    {\
      "id":         "req_7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",\
      "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a",\
      "prompt":     "Find 5 CEOs of software companies in the Netherlands",\
      "response":   "I found 5 CEOs and have listed them below with their LinkedIn profiles...",\
      "status":     "completed",\
      "created_at": "2026-03-22T15:18:00+00:00"\
    }\
  ],
  "count": 1
}
Field Type Description
id string Thread identifier — equals the request_id from the WebSocket acknowledgement
project_id string Associated project
prompt string The original user input sent via WebSocket
response string The agent's final response. Present for completed, error, and awaiting_input. Absent while still running.
status string running, completed, error, stopped, awaiting_input
credits_consumed number Credits used for this execution (when available)
created_at string (ISO 8601) When the execution started

Threads vs Tasks: Threads are read-only history records. Tasks are schedulable work items. A task execution creates a thread automatically.


GET /v1/threads/{thread_id}

Get a single thread by ID.

Query parameters:

Parameter Type Required Description
project_id string ✅ The project the thread belongs to

Response 200: Returns {"thread": {...}} with the same fields as the list.

Errors:

  • 400 validation_error — project_id query parameter missing
  • 403 forbidden — Thread belongs to another account
  • 404 not_found — Thread not found

DELETE /v1/projects/{project_id}/threads/{thread_id}

Delete a thread (execution history record).

Response 204: No content.


11. Artifacts

Artifacts are structured data outputs created by AI agents — lead databases, research documents, and images. Agents create and update artifacts automatically during execution.

Type Description
database Spreadsheet-like table with named columns and rows
document Markdown text document
image Image file — accessible via time-limited presigned URL

GET /v1/artifacts

List all artifacts for your account.

Query parameters:

Parameter Type Description
project_id string Filter by project
type string Filter by type: database, document, or image

Response 200:

jsonCopy

{
  "artifacts": [\
    {\
      "id":           "art_1d749beff51f",\
      "project_id":   "df8a2d7c-2eef-4250-99ae-1d167c5e125a",\
      "name":         "Berlin Leads Q1 2026",\
      "type":         "database",\
      "description":  "50 qualified B2B leads for Q1 Berlin outreach",\
      "size_bytes":   48200,\
      "row_count":    50,\
      "column_count": 8,\
      "version":      3,\
      "created_at":   "2026-03-22T08:02:43+00:00",\
      "updated_at":   "2026-03-22T09:15:00+00:00"\
    }\
  ]
}
Field Type Description
id string Artifact identifier
project_id string Associated project
name string Display name
type string database, document, or image
description string Short description
size_bytes integer Storage size
row_count integer Number of rows (database type only)
column_count integer Number of columns (database type only)
version integer Version number — incremented on each update
content string / object Inline content for document and database types
download_url string Presigned URL for image type

POST /v1/artifacts

Create a new artifact manually. Agents create artifacts automatically — use this endpoint to create one programmatically.

Request body:

jsonCopy

{
  "project_id":  "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
  "name":        "Berlin Leads Q1 2026",
  "type":        "database",
  "description": "50 qualified B2B leads for Q1 Berlin outreach",
  "content": {
    "columns": [\
      { "id": "col_1", "name": "Company",  "type": "text" },\
      { "id": "col_2", "name": "Email",    "type": "text" },\
      { "id": "col_3", "name": "Status",   "type": "text" }\
    ],
    "rows": [\
      { "id": "row_1", "Company": "GlobalTech GmbH", "Email": "ceo@globaltech.de", "Status": "prospect" }\
    ]
  }
}
Field Type Required Description
project_id string ✅ Project to create the artifact in
name string ✅ Display name
type string ✅ database, document, or image
description string ❌ Short description
content object / string ❌ Initial content — see structure above

Response 201: Returns {"artifact": {...}}.


GET /v1/artifacts/{artifact_id}

Get a single artifact with its full content.

Response 200:

jsonCopy

{
  "artifact": {
    "id":          "art_1d749beff51f",
    "project_id":  "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
    "name":        "Berlin Leads Q1 2026",
    "type":        "database",
    "content": {
      "columns": [\
        { "id": "col_1", "name": "Company",  "type": "text" },\
        { "id": "col_2", "name": "Email",    "type": "text" },\
        { "id": "col_3", "name": "Status",   "type": "text" }\
      ],
      "rows": [\
        { "id": "row_1", "Company": "GlobalTech GmbH", "Email": "ceo@globaltech.de", "Status": "prospect" }\
      ]
    },
    "row_count":   1,
    "version":     3,
    "created_at":  "2026-03-22T08:02:43+00:00",
    "updated_at":  "2026-03-22T09:15:00+00:00"
  }
}

Errors:

  • 404 not_found — Artifact not found or belongs to another account

PATCH /v1/artifacts/{artifact_id}

Update artifact metadata or patch its content (add/delete rows, add columns, update cells).

Mode 1 — Update metadata:

jsonCopy

{
  "name":        "Berlin Leads Q1 2026 (final)",
  "description": "50 qualified leads, enriched with LinkedIn profiles"
}

Mode 2 — Patch database content:

jsonCopy

{
  "add_rows": [\
    { "Company": "GlobalTech GmbH", "Email": "ceo@globaltech.de", "Status": "prospect" }\
  ],
  "delete_rows":  ["row_id_abc123"],
  "add_columns":  [{ "name": "LinkedIn", "type": "text" }],
  "update_cells": [\
    { "row_id": "row_id_abc123", "column_id": "col_3", "value": "qualified" }\
  ]
}
Field Type Description
add_rows array of objects Rows to add. Each object maps column names to values.
delete_rows array of strings Row IDs to delete
add_columns array of objects Columns to add: {"name": "ColName", "type": "text"}
update_cells array of objects {"row_id": "...", "column_id": "...", "value": "..."}

When any patch field is present, content-patch mode is used. Otherwise metadata mode is used.

Response 200: Returns {"artifact": {...}} with version incremented.

Errors:

  • 400 validation_error — No valid patch operations provided
  • 404 not_found — Artifact not found or belongs to another account

POST /v1/artifacts/{artifact_id}/export

Export a database artifact to a downloadable file. Supported formats: xlsx, csv, json.

Request body:

jsonCopy

{
  "format": "xlsx"
}
Field Type Required Description
format string ✅ xlsx, csv, or json

Supported formats only:xlsx, csv, json. Any other value returns 400 validation_error.

Response 200:

jsonCopy

{
  "download_url": "https://users-documents.s3.amazonaws.com/...?X-Amz-...",
  "format":       "xlsx"
}

download_url is a presigned S3 URL valid for ~60 minutes. Fetch it with a simple GET — no authentication header required.

Download example (Python):

pythonCopy

import requests, pathlib

result = requests.post(
    "https://api.starnus.com/v1/artifacts/art_1d749beff51f/export",
    headers={"Authorization": "Bearer sk_live_your_key"},
    json={"format": "xlsx"},
).json()

data = requests.get(result["download_url"]).content
pathlib.Path("leads.xlsx").write_bytes(data)
print(f"Saved {len(data):,} bytes")

Errors:

  • 400 validation_error — format is missing or not one of xlsx, csv, json
  • 404 not_found — Artifact not found or belongs to another account

DELETE /v1/artifacts/{artifact_id}

Delete an artifact permanently.

Response 204: No content.


12. Files

Files are documents you upload to attach to task executions — PDFs, CSVs, prospect lists, research briefs, etc.

Upload flow (3 steps)

1. POST /v1/files/upload-url → get a presigned S3 PUT URL 2. PUT <upload_url> → upload the file bytes directly to S3 3. POST /v1/files/confirm → confirm the upload, receive your permanent file ID

After step 3, use the returned id in the files array when creating a task or sending a WebSocket execution.


POST /v1/files/upload-url

Request a presigned upload URL.

Request body:

jsonCopy

{
  "filename":     "acme_prospects.csv",
  "content_type": "text/csv",
  "project_id":   "df8a2d7c-2eef-4250-99ae-1d167c5e125a"
}
Field Type Required Description
filename string ✅ File name including extension
content_type string ✅ MIME type: text/csv, application/pdf, image/png, etc.
project_id string ❌ Associate with a project

Response 200:

jsonCopy

{
  "upload_url":   "https://users-documents.s3.eu-central-1.amazonaws.com/temp/acme_prospects_a1b2c3d4.csv?...",
  "file_id":      "temp/acme_prospects_a1b2c3d4.csv",
  "filename":     "acme_prospects.csv",
  "content_type": "text/csv"
}
Field Description
upload_url Presigned S3 PUT URL. Valid for ~10 minutes. No Authorization header needed.
file_id Temporary ID — pass to POST /v1/files/confirm after uploading.

S3 Direct Upload

httpCopy

PUT <upload_url>
Content-Type: text/csv

<raw file bytes>

S3 returns HTTP 200 or 204. Do not include your Starnus Authorization header — the URL is self-authenticating. Complete within ~10 minutes of requesting the URL.

Python example:

pythonCopy

import requests

with open("acme_prospects.csv", "rb") as f:
    resp = requests.put(
        upload_url,
        data=f,
        headers={"Content-Type": "text/csv"},
    )
resp.raise_for_status()  # S3 returns 200 on success

POST /v1/files/confirm

Confirm the upload is complete. This moves the file to permanent storage and returns your permanent file ID.

Request body:

jsonCopy

{
  "file_id":    "temp/acme_prospects_a1b2c3d4.csv",
  "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a"
}
Field Type Required Description
file_id string ✅ The file_id from POST /v1/files/upload-url
project_id string ❌ Associate the file with a project

Response 200:

jsonCopy

{
  "files": [\
    {\
      "id":     "acme_prospects_a1b2c3d4.csv",\
      "status": "valid"\
    }\
  ]
}
Field Description
id Your permanent file ID — use this in files arrays. Contains no internal path or user ID.
status valid = upload confirmed and ready to use

GET /v1/files

List uploaded files.

Query parameters:

Parameter Type Description
project_id string Filter by project

Response 200:

jsonCopy

{
  "files": [\
    {\
      "id":           "acme_prospects_a1b2c3d4.csv",\
      "filename":     "acme_prospects.csv",\
      "content_type": "text/csv",\
      "size_bytes":   24500,\
      "status":       "valid",\
      "created_at":   "2026-03-22T14:00:00+00:00"\
    }\
  ]
}

GET /v1/files/{file_id}/download

Get a download URL for a file. Returns HTTP 302 redirect to a presigned URL.

Parameter Description
file_id The id returned by POST /v1/files/confirm

Response 302: Your HTTP client follows the redirect automatically. The presigned URL is valid for ~15 minutes.

HTTP/1.1 302 Found Location: https://users-documents.s3.amazonaws.com/...?X-Amz-...

Python requests: Follows redirects by default. You receive the file content with status 200 — no special handling needed.

Errors:

  • 404 not_found — File not found or belongs to another account

13. Integrations

Integrations connect Starnus to external services so AI agents can act on them (send emails, update Notion, search LinkedIn, etc.).

Slug Service Auth method
gmail Gmail Composio OAuth
notion Notion Composio OAuth
slack Slack Composio OAuth
google_sheets Google Sheets Composio OAuth
google_calendar Google Calendar Composio OAuth
google_docs Google Docs Composio OAuth
googledrive Google Drive Composio OAuth
hubspot HubSpot Composio OAuth
linear Linear Composio OAuth
airtable Airtable Composio OAuth
outlook Outlook Composio OAuth
onedrive OneDrive Composio OAuth
x X (Twitter) Composio OAuth
linkedin LinkedIn Unipile OAuth
instagram Instagram Unipile OAuth
email SMTP outbound email Smartlead credentials

GET /v1/integrations

List all integrations with their connection status.

Response 200:

jsonCopy

{
  "integrations": [\
    {\
      "name":         "gmail",\
      "app_type":     "composio",\
      "connected":    true,\
      "connected_at": null\
    },\
    {\
      "name":         "linkedin",\
      "app_type":     "unipile",\
      "connected":    false,\
      "connected_at": null\
    }\
  ]
}
Field Type Description
name string Integration slug — use in other endpoints
app_type string composio, unipile, or smartlead
connected boolean Whether the integration is connected
connected_at string or null When it was connected

GET /v1/integrations/{name}/status

Get the status of a single integration.

Response 200: Returns {"integration": {...}}.


POST /v1/integrations/{name}/connect

Start the OAuth authorization flow. Returns a URL to open in a browser.

Response 200:

jsonCopy

{
  "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}

Open auth_url in a browser. After the user authorizes, the integration becomes connected.

Browser-only: Cannot be completed programmatically. The URL must be opened in a browser.


DELETE /v1/integrations/{name}/disconnect

Disconnect an integration and revoke access.

Response 204: No content.


POST /v1/integrations/email

Add an SMTP outbound email account (via Smartlead).

Request body:

jsonCopy

{
  "email":    "outreach@acme.com",
  "name":     "ACME Outreach",
  "password": "app-password-here",
  "provider": "gmail"
}
Field Type Required Description
email string ✅ Email address
name string ✅ Display name
password string ✅ App password (not your login password)
provider string ✅ gmail or outlook

Response 201:{"message": "Email account added successfully"}


DELETE /v1/integrations/email/{email}

Remove a connected email account.

Response 204: No content.


14. Billing

GET /v1/billing/balance

Get your current credit balance and subscription status.

Response 200:

jsonCopy

{
  "balance": {
    "credits_remaining":   180000.0,
    "credits_limit":       350000.0,
    "tier":                "pro+ monthly",
    "subscription_status": "active"
  }
}

GET /v1/billing/plans

List available subscription plans.

Query parameters:

Parameter Type Default Description
currency string eur Currency: eur, usd, gbp

Response 200:

jsonCopy

{
  "plans": [\
    {\
      "name":     "Pro",\
      "slug":     "pro_monthly",\
      "price":    50.0,\
      "interval": "monthly",\
      "currency": "eur",\
      "credits":  50000\
    },\
    {\
      "name":     "Pro+",\
      "slug":     "pro+_monthly",\
      "price":    125.0,\
      "interval": "monthly",\
      "currency": "eur",\
      "credits":  200000\
    },\
    {\
      "name":     "Ultra",\
      "slug":     "ultra_monthly",\
      "price":    250.0,\
      "interval": "monthly",\
      "currency": "eur",\
      "credits":  500000\
    }\
  ]
}

GET /v1/billing/invoices

List past invoices.

Response 200:

jsonCopy

{
  "invoices": [\
    {\
      "amount":      125.0,\
      "currency":    "eur",\
      "description": "Starnus Pro+ Monthly",\
      "status":      "paid",\
      "date":        "2026-03-01T00:00:00+00:00",\
      "receipt_url": "https://pay.stripe.com/receipts/..."\
    }\
  ]
}

POST /v1/billing/checkout

Create a Stripe checkout session to subscribe to a new plan.

Request body:

jsonCopy

{
  "plan":     "pro_plus",
  "interval": "monthly"
}

Response 200:{"checkout_url": "https://checkout.stripe.com/..."}

Open checkout_url in a browser to complete payment.


POST /v1/billing/upgrade

Upgrade an existing subscription (no browser redirect).

Request body:{"plan": "ultra"}

Response 200:{"message": "Subscription upgraded successfully"}


POST /v1/billing/cancel

Cancel your subscription at the end of the current period.

Response 200:{"message": "Subscription cancelled successfully"}


POST /v1/billing/activate-trial

Activate the free trial.

Response 200:{"message": "Trial activated successfully"}


15. Usage

GET /v1/usage

Get per-execution credit consumption records.

Query parameters:

Parameter Type Default Description
limit integer 50 Max records (max 100)

Response 200:

jsonCopy

{
  "usage": {
    "credits_consumed": 14888.0,
    "records": [\
      {\
        "credits_consumed": 463.0,\
        "project_id":       "df8a2d7c-2eef-4250-99ae-1d167c5e125a",\
        "project_name":     "Q1 Lead Generation",\
        "created_at":       "2026-03-22T08:02:43+00:00"\
      }\
    ],
    "count":    50,
    "has_more": true
  }
}
Field Description
credits_consumed (top level) Total across all returned records
records[].credits_consumed Credits for this specific execution
has_more true if more records exist — reduce limit and paginate

16. Triggers

Triggers fire tasks automatically on a schedule or in response to integration events.

GET /v1/triggers

Response 200:

jsonCopy

{
  "triggers": [\
    {\
      "id":          "a3f7b2e1-9d4c-4a1e-8b5f-123456789abc",\
      "description": "Summarize project status every Monday",\
      "type":        "webhook",\
      "schedule":    "weekly",\
      "active":      true,\
      "last_run":    "2026-03-17T09:00:00+00:00",\
      "next_run":    "2026-03-24T09:00:00+00:00",\
      "created_at":  "2026-03-22T10:00:00+00:00"\
    }\
  ],
  "count": 1
}

POST /v1/triggers

Create a trigger.

Request body:

jsonCopy

{
  "description": "Summarize project status every Monday",
  "type":        "schedule",
  "schedule":    "weekly"
}
Field Type Required Description
description string ✅ Used as the task prompt when the trigger fires
type string ❌ schedule (default) or webhook
schedule string ❌ daily, weekly, or monthly (for schedule type)
integration string ❌ Integration slug for webhook type (e.g. gmail)

Response 201: Returns {"trigger": {...}}.


DELETE /v1/triggers/{trigger_id}

Delete a trigger. It stops firing immediately.

Response 204: No content.


17. Updates

Platform update notifications from the Starnus team.

GET /v1/updates

Get unseen platform updates.

Response 200:

jsonCopy

{
  "updates": [\
    {\
      "id":         "a1b2c3d4-e5f6-7890-abcd-ef1234567890",\
      "title":      "New: WebSocket streaming supports artifact events",\
      "body":       "The `artifact_updated` event is now emitted in real time when an agent creates or modifies an artifact.",\
      "category":   "feature",\
      "summary":    "Real-time artifact notifications via WebSocket",\
      "action_url": null,\
      "seen":       false,\
      "created_at": "2026-03-15T09:00:00+00:00"\
    }\
  ]
}

POST /v1/updates/{update_id}/seen

Mark an update as seen.

Response 200:{"message": "Update marked as seen"}


18. Support

POST /v1/support

Submit a support request.

Request body:

jsonCopy

{
  "subject":  "Gmail integration not connecting",
  "message":  "When I click Connect Gmail I receive an error. Steps: 1. Go to Integrations. 2. Click Connect next to Gmail. 3. Error appears.",
  "category": "technical"
}
Field Type Required Description
subject string ✅ Short issue description
message string ✅ Detailed description. Include reproduction steps for bugs.
category string ❌ general, billing, technical, feature_request, bug_report, other. Default: general

Response 200:{"message": "Support request submitted successfully"}

Errors:

  • 400 validation_error — subject or message is blank

19. Expert Sessions

GET /v1/expert/sessions

List your booked and completed expert sessions.

Response 200:

jsonCopy

{
  "sessions": [\
    {\
      "session_id":       "sess_abc123",\
      "status":           "scheduled",\
      "type":             "free",\
      "scheduled_at":     "2026-04-01T14:00:00+00:00",\
      "duration_minutes": 30,\
      "notes":            "Onboarding and integration setup"\
    }\
  ]
}

POST /v1/expert/sessions

Request an expert session.

Request body:{"type": "free"}

Response 201: Returns {"session": {...}}.

Browser flow: After calling this endpoint, open the returned booking_url in a browser to complete scheduling.


20. WebSocket Streaming

WebSocket is the primary channel for real-time AI task execution. Connect once, send tasks, and stream supervisor progress, agent updates, and final results as they happen.

WebSocket URL:

wss://0vxd7s295f.execute-api.eu-central-1.amazonaws.com/prod?api_key=sk_live_your_key_here

Use the SDK (client.executions.create()) for the simplest integration. See §21 Code Examples for raw WebSocket examples.


20.1 Connection & Authentication

Append your API key as a query parameter:

?api_key=sk_live_your_api_key_here

The connection is rejected immediately (HTTP 401) if the key is invalid. The key is never transmitted in a WebSocket message body — only in the initial query string.


20.2 Sending a task execution

Send a send_message action after connecting:

jsonCopy

{
  "action": "send_message",
  "payload": {
    "type":       "process",
    "user_input": "Find 5 CEOs of software companies in the Netherlands",
    "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
    "files":      ["acme_report_uuid.pdf"],
    "integrations": {}
  }
}
Field Type Required Description
type string ✅ Always "process" for task execution
user_input string ✅ The task prompt
project_id string ✅ Project to run the task in
files array ❌ File IDs from POST /v1/files/confirm. The server resolves them to S3 paths automatically — never pass S3 URIs directly.
integrations object ❌ Pass {} or omit entirely

request_id is auto-generated by the server. Do not include it in your payload. The server creates a UUID, injects it, and echoes it back in the acknowledgement frame. All subsequent events for this execution carry the same request_id.

Immediate acknowledgement frame (server → client):

jsonCopy

{
  "message":    "Success",
  "request_id": "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e"
}

Save request_id — every streaming event carries it, and you need it to stop the execution.


20.3 Received event types

Event type Description
supervisor_update Progress text from the AI supervisor — what it is thinking or planning
user_update A message explicitly directed at the end user (formatted for display)
component_input The supervisor's agent execution plan — which agents will run and in what order
component_update Progress from a running agent. May include "wait_for_user": true
process_result Final result.status is completed, error, stopped, or awaiting_input
process_todo_result Final result for a task triggered via the trigger/scheduler system
stop_process_result Confirmation that a stop request was processed
scheduling Confirmation that a task was deferred or scheduled
artifact_updated An artifact was created or modified during execution
error Server-side error during execution
pong Heartbeat response

supervisor_update example:

jsonCopy

{
  "type":             "supervisor_update",
  "project_id":       "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
  "request_id":       "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
  "supervisor_update": "Searching for CEOs of software companies in the Netherlands…"
}

component_input example (execution plan):

jsonCopy

{
  "type":       "component_input",
  "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
  "request_id": "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
  "mode":       "agentic",
  "execution_plan": {
    "total_tasks": "1",
    "batches": [\
      {\
        "batch_id": "batch_1",\
        "tasks": [\
          {\
            "task_id":      "task_1",\
            "description":  "Find 5 CEOs of NL software companies",\
            "component_id": "sb1-lead-finder-agent"\
          }\
        ]\
      }\
    ]
  }
}

component_update example:

jsonCopy

{
  "type":             "component_update",
  "project_id":       "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
  "request_id":       "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
  "task_id":          "task_1",
  "component_update": "component sb1-lead-finder-agent-prod has started processing"
}

process_result example (final):

jsonCopy

{
  "type":       "process_result",
  "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
  "request_id": "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
  "status":     "completed",
  "response":   "I found 5 CEOs and created the Leads artifact with their names and LinkedIn URLs.",
  "timestamp":  "2026-03-22T15:30:00+00:00"
}

status values on process_result:

Status Meaning
completed Task finished successfully. response contains the final answer.
awaiting_input The agent paused and is waiting for your next message. Start a new execution to continue.
error Execution failed. response contains the error description.
stopped You sent a stop_process and it was processed.

20.4 Stopping an execution

jsonCopy

{
  "action": "stop_process",
  "payload": {
    "type":       "stop_process",
    "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
    "request_id": "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e"
  }
}

The server responds with a stop_process_result event.


20.5 Responding to a paused agent

When an agent sends "wait_for_user": true in a component_update, it is waiting for input:

jsonCopy

{
  "type":           "component_update",
  "request_id":     "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
  "component_id":   "comp_linkedin_outreach",
  "wait_for_user":  true,
  "message":        "Should I proceed with option 1 (bulk) or option 2 (personalized)?"
}

Respond with:

jsonCopy

{
  "action": "user_response",
  "payload": {
    "type":         "user_update",
    "request_id":   "7e2a1c3d-4f5b-6a7c-8d9e-0f1a2b3c4d5e",
    "project_id":   "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
    "component_id": "comp_linkedin_outreach",
    "message":      "Option 2 — personalized subject lines.",
    "timestamp":    "2026-03-22T15:00:00+00:00"
  }
}

20.6 Heartbeat / keepalive

Send a ping every 15 seconds to prevent the connection from timing out:

jsonCopy

{ "type": "ping", "timestamp": 1742654400000 }

The server responds with:

jsonCopy

{ "type": "pong", "timestamp": 1742654400000 }

21. Code Examples

21.1 Complete Python setup (raw WebSocket with full logging)

This example connects over raw WebSocket without the SDK, logs every sent/received frame to a JSON file, and handles all event types.

pythonCopy

"""
starnus_ws_example.py — raw WebSocket execution with full logging.
Install: pip install websocket-client requests
"""

import json
import pathlib
import threading
import time
import uuid
from datetime import datetime, timezone

import requests
import websocket

# ── Config ────────────────────────────────────────────────────────────────────
API_KEY    = "sk_live_your_key_here"
REST_BASE  = "https://api.starnus.com/v1"
WS_URL     = "wss://0vxd7s295f.execute-api.eu-central-1.amazonaws.com/prod"
PROJECT_ID = "df8a2d7c-2eef-4250-99ae-1d167c5e125a"   # replace with your project

REST_HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type":  "application/json",
}

# ── Logging ───────────────────────────────────────────────────────────────────
LOG_FILE = pathlib.Path(f"starnus_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
_log: dict = {"events": []}

def _flush():
    LOG_FILE.write_text(json.dumps(_log, indent=2, default=str))

def log_http(direction: str, method: str, url: str, payload=None, response=None, status=None):
    entry = {
        "ts":        datetime.now(timezone.utc).isoformat(),
        "kind":      "http",
        "direction": direction,
        "method":    method,
        "url":       url,
    }
    if payload:  entry["payload"]  = payload
    if response: entry["response"] = response
    if status:   entry["status"]   = status
    _log["events"].append(entry)
    _flush()
    arrow = "→" if direction == "sent" else "←"
    print(f"\n[HTTP {arrow}] {method} {url}")
    if payload:  print(f"  SENT:     {json.dumps(payload, default=str)[:300]}")
    if response: print(f"  RECEIVED: {json.dumps(response, default=str)[:300]}")

def log_ws(direction: str, data: dict):
    entry = {
        "ts":        datetime.now(timezone.utc).isoformat(),
        "kind":      "websocket",
        "direction": direction,
        "data":      data,
    }
    _log["events"].append(entry)
    _flush()
    arrow = "→" if direction == "sent" else "←"
    msg_type = data.get("type") or data.get("action", "")
    print(f"\n[WS {arrow}] type={msg_type}")
    print(f"  {json.dumps(data, default=str)[:400]}")

# ── REST helpers ──────────────────────────────────────────────────────────────
def api_get(path: str):
    url = f"{REST_BASE}{path}"
    r = requests.get(url, headers=REST_HEADERS)
    result = r.json()
    log_http("received", "GET", url, response=result, status=r.status_code)
    return result

def api_post(path: str, body: dict):
    url = f"{REST_BASE}{path}"
    log_http("sent", "POST", url, payload=body)
    r = requests.post(url, headers=REST_HEADERS, json=body)
    result = r.json()
    log_http("received", "POST", url, response=result, status=r.status_code)
    r.raise_for_status()
    return result

# ── WebSocket execution ───────────────────────────────────────────────────────
request_id   = None   # filled from server acknowledgement
result_event = threading.Event()
final_result = {}

def on_open(ws):
    print("\n[WS] Connected")
    payload = {
        "action": "send_message",
        "payload": {
            "type":         "process",
            "user_input":   "Find 5 CEOs of software companies in the Netherlands",
            "project_id":   PROJECT_ID,
            "files":        [],
            "integrations": {},
        },
    }
    # NOTE: Do NOT include request_id — the server auto-generates it
    log_ws("sent", payload)
    ws.send(json.dumps(payload))

# Start heartbeat thread
    def heartbeat():
        while True:
            time.sleep(15)
            if ws.sock and ws.sock.connected:
                ping = {"type": "ping", "timestamp": int(time.time() * 1000)}
                log_ws("sent", ping)
                ws.send(json.dumps(ping))
    threading.Thread(target=heartbeat, daemon=True).start()

def on_message(ws, raw):
    global request_id
    data = json.loads(raw)
    log_ws("received", data)

msg_type = data.get("type", "")

# Capture server-assigned request_id from acknowledgement
    if data.get("message") == "Success" and data.get("request_id"):
        request_id = data["request_id"]
        print(f"\n[WS] Execution started. request_id = {request_id}")
        return

if msg_type == "supervisor_update":
        print(f"[supervisor] {data.get('supervisor_update', '')}")

elif msg_type == "user_update":
        print(f"[update]     {data.get('message', '')}")

elif msg_type in ("component_input", "component_todo_input"):
        plan = data.get("execution_plan", {})
        tasks = [t.get("description", "") for b in plan.get("batches", []) for t in b.get("tasks", [])]
        print(f"[plan]       {'; '.join(tasks)}")

elif msg_type == "component_update":
        if data.get("wait_for_user"):
            print(f"[PAUSED ⏸]  Agent waiting: {data.get('message', '')}")
            # Respond here if needed:
            # ws.send(json.dumps({"action": "user_response", "payload": {...}}))
        else:
            print(f"[agent]      {data.get('component_update', '')}")

elif msg_type in ("process_result", "process_todo_result"):
        final_result.update(data)
        print(f"\n✅ Result ({data.get('status')}):\n{data.get('response', '')}")
        result_event.set()
        ws.close()

elif msg_type == "stop_process_result":
        final_result.update(data)
        result_event.set()
        ws.close()

elif msg_type == "error":
        final_result.update(data)
        print(f"\n❌ Error: {data.get('error', 'Unknown')}")
        result_event.set()
        ws.close()

elif msg_type == "pong":
        print("[WS] Pong ♥")

elif msg_type == "artifact_updated":
        print(f"[artifact]   Created/updated: {data.get('artifact_id')}")

def on_error(ws, error):
    print(f"\n[WS] Error: {error}")

def on_close(ws, code, msg):
    print(f"\n[WS] Closed (code={code})")
    result_event.set()

def run():
    ws_url_with_key = f"{WS_URL}?api_key={API_KEY}"
    ws = websocket.WebSocketApp(
        ws_url_with_key,
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close,
    )
    thread = threading.Thread(target=ws.run_forever, kwargs={"ping_interval": 0})
    thread.daemon = True
    thread.start()

# Wait up to 10 minutes for a result
    result_event.wait(timeout=600)
    print(f"\nLog saved to {LOG_FILE}")
    return final_result

if __name__ == "__main__":
    result = run()
    print(f"\nFinal status: {result.get('status')}")

21.2 File upload + task with attachment (Python)

pythonCopy

"""
Upload a PDF and run a task that references it.
"""
import pathlib, requests

API_KEY   = "sk_live_your_key_here"
REST_BASE = "https://api.starnus.com/v1"
H         = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

# 1. Request presigned upload URL
resp = requests.post(f"{REST_BASE}/files/upload-url", headers=H, json={
    "filename":     "acme_report.pdf",
    "content_type": "application/pdf",
    "project_id":   "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
})
resp.raise_for_status()
upload_data = resp.json()
upload_url  = upload_data["upload_url"]
temp_id     = upload_data["file_id"]
print(f"Got upload URL. temp file_id = {temp_id}")

# 2. Upload directly to S3 — no Starnus auth header
with open("acme_report.pdf", "rb") as f:
    s3_resp = requests.put(upload_url, data=f, headers={"Content-Type": "application/pdf"})
s3_resp.raise_for_status()
print("File uploaded to S3 ✓")

# 3. Confirm the upload — get permanent file ID
resp = requests.post(f"{REST_BASE}/files/confirm", headers=H, json={
    "file_id":    temp_id,
    "project_id": "df8a2d7c-2eef-4250-99ae-1d167c5e125a",
})
resp.raise_for_status()
permanent_id = resp.json()["files"][0]["id"]
print(f"Permanent file ID: {permanent_id}")

# 4. Create a task with the file attached
# (or use the WebSocket example above and pass permanent_id in the files array)
resp = requests.post(
    f"{REST_BASE}/projects/df8a2d7c-2eef-4250-99ae-1d167c5e125a/tasks",
    headers=H,
    json={
        "description": "Summarize the key findings from the attached ACME report and create a briefing artifact",
        "files":       [permanent_id],
    },
)
resp.raise_for_status()
task = resp.json()["task"]
print(f"Task created: {task['id']} — request_id: {task['request_id']}")

21.3 Export artifact as Excel, CSV, and JSON (Python)

pythonCopy

"""
Export the same artifact in all three supported formats.
"""
import pathlib, requests

API_KEY     = "sk_live_your_key_here"
REST_BASE   = "https://api.starnus.com/v1"
ARTIFACT_ID = "art_1d749beff51f"
H           = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

for fmt, filename in [("xlsx", "leads.xlsx"), ("csv", "leads.csv"), ("json", "leads.json")]:
    # Request export
    resp = requests.post(
        f"{REST_BASE}/artifacts/{ARTIFACT_ID}/export",
        headers=H,
        json={"format": fmt},
    )
    resp.raise_for_status()
    download_url = resp.json()["download_url"]

# Download the file — no auth header needed for the presigned URL
    file_resp = requests.get(download_url)
    file_resp.raise_for_status()
    pathlib.Path(filename).write_bytes(file_resp.content)
    print(f"Saved {filename} ({len(file_resp.content):,} bytes)")

Supported export formats:xlsx, csv, json — nothing else. Requesting "excel", "markdown", "xls", etc. returns 400 validation_error.


21.4 SDK quickstart (recommended)

pythonCopy

"""
pip install starnus
"""
import pathlib, requests
import starnus

client = starnus.Client(api_key="sk_live_your_key_here")

# ── Create a project ──────────────────────────────────────────────────────────
project = client.projects.create(name="NL CEO Leads")
pid     = project["id"]
print(f"Project: {pid}")

# ── Run an execution and stream events ────────────────────────────────────────
execution = client.executions.create(
    project_id=pid,
    prompt="Find 5 CEOs of software companies in the Netherlands. Return names, companies, and LinkedIn URLs.",
)

for event in execution.stream():
    if event.type == "supervisor_update":
        print(f"[supervisor] {event.message}")
    elif event.type == "plan":
        print(f"[plan]       {event.message}")
    elif event.type == "component_update":
        print(f"[agent]      {event.message}")
    elif event.type == "waiting_for_user":
        answer = input(f"[Agent asks] {event.message}\nYour reply: ")
        execution.send_update(answer, event.component_id)
    elif event.type == "result":
        print(f"\n✅ Done ({event.status})\n{event.message}")
        break

# ── Retrieve and export the created artifact ──────────────────────────────────
arts = client.artifacts.list(project_id=pid)
for art in arts["artifacts"]:
    if art["type"] == "database":
        print(f"Artifact: {art['name']} ({art['row_count']} rows)")

# Export as Excel
        export = client.artifacts.export(artifact_id=art["id"], format="xlsx")
        data   = requests.get(export["download_url"]).content
        pathlib.Path("nl_ceo_leads.xlsx").write_bytes(data)
        print(f"Saved nl_ceo_leads.xlsx")

# ── Read conversation history ─────────────────────────────────────────────────
threads = client.threads.list(project_id=pid)
for thread in threads["threads"]:
    print(f"\n[{thread['status']}] {thread['prompt'][:80]}")
    if thread.get("response"):
        print(f"  → {thread['response'][:200]}")

# ── Clean up ──────────────────────────────────────────────────────────────────
client.projects.delete(pid)

21.5 Common errors and how to fix them

Error Cause Fix
401 authentication_error API key missing, wrong, or revoked Check Authorization: Bearer sk_live_... header. Create a new key if needed.
400 validation_error: format must be one of: xlsx, json, csv Wrong export format (e.g. "excel", "markdown") Use exactly "xlsx", "csv", or "json"
400 validation_error: name is required Missing name when creating a project Include "name" in the POST body
404 not_found Resource doesn't exist or belongs to another account Verify the ID. The API returns 404 (not 403) for security on ownership checks.
429 rate_limit_exceeded Too many requests Wait until X-RateLimit-Reset and retry with exponential backoff
WS action not routed Sent wrong action key over WebSocket Use exactly "send_message", "stop_process", or "user_response"
No WebSocket events received request_id included in payload Remove request_id from your send payload — the server auto-generates it

22. OpenAPI Specification

The full OpenAPI 3.0 spec is available at:

GET https://api.starnus.com/v1/openapi.json

No authentication required.

Import into Postman:

  1. Open Postman → Import → Link
  2. Paste https://api.starnus.com/v1/openapi.json
  3. All endpoints are imported with correct paths and schemas

View in Swagger UI (browser):

Open this URL:

https://petstore.swagger.io/?url=https://api.starnus.com/v1/openapi.json

Or use Redoc for a cleaner read-only view.

Import into Insomnia:

  1. Create → Import → From URL
  2. Paste https://api.starnus.com/v1/openapi.json

23. BYOK Keys (Bring Your Own LLM Key)

Register your own LLM provider API keys so Starnus agents use your keys instead of the platform's shared pool. Useful for organizations with existing provider contracts or usage tracking requirements.

Supported providers:anthropic, openai, gemini, perplexity

Authentication: all /v1/byok-keys routes accept either a Starnus API key (Bearer header) or a Cognito session token (frontend).


GET /v1/byok-keys

List all registered BYOK keys for your account.

Response 200:

jsonCopy

{
  "keys": [\
    {\
      "provider":    "anthropic",\
      "key_prefix":  "sk-ant-api03-",\
      "enabled":     true,\
      "created_at":  "2026-03-01T10:00:00+00:00",\
      "updated_at":  "2026-03-01T10:00:00+00:00"\
    }\
  ]
}
Field Type Description
provider string LLM provider: anthropic, openai, gemini, or perplexity
key_prefix string First 10 characters of the registered key (for display only)
enabled boolean Whether this key is currently active
created_at string (ISO 8601) When the key was registered
updated_at string (ISO 8601) Last modification timestamp

The plaintext key is never returned — it is encrypted with KMS on registration and never exposed again.


POST /v1/byok-keys

Register a new BYOK key (or replace the existing key for that provider).

Request body:

jsonCopy

{
  "provider": "anthropic",
  "api_key":  "sk-ant-api03-your-key-here"
}
Field Type Required Description
provider string ✅ One of: anthropic, openai, gemini, perplexity
api_key string ✅ Your plaintext API key — stored encrypted; never returned again

Response 201:

jsonCopy

{
  "message":    "BYOK key registered successfully",
  "provider":   "anthropic",
  "key_prefix": "sk-ant-api03-"
}

Errors:

  • 400 validation_error — provider is missing or invalid
  • 400 validation_error — api_key is too short for the provider

PATCH /v1/byok-keys/{provider}

Enable or disable an existing BYOK key without replacing it.

Request body:

jsonCopy

{ "enabled": false }

Response 200:{"message": "BYOK key updated", "provider": "anthropic", "enabled": false}


DELETE /v1/byok-keys/{provider}

Permanently remove a registered BYOK key for a provider.

Path parameter:provider — one of anthropic, openai, gemini, perplexity

Response 200:{"message": "BYOK key deleted successfully"}

Errors:

  • 404 not_found — No key registered for this provider