Siphondocs
Reference

REST API

The /api/v1 surface — API-key authenticated, org-scoped. Full endpoint reference with request/response shapes.

The control plane exposes a REST surface under /api/v1. Requests authenticate with an API key and are org-scoped — a key only ever sees its own organization's data; ids from another tenant return 404, never a leak.

New to the API? Start with the REST API quickstart for a runnable end-to-end example. This page is the exhaustive reference.

Base URL

All paths are relative to your deployment's origin.

https://your-siphon.app/api/v1

The local dev server defaults to http://localhost:3100.

Authentication

Every /api/v1 endpoint (except the OAuth callback and health probes) requires an API key. Mint one in the app under Settings → API keys, or via POST /v1/api-keys. Keys are prefixed ck_, hashed at rest, org-scoped, and revocable.

Send it as a bearer token (preferred) or the x-api-key header:

# Bearer
curl https://your-siphon.app/api/v1/connections \
  -H "Authorization: Bearer ck_your_key"

# Equivalent
curl https://your-siphon.app/api/v1/connections \
  -H "x-api-key: ck_your_key"

The org is resolved from the verified key, never from a client-supplied header — there is no way to act on another org's behalf.

Keep the key server-side

An API key is org-scoped and long-lived — never ship it to a browser. For customer-facing UI, mint a short-lived embed token instead, which is scoped to a single connection.

Conventions

Responses

Success responses return the resource directly — there is no { data: ... } envelope. Only errors are wrapped.

// GET /v1/connections/con_123  → 200
{ "id": "con_123", "displayName": "Acme HubSpot", "status": "active", /* … */ }

Errors

Every error is a JSON object with an error field carrying a machine-readable code and a human message. Validation errors additionally include an issues array.

// 400
{
  "error": {
    "code": "validation",
    "message": "connection params are invalid",
    "issues": [
      { "code": "schema", "path": "params.instanceUrl", "message": "required param 'instanceUrl' is missing" }
    ]
  }
}
HTTPerror.codeMeaning
400validationMalformed body, bad spec, or params that fail their rules. Carries issues.
400(none)Pre-service error — invalid JSON, or a missing required query param.
401unauthorizedMissing, malformed, revoked, or expired credential.
402plan_limitThe action would exceed the org's plan cap (e.g. active connections).
404not_foundResource absent or owned by another org.
409conflictState conflict — e.g. syncing a non-active connection, publishing twice.
409immutableAttempt to mutate a published (frozen) resource.
500internalUnexpected server error.

An issue has the shape { code, path, message }, where code is one of schema · duplicate_key · duplicate_assign_to · unresolved_dependency · cycle.

Status codes

CodeUsed for
200Reads and state toggles (activate, pause, publish, requeue).
201Resource creation (spec draft, connection, schedule, API key).
202Accepted async work — sync, replay, webhook redeliver. Poll the returned run.

Filtering

List endpoints accept filters as query params (documented per endpoint). There is no cursor pagination on the /v1 surface today; list endpoints return the full org-scoped set, and webhook-deliveries takes a limit.


Specs

A spec is the declarative connector definition. Drafts are mutable; publishing freezes a version so connections can pin to it. See Connector Spec for the document schema.

POST /v1/specs

Create (and validate) a new draft spec version. The body is a full ConnectorSpec; it is validated before it lands.

  • Auth: API key
  • Body: a ConnectorSpec document — metadata.key, metadata.name, metadata.version (semver), plus connectionParams, resources, auth, etc. orgId is taken from the key and ignored if present in the body.
  • 201 → a SpecVersionRow with status: "draft".
  • Errors: 400 validation (with issues) if the spec is invalid; 409 conflict if that version already exists for the key.
curl -X POST https://your-siphon.app/api/v1/specs \
  -H "Authorization: Bearer ck_your_key" \
  -H "Content-Type: application/json" \
  --data @connector.json

POST /v1/specs/:key/validate

Validate a candidate spec without saving. Pure function — no persistence.

  • Auth: API key
  • Body: a candidate ConnectorSpec.
  • 200 → a ValidationResult: either { "valid": true, "spec": { … } } or { "valid": false, "issues": [ … ] }.

