HTTP API reference
The base URL is https://super-connect.dogar.biz/api/v1; every route below
is relative to it except /health, /api/auth/*, /oauth/*, /proxy/* and
/webhook/*, which stay at the site root. The unprefixed API paths were removed on
2026-09-12 with SDK 0.2.0.
All bodies are JSON. Responses set Cache-Control: no-store. Errors are
{ "error": "message" } unless a section says otherwise.
Authentication
| Mode | How | Who uses it |
|---|---|---|
| Organization key | Authorization: Bearer sc_… |
Your backend |
| Member session | Better Auth cookie with an active organization | The admin panel |
| Connect session | Authorization: Bearer <token> or ?token= |
The hosted connect page |
| Operator session | Better Auth cookie, user.role = 'admin' |
Platform operators |
| None | /health, OAuth browser routes |
Organization key and member session are interchangeable on every management route. The key or the session's active organization decides which organization the request reads; there is no organization segment in any URL.
Errors: 401 for a missing or invalid credential (with code from the API
key plugin when available), 403 for a session without an active
organization or membership.
Billing refusals
402 { "error": "<message>", "code": "<code>" } when the organization's plan
does not cover the request. A 402 is terminal until the organization
subscribes; do not retry it.
| Code | Where |
|---|---|
organization_unpaid |
Any organization-key request while the subscription is unpaid |
free_tier_connection_limit |
POST /api/v1/connect/sessions, POST /api/v1/connect/:integrationId, POST /api/v1/connect/session/authorize, OAuth callback |
free_tier_provider_limit |
PUT /api/v1/providers/:id when it creates a provider |
The OAuth callback has no JSON body; it renders the message on the callback page.
X-User-Id
Names the end user your backend believes owns a connection, by the id you
gave POST /api/v1/users. Required on every
connection-scoped route when an organization key authenticates the request;
optional with a member session. An id that names no user answers
404 { "error": "User not found", "code": "user_not_found" }; a user who
does not own the connection answers a plain 404. The header is never
forwarded to the provider.
Public
GET /health
200 { "prototype": true, "status": "ok" }
/api/auth/*
Better Auth. Email and password sign-in and sign-up, plus the organization,
admin and apiKey plugin routes. Sign-up succeeds only for the first user
or for an email with a pending, unexpired organization invitation; otherwise
it answers 403. The tests seed organizations through
/api/auth/sign-up/email, /api/auth/organization/create and
/api/auth/api-key/create.
POST /webhook/:receiveId
The receive URL of one integration. Slack and GitHub post their webhooks here. The request is verified with the integration's inbound secret, matched to connections, answered, and then forwarded and logged. No Super Connect credential is involved; the provider's signature is the credential. See Receive provider events.
| Status | When |
|---|---|
| 200 | Verified and accepted. Empty, except Slack's url_verification, which is answered with the challenge as text/plain |
| 400 | GitHub request without an X-GitHub-Event header |
| 401 | The integration has no inbound secret, or the signature is missing, wrong, or carries a Slack timestamp more than five minutes old |
| 404 | Unknown receiveId, or the provider's template has no webhook adapter |
| 405 | Any method other than POST |
| 413 | The body is over 1 MiB |
Every request is logged, rejections included, and readable through
GET /api/v1/integrations/:id/webhooks. Recipients are chosen and forwards
written after the response, so a slow customer endpoint never delays the
provider's ack.
GET /oauth/start?state=…
Binds the browser with a cookie and redirects to the provider. A state may only be started once per browser.
GET /oauth/callback?state=…&code=…
Consumes the state, exchanges the code, stores or replaces the connection,
completes the connect session if any, and renders an HTML page that posts the
outcome to window.opener. Failures render the same page with 400.
Connect session routes
Authorized by the session token.
GET /api/v1/connect/session
{
"userId": "user_123",
"displayName": "Ada",
"expiresAt": 1757622600000,
"mode": "connect",
"connectionId": null,
"integrations": [
{
"id": "github",
"providerId": "github",
"displayName": "GitHub (User OAuth)",
"category": "Dev Tools",
"scopes": "repo read:user",
"authMode": "oauth2"
},
{
"id": "retool",
"providerId": "retool",
"displayName": "Retool",
"category": "Dev Tools",
"scopes": "",
"authMode": "api_key",
"apiKey": { "title": "Access token", "description": "…", "pattern": "^retool_" }
}
]
}mode is reauthorize when the session was minted for a connection.
integrations is filtered by the session's allowlist. authMode says how
the integration authorizes; apiKey describes the field to show when it is
api_key (pattern is optional). 401 for an invalid, expired or already
completed session.
POST /api/v1/connect/session/authorize
Body { "integrationId": "github" } for an OAuth integration:
201 { "kind": "handoff", "connectionId", "url" }.
Body { "integrationId": "retool", "apiKey": "…" } for an API key
integration: the key is validated against the provider's pattern, stored, and
the session completes. 201 { "kind": "connected", "connectionId" }. 400
with the field description when the key fails the pattern, or when apiKey
is sent to an OAuth integration.
403 when the integration is outside the allowlist. 402 when a new
connection is over the plan, see Billing refusals.
Operator routes
GET /api/v1/operator/organizations
[{ id, name, slug, createdAt, ownerEmail, memberCount }], newest first.
403 for non-operators.
Providers
GET /api/v1/providers
The providers your organization has saved, sorted by ID. Built-in presets
appear only once saved, or once an integration references them.
Each entry is { id, ...provider fields }; see
Settings → Provider.
tokenResponseMetadata is a list of dot paths copied out of the token
response into the connection's metadata at exchange, refresh and
reauthorization. The Slack preset declares ["team.id", "authed_user.id"],
which is how Slack webhooks find their recipients. The webhookForwarding field was removed on
2026-09-13; it is stripped from stored provider configurations by migration
0011, and sending it now fails validation. Inbound webhooks are configured
on the integration instead.
PUT /api/v1/providers/:id
Create or replace. 201 with the stored provider. 400 on validation
errors. 402 when creating a provider on the free tier over its limit, see
Billing refusals.
DELETE /api/v1/providers/:id
200 { "deleted": true }. 409 while an integration references it. 404
when there is no stored row, including presets that were never saved.
GET /api/v1/providers/:id/actions
Action summaries for the provider's template:
[
{
"id": "get-repository",
"name": "Get Repository",
"description": "…",
"version": "1.0.0",
"scopes": ["repo"],
"tags": [],
"input": {/* JSON Schema */},
"output": {/* JSON Schema */}
}
]404 when the provider is unknown or has no template.
Integrations
GET /api/v1/integrations
[{ id, providerId, clientId, hasSecret, scopes, webhookUrl, hasWebhookSecret, receiveId, hasInboundSecret, webhooks }].
Never returns the client secret, the webhook secret or the inbound secret.
webhooks is { instructions, secretLabel } from the webhook adapter of the
provider's template, or null when the template has no adapter and the
receive URL answers 404. instructions is the setup text the admin panel
shows; secretLabel names the secret the provider calls it, such as Slack's
"Signing Secret".
receiveId is the path segment of the integration's receive URL,
https://super-connect.dogar.biz/webhook/<receiveId>. It is minted when the
integration is created and never changes. hasInboundSecret says whether an
inbound secret is stored.
PUT /api/v1/integrations/:id
Create or update. See
Settings → Integration.
201 { id, providerId, scopes, webhookUrl, hasWebhookSecret, receiveId, hasInboundSecret }.
inboundSecret is the provider's own signing secret, used to verify inbound
webhooks. Omit it to keep the stored value, send null to clear it. It is
encrypted at rest and never returned.
DELETE /api/v1/integrations/:id
200 { "deleted": true }. 409 while a connection references it. 404 when
absent.
GET /api/v1/integrations/:id/webhooks
The requests this integration's receive URL has taken, newest first. Bodies are omitted here; read one with the detail route.
Query parameters: outcome (forwarded, unmatched, filter_failed,
replied, rejected or too_large), since (epoch milliseconds), limit (default 50, max 200),
cursor.
{
"data": [
{
"sequence": 87,
"id": "inb_5b3c…",
"provider_id": "slack",
"received_at": 1757620800000,
"verified": 1,
"outcome": "forwarded",
"event_type": "message",
"provider_event_id": "Ev09ABCDEF",
"retry_num": null,
"matched": 1,
"status": 200,
"error": null,
"body_size": 812
}
],
"nextCursor": "87"
}Page until nextCursor is null. 400 for an unknown outcome. 404 when
the integration is unknown or belongs to another organization.
GET /api/v1/integrations/:id/webhooks/:inboundId
One logged request with its captured headers, its raw body and the forward events it produced.
{
"sequence": 87,
"id": "inb_5b3c…",
"provider_id": "slack",
"received_at": 1757620800000,
"verified": 1,
"outcome": "forwarded",
"event_type": "message",
"provider_event_id": "Ev09ABCDEF",
"retry_num": null,
"matched": 1,
"status": 200,
"error": null,
"body_size": 812,
"headers": { "content-type": "application/json", "x-slack-signature": "v0=…" },
"body": "{\"type\":\"event_callback\",…}",
"bodyExpired": false,
"forwards": [
{
"id": "evt_9f21…",
"connectionId": "conn_4b0e…",
"attempts": 1,
"nextAttemptAt": 1757620800000,
"deliveredAt": 1757620801000,
"failedAt": null,
"lastError": null
}
]
}Bodies are nulled seven days after the request arrives; bodyExpired is
true once that has happened. A too_large row never stored a body and
reports bodyExpired: false. unmatched and filter_failed rows have an
empty forwards list. 404 when the integration or the inbound ID is
unknown.
Users
End users are the people in your product who own connections. Create each
one before minting a connect session or a connection for them; an unknown
id is 404 { "code": "user_not_found" } everywhere, never an upsert. Ids are
yours: 1 to 200 characters, unique within your organization, case-sensitive.
Every route here takes an organization key or a member session.
POST /api/v1/users
Body { "id": "user_123", "name": "Ada", "metadata": { "plan": "pro" } }.
name and metadata are optional; metadata is an object of string values
serializing to at most 4096 characters.
201 { "id": "user_123", "name": "Ada", "metadata": { "plan": "pro" }, "createdAt": 1757620800000 }.
409 { "code": "user_exists" } when the id is taken.
GET /api/v1/users
Query parameters: limit (default 100, max 200), cursor,
include=connections. Newest first,
{ "data": [...], "nextCursor": "…" | null, "total": n } where total is the
organization's user count independent of paging. 400 for a malformed cursor.
With include=connections each user carries connectionCount, the number
of connections they own; without it the field is absent.
GET /api/v1/users/:id
One user, the same shape as the create response. include=connections adds
connectionCount as on the list. 404 user_not_found.
PATCH /api/v1/users/:id
Body { "name"?, "metadata"? }. Each given field replaces the stored value
whole; name: null clears it. Returns the updated user.
DELETE /api/v1/users/:id
Disconnects every connection the user owns through the same path as
DELETE /api/v1/connections/:id (provider hooks included), revokes their
open connect sessions, then removes the user. 200 { "deleted": true }.
Invocation rows keep the user's id as plain text, so the audit log
outlives the user. A 502 from a provider's disconnect hook aborts the
delete; retry it.
Connect
POST /api/v1/connect/sessions
Mints a browser-safe session. See Settings → Connect session.
201 { "sessionId", "token", "connectLink", "expiresAt" }. Only the SHA-256
hash of the token is stored. 404 user_not_found when userId names no
user, 404 when connectionId is unknown. 402 when a new connection is
over the plan, see Billing refusals.
POST /api/v1/connect/:integrationId
Low-level handoff. Body { "userId": "user_123" }.
201 { "kind": "handoff", "connectionId", "url" }. The URL is valid for ten
minutes and one browser. 404 user_not_found when userId names no user.
402 when a new connection is over the plan, see
Billing refusals.
For an API key integration, body { "userId": "user_123", "apiKey": "…" }
creates the connection directly: 201 { "kind": "connected", "connectionId" }.
Send { "connectionId": "…", "apiKey": "…" } instead to replace the key of an
existing connection in place; the owner comes from the connection. 404 when
connectionId is unknown, 409 when it belongs to another integration or
user, 400 when the key fails the provider's pattern or the integration uses
OAuth.
API key connections have no expiry and are never refreshed. A revoked key surfaces as the provider's own error on the proxy or action call.
Connections
GET /api/v1/connections
Query parameters: integrationId, userId, health, limit (default 100,
max 200), cursor.
{
"data": [
{
"id": "…",
"integration_id": "github",
"user_id": "user_123",
"expires_at": null,
"scopes": "repo,read:user",
"created_at": 0,
"updated_at": 0,
"refresh_until": 0,
"next_refresh_at": 0,
"last_refresh_success": 0,
"last_refresh_failure": null,
"refresh_attempts": 0,
"refresh_exhausted": 0,
"last_refresh_error_type": null,
"last_refresh_error": null,
"health": "healthy"
}
],
"nextCursor": "1757620800000.uuid"
}Newest first. health is filtered after the page is read, so a filtered page
may hold fewer than limit rows while nextCursor is still set. Page until
nextCursor is null. 400 for an unknown health or a malformed cursor.
GET /api/v1/connections/:id
One connection with health. Honours X-User-Id. metadata is the parsed
routing object, or null when the connection has none.
PATCH /api/v1/connections/:id
Merges routing metadata into the connection. Body
{ "metadata": { "repository.612345678": "1", "organization.9876543": null } }.
Returns the updated connection, the same shape as the GET. Honours
X-User-Id.
- Values are strings or
null. Anulldeletes the key; every other key is left alone. - Keys match
[A-Za-z0-9_.-]{1,100}; a value is at most 1024 characters, and the merged object must serialize to at most 4096 characters. - Keys the provider captures from the token response, such as Slack's
team.id, are refused with400. Super Connect owns those. 404on an unknown connection or anX-User-Idmismatch.
Metadata is how inbound GitHub webhooks find a connection. See Receive provider events.
DELETE /api/v1/connections/:id
Removes the stored tokens. Does not revoke at the provider.
200 { "deleted": true }; 404 on mismatch.
POST /api/v1/connections/:id/reconnect
400 for an API key connection; replace its key through
POST /api/v1/connect/:integrationId or a reconnect link instead.
201 { "connectionId", "url" }, a 30-minute one-use reauthorization handoff
for the existing owner and integration. Does not take X-User-Id.
GET /api/v1/connections/:id/invocations
Last 20 invocations, newest first:
[
{
"sequence": 12,
"kind": "action",
"connection_id": "…",
"integration_id": "github",
"user_id": "user_123",
"action_id": "get-repository",
"method": null,
"path": null,
"status": null,
"error_code": null,
"error": null,
"duration_ms": 412,
"subrequests": 1,
"created_at": 1757620800000
}
]kind: "proxy" rows carry method, path and the upstream status.
GET /api/v1/invocations/summary
{ "since": <ms>, "integrations": [{ "integration_id", "total", "ok" }] } for
the last 24 hours. Rows with action_input_invalid are excluded because they
are caller errors.
Proxy
ANY /proxy/:connectionId/*
Forwards to apiBase + path + query with the user's bearer token. Method,
query and streamed bodies are preserved. Forwarded request headers: Accept,
Content-Type, If-Match, If-None-Match, Range. Upstream Set-Cookie is
dropped.
| Status | Meaning |
|---|---|
| any | Upstream response passed through, including 4xx and 5xx |
| 400 | Path escapes apiBase (Invalid provider path) |
| 404 | Unknown connection or X-User-Id mismatch |
| 502 | Upstream redirect blocked |
| 401 / 409 / 502 | Connection error; see below |
Actions
POST /api/v1/connections/:id/actions/:actionId
Body is the action input. Success:
{ "ok": true, "value": { … }, "subrequests": 1 }Failure is { "error": { "code", "message", "subrequests", …details } }:
| Status | code |
Details |
|---|---|---|
| 400 | action_input_invalid |
issues: Zod issues |
| 422 | action_error |
type, extra: thrown by the action |
| 502 | provider_error |
status, body (2 KB), Retry-After header |
| 502 | action_output_invalid |
issues |
| 504 | action_timeout |
|
| 500 | action_subrequests_exceeded |
|
| 500 | action_failed |
name, message |
| 404 | plain error | No template, or unknown action |
An action's declared scopes are informational. Nothing is refused before the
call; a connection that lacks a permission gets the provider's own answer as
provider_error.
Connection errors
Shared by proxy and actions.
| Status | error.code |
Extra fields |
|---|---|---|
| 401 | connection_refresh_exhausted |
action: "reauthorize", connectionId, integrationId |
| 409 | connection_refresh_backoff |
Retry-After: 1 header |
| 502 | connection_refresh_failed |
message from the provider |
| 404 | plain error | Connection vanished mid-request |
Cron
Every ten minutes the Worker refreshes every connection whose
next_refresh_at has passed, drains the webhook outbox, and clears the stored
bodies of inbound webhooks older than seven days, 500 rows a tick. The refresh
and outbox sweeps page across organizations and stop after a ten-minute
budget.