SCSuper ConnectDocumentationOpen the admin panel →

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.

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.