An invalid spec still returns 200 — validity is carried in the body (valid: false), not the HTTP status. Check valid before trusting the result.

POST /v1/specs/:key/versions/:version/publish

Publish a draft, making it immutable and available to pin.

  • Auth: API key
  • Body: none.
  • 200 → the SpecVersionRow with status: "published" and publishedAt set.
  • Errors: 404 not_found if the spec/version is unknown; 409 conflict if it is already published.

POST /v1/specs/transform-preview

Run a spec's JSONata transform against a sample record and see the output — the same engine the runtime uses. Handy while authoring.

  • Auth: API key
  • Body:
    {
      "expression": "…jsonata…",
      "record": { /* sample input record */ },
      "lookups": { /* optional: named lookup tables */ }
    }
  • 200 → the transformed record (Record<string, unknown>).
  • Errors: 400 validation (transform failed: …) if the expression throws.

Connections

A connection is one customer's configured instance of a spec — its params, encrypted secrets, pinned spec version, and lifecycle state. See Concepts.

GET /v1/connections

List every connection in the org.

POST /v1/connections

Create a connection. Params are validated against the pinned (published) spec version; secrets are encrypted and stored separately (never echoed back). The connection lands in pending.

  • Auth: API key

  • Body:

    FieldTypeNotes
    workspaceIdstringTarget workspace (dev/staging/prod).
    specKeystringWhich spec.
    specVersionstringMust be a published version.
    displayNamestringHuman label.
    externalCustomerIdstring (optional)Your id for the end customer.
    paramsobjectNon-secret connection params (e.g. instance URL).
    secretsobjectSecret params — encrypted at rest, never returned.

    orgId is injected from the key.

  • 201 → a ConnectionRow (status: "pending"). No secrets in the response.

  • Errors: 404 not_found (unknown spec/version), 409 conflict (version not published), 400 validation (params/secrets fail the spec's rules).

curl -X POST https://your-siphon.app/api/v1/connections \
  -H "Authorization: Bearer ck_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "prod",
    "specKey": "hubspot",
    "specVersion": "1.2.0",
    "displayName": "Acme HubSpot",
    "externalCustomerId": "acme",
    "params": { "instanceUrl": "https://api.hubapi.com" },
    "secrets": { "apiKey": "…" }
  }'

GET /v1/connections/:id

