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/v1The 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" }
]
}
}| HTTP | error.code | Meaning |
|---|---|---|
400 | validation | Malformed body, bad spec, or params that fail their rules. Carries issues. |
400 | (none) | Pre-service error — invalid JSON, or a missing required query param. |
401 | unauthorized | Missing, malformed, revoked, or expired credential. |
402 | plan_limit | The action would exceed the org's plan cap (e.g. active connections). |
404 | not_found | Resource absent or owned by another org. |
409 | conflict | State conflict — e.g. syncing a non-active connection, publishing twice. |
409 | immutable | Attempt to mutate a published (frozen) resource. |
500 | internal | Unexpected 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
| Code | Used for |
|---|---|
200 | Reads and state toggles (activate, pause, publish, requeue). |
201 | Resource creation (spec draft, connection, schedule, API key). |
202 | Accepted 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), plusconnectionParams,resources,auth, etc.orgIdis taken from the key and ignored if present in the body. 201→ a SpecVersionRow withstatus: "draft".- Errors:
400 validation(withissues) if the spec is invalid;409 conflictif thatversionalready 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.jsonPOST /v1/specs/:key/validate
Validate a candidate spec without saving. Pure function — no persistence.
- Auth: API key
- Body: a candidate ConnectorSpec.
200→ aValidationResult: 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 withstatus: "published"andpublishedAtset.- Errors:
404 not_foundif the spec/version is unknown;409 conflictif 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.
- Auth: API key
200→ConnectionRow[](see ConnectionRow).
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:
Field Type Notes 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. orgIdis 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 conflictif the connection is notactive, 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_limitif 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 }(dryRunoptional, defaults tofalse). 200→{ "connectionId": "…", "from": "1.2.0", "to": "1.3.0", "applied": true }(appliedisfalsefor a dry run).- Errors:
404 not_found(connection or target version);409 conflictif the target version is not published.
GET /v1/connections/:id/schedules
List the connection's schedules.
- Auth: API key
200→ScheduleRow[](see ScheduleRow).- Errors:
404 not_found.
POST /v1/connections/:id/schedules
Create a cron schedule for one resource. See Schedules.
-
Auth: API key
-
Body:
Field Type Notes 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). 200→RunRow[](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
200→CallLogRow[](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 conflictif 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:
connectionId— required. Missing →400(connectionId query param is required). 200→QuarantineRow[](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 withresolvedAtset.- 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 }(ttlSecondsoptional; default900, max3600). 200→{ "token": "…", "expiresAt": 1735689600 }(expiresAtis epoch seconds).- Errors:
404 not_found(connection not in this org);500if 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-tokenheader. 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— default50, max200. 200→WebhookDeliveryView[](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 conflictif 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 conflictif the connection's auth is notoauth2_authorization_code;500if 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
stateis 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);500if 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
| Field | Type |
|---|---|
id | string |
orgId | string |
workspaceId | string |
specId | string |
pinnedSpecVersion | string |
displayName | string |
externalCustomerId | string | null |
status | pending | active | paused | error |
paramsJson | object |
specOverridesJson | object | null |
SpecVersionRow
| Field | Type |
|---|---|
id | string |
specId | string |
version | string |
specJson | object |
status | draft | published | deprecated |
checksum | string |
publishedAt | string | null |
RunRow
| Field | Type |
|---|---|
id | string |
connectionId | string |
resourceKey | string |
trigger | manual | schedule | webhook | backfill |
status | queued | running | succeeded | failed | partial | canceled |
startedAt | string | null |
finishedAt | string | null |
statsJson | object — recordsIn, recordsOut, skipped, deleted |
watermarkBefore | string | null |
watermarkAfter | string | null |
CallLogRow
| Field | Type |
|---|---|
id | string |
runId | string |
sequence | number |
method | string |
url | string |
statusCode | number | null |
durationMs | number | null |
attempt | number |
cacheHit | boolean |
reqHeadersRedacted | object |
reqBodyRedacted | string | null |
respHeaders | object |
respBodyTruncated | string | null |
QuarantineRow
| Field | Type |
|---|---|
id | string |
connectionId | string |
resourceKey | string |
externalId | string |
rawJson | unknown |
validationErrors | unknown |
resolvedAt | string | null |
ScheduleRow
| Field | Type |
|---|---|
id | string |
connectionId | string |
resourceKey | string |
cron | string |
timezone | string |
enabled | boolean |
WebhookDeliveryView
| Field | Type |
|---|---|
id | string |
orgId | string |
connectionId | string | null |
runId | string | null |
eventType | string |
url | string |
statusCode | number | null |
delivered | boolean |
createdAt | string |