Read one connection.

  • Auth: API key
  • 200 → a ConnectionRow.
  • Errors: 404 not_found (absent or another org's).

POST /v1/connections/:id/sync

Trigger a manual sync of one resource. Returns immediately with the created run — poll GET /v1/runs/:id for completion.

  • Auth: API key
  • Body: { "resource": "<resourceKey>" }
  • 202 → a RunRow.
  • Errors: 404 not_found (connection or pinned spec missing); 409 conflict if the connection is not active, or its effective spec (with overrides) is invalid.
curl -X POST https://your-siphon.app/api/v1/connections/con_123/sync \
  -H "Authorization: Bearer ck_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "resource": "contacts" }'

POST /v1/connections/:id/activate

Move a connection to active. Idempotent — returns as-is if already active. Enforces the org's plan cap on active connections.

  • Auth: API key
  • Body: none.
  • 200 → the ConnectionRow (status: "active").
  • Errors: 404 not_found; 402 plan_limit if activating would exceed the plan's active-connection cap — pause one or upgrade.

POST /v1/connections/:id/pause

Move a connection to paused (stops scheduled syncs).

  • Auth: API key
  • Body: none.
  • 200 → the ConnectionRow (status: "paused").
  • Errors: 404 not_found.

POST /v1/connections/:id/upgrade-spec

Re-pin a connection to a different published spec version. Deliberate, never automatic. Pass dryRun to preview the change without applying it.

  • Auth: API key
  • Body: { "toVersion": "1.3.0", "dryRun": false } (dryRun optional, defaults to false).
  • 200{ "connectionId": "…", "from": "1.2.0", "to": "1.3.0", "applied": true } (applied is false for a dry run).
  • Errors: 404 not_found (connection or target version); 409 conflict if the target version is not published.

GET /v1/connections/:id/schedules

List the connection's schedules.

  • Auth: API key
  • 200ScheduleRow[] (see ScheduleRow).
  • Errors: 404 not_found.

POST /v1/connections/:id/schedules

Create a cron schedule for one resource. See Schedules.

  • Auth: API key

  • Body:

    FieldTypeNotes
    resourceKeystringWhich resource to sync.
    cronstring5- or 6-field cron expression.
    timezonestring (optional)IANA tz; defaults to "UTC".

    New schedules are created enabled: true.

  • 201 → a ScheduleRow.

  • Errors: 404 not_found (connection); 400 validation (malformed cron).


Schedules

Manage an individual schedule by id (org-scoped).

PATCH /v1/schedules/:id

Enable or disable a schedule.

  • Auth: API key
  • Body: { "enabled": true }
  • 200 → the updated ScheduleRow.
  • Errors: 404 not_found.

DELETE /v1/schedules/:id

Delete a schedule.

  • Auth: API key
  • 200{ "deleted": true }.
  • Errors: 404 not_found.

Runs

A run is one execution of a sync. Its statsJson carries recordsIn, recordsOut, skipped, and deleted. See Concepts and Freshness.

GET /v1/runs

List runs, most recent first. Filter by connection and/or status.

  • Auth: API key
  • Query: connectionId (optional), status (optional run status).
  • 200RunRow[] (see RunRow).
curl "https://your-siphon.app/api/v1/runs?connectionId=con_123&status=failed" \
  -H "Authorization: Bearer ck_your_key"

GET /v1/runs/:id

Read one run — its status, timings, watermarks, and stats.

  • Auth: API key
  • 200 → a RunRow.
  • Errors: 404 not_found.

Poll this after a sync until status is succeeded, partial, or failed.

GET /v1/runs/:id/calls

The redacted call log — every outbound HTTP request the run made, with headers redacted and bodies truncated. The audit trail behind a sync.

  • Auth: API key
  • 200CallLogRow[] (see CallLogRow).
  • Errors: 404 not_found.

POST /v1/runs/:id/replay

Re-run the original run's connection + resource as a fresh manual run.

  • Auth: API key
  • Body: none.
  • 202 → the new RunRow.
  • Errors: 404 not_found (original run, connection, or spec); 409 conflict if the connection is no longer active.

Quarantine

Records that failed validation with onInvalid: quarantine land here instead of failing the run. See Validate output.

GET /v1/quarantine

List quarantined records for a connection.

  • Auth: API key
  • Query: connectionIdrequired. Missing → 400 (connectionId query param is required).
  • 200QuarantineRow[] (see QuarantineRow).

POST /v1/quarantine/:id/requeue

Mark a quarantined record resolved so it is retried on the next sync.

  • Auth: API key
  • Body: none.
  • 200 → the QuarantineRow with resolvedAt set.
  • Errors: 404 not_found.

API keys

POST /v1/api-keys

Mint an additional API key for the authenticated org. The raw key is returned once and is never recoverable — store it immediately.

  • Auth: API key
  • Body: none. Workspace binding and scopes are server-decided (no scope escalation, no cross-org binding).
  • 201{ "id": "…", "key": "ck_…" }.

Embed

For customer-facing UI. Mint a short-lived, connection-scoped embed token server-side, then hand it to the browser widget. See Embed the widget.

POST /v1/tokens/embed

Mint an embed token scoped to a single connection.

  • Auth: API key
  • Body: { "connectionId": "con_123", "ttlSeconds": 900 } (ttlSeconds optional; default 900, max 3600).
  • 200{ "token": "…", "expiresAt": 1735689600 } (expiresAt is epoch seconds).
  • Errors: 404 not_found (connection not in this org); 500 if embed tokens are not configured on the deployment.

GET /v1/embed/connection

Connection health for the widget. Authenticated by the embed token, not an API key — the widget can only ever see its own connection.

  • Auth: embed token via the x-siphon-embed-token header.
  • 200
    {
      "connection": { "id": "…", "displayName": "…", "status": "active" },
      "freshness": {
        "health": "healthy",              // healthy | degraded | failing | syncing | pending
        "lastSuccessAt": "2026-01-01T…",
        "ageSeconds": 120,
        "overdue": false,
        "resources": [
          {
            "resourceKey": "contacts",
            "health": "healthy",
            "lastSuccessAt": "…", "lastRecords": 42,
            "lastRunAt": "…", "lastRunStatus": "succeeded",
            "ageSeconds": 120, "expectedIntervalSeconds": 3600, "overdue": false
          }
        ]
      },
      "recentRuns": [
        { "id": "…", "status": "succeeded", "resourceKey": "contacts", "startedAt": "…", "stats": { /* … */ } }
      ]                                   // up to 10
    }
  • Errors: 401 (missing/expired token), 404 not_found.

Webhook deliveries

The delivery log for the org's outbound event webhooks. See Webhooks.

GET /v1/webhook-deliveries

List recent deliveries, newest first.

  • Auth: API key
  • Query: limit — default 50, max 200.
  • 200WebhookDeliveryView[] (see WebhookDeliveryView).

POST /v1/webhook-deliveries/:id/redeliver

Resend the stored, byte-identical payload, re-signed with the org's current secret. Appends a new attempt to the log.

  • Auth: API key
  • Body: none.
  • 202 → the newly recorded WebhookDeliveryView.
  • Errors: 404 not_found (original delivery); 409 conflict if no webhook is configured for the org.

OAuth

For connections whose spec uses oauth2_authorization_code. Drive the consent flow without handling the customer's credentials yourself.

GET /v1/oauth/:connectionId/authorize

Get the provider authorize URL (with a signed, short-lived state) to redirect the customer to.

  • Auth: API key
  • 200{ "authorizeUrl": "https://provider…?state=…" }.
  • Errors: 404 not_found (connection or spec); 409 conflict if the connection's auth is not oauth2_authorization_code; 500 if OAuth is not configured.

GET /v1/oauth/callback

The provider redirects here after consent. The signed state authenticates the request — no API key. Exchanges the code, stores encrypted tokens, and sets the connection active.

  • Auth: none (the signed state is the credential).
  • Query: code (required), state (required). Either missing → 400.
  • 200{ "connected": true, "connectionId": "…" }.
  • Errors: 401 unauthorized (bad/expired state); 409 conflict (exchange returned no token); 500 if OAuth is not configured.

Health

Unauthenticated probes, served under /api (not /api/v1).

GET /api/health

Liveness. No dependencies.

  • 200{ "status": "ok", "service": "siphon-api" }.

GET /api/health/ready

Readiness. Probes the database.

  • 200{ "status": "ready" }.
  • 503{ "status": "not_ready", "detail": "…" }.

Data types

Response bodies reference these shapes. Enumerated values are catalogued in Enums & values.

ConnectionRow

FieldType
idstring
orgIdstring
workspaceIdstring
specIdstring
pinnedSpecVersionstring
displayNamestring
externalCustomerIdstring | null
statuspending | active | paused | error
paramsJsonobject
specOverridesJsonobject | null

SpecVersionRow

FieldType
idstring
specIdstring
versionstring
specJsonobject
statusdraft | published | deprecated
checksumstring
publishedAtstring | null

RunRow

FieldType
idstring
connectionIdstring
resourceKeystring
triggermanual | schedule | webhook | backfill
statusqueued | running | succeeded | failed | partial | canceled
startedAtstring | null
finishedAtstring | null
statsJsonobjectrecordsIn, recordsOut, skipped, deleted
watermarkBeforestring | null
watermarkAfterstring | null

CallLogRow

FieldType
idstring
runIdstring
sequencenumber
methodstring
urlstring
statusCodenumber | null
durationMsnumber | null
attemptnumber
cacheHitboolean
reqHeadersRedactedobject
reqBodyRedactedstring | null
respHeadersobject
respBodyTruncatedstring | null

QuarantineRow

FieldType
idstring
connectionIdstring
resourceKeystring
externalIdstring
rawJsonunknown
validationErrorsunknown
resolvedAtstring | null

ScheduleRow

FieldType
idstring
connectionIdstring
resourceKeystring
cronstring
timezonestring
enabledboolean

WebhookDeliveryView

FieldType
idstring
orgIdstring
connectionIdstring | null
runIdstring | null
eventTypestring
urlstring
statusCodenumber | null
deliveredboolean
createdAtstring

On this page