Reference
Reference
Section titled “Reference”The real provider catalog, HTTP API, environment, and SDK surface for Connect.
Providers
Section titled “Providers”Connect registers more than fifty providers across ten categories. GET /v1/catalog is the authoritative, always-current list — the table below groups them so you can see the shape.
| Category | Providers | Auth |
|---|---|---|
dev_tool |
github gitlab bitbucket slack linear jira asana clickup notion sentry vercel cloudflare |
OAuth |
trello |
API key | |
social |
youtube instagram facebook threads tiktok linkedin pinterest |
OAuth |
advertising |
meta_ads google_ads |
OAuth |
communication |
gmail discord microsoft_teams zoom |
OAuth |
whatsapp |
Embedded signup | |
calcom |
API key | |
crm |
salesforce pipedrive intercom |
OAuth |
storage |
google_drive google_sheets dropbox |
OAuth |
stock |
shutterstock |
OAuth |
pexels pixabay unsplash freepik |
API key | |
ai |
openai anthropic cohere replicate huggingface stability elevenlabs |
API key |
commerce |
stripe paypal |
OAuth |
wompi skydropx |
API key | |
cloud |
aws |
Cross-account role |
Multi-account providers — where one login can expose several downstream accounts — include youtube, instagram, facebook, gmail, meta_ads, google_ads, google_drive, and google_sheets. The catalog reports this per provider as multi_account_capable.
Every API-key provider is always connectable: there is no application to register on Destesi’s side, so you bring your own key per workspace. That covers all seven AI providers, the four API-key stock providers, Cal.com, Wompi, Skydropx, and Trello.
HTTP API
Section titled “HTTP API”Connect’s base URL is https://connect.destesi.io. Workspaces are addressed by slug in the path. All responses are JSON.
Health
Section titled “Health”| Method & path | Purpose |
|---|---|
GET /healthz |
Liveness — returns {"status":"ok"}. |
GET /readyz |
Readiness — 200 when dependencies are reachable, 503 degraded otherwise. |
Catalog (public)
Section titled “Catalog (public)”| Method & path | Purpose |
|---|---|
GET /v1/catalog |
List every known provider with id, display_name, category, auth_kind, multi_account, scopes, and configured. |
curl -s https://connect.destesi.io/v1/catalog | python3 -m json.toolConnections (signed-in user)
Section titled “Connections (signed-in user)”These routes require a Connect session cookie (established by single sign-on). {slug} is the workspace slug.
| Method & path | Purpose |
|---|---|
GET /v1/workspaces/{slug}/connections/{provider}/connect/start |
Begin an OAuth flow. Returns { "authorize_url": "..." }; redirect the browser there. |
GET /v1/connections/callback |
OAuth redirect target (the provider sends the user here with code and state). |
GET /v1/connections/picker/{state_id} |
Fetch a multi-account picker — returns provider and accounts (account metadata only, never tokens). |
POST /v1/workspaces/{slug}/connections/{provider}/accounts |
Confirm picked accounts. Body: { "state_id": "...", "account_ids": ["..."] }. Creates one connection per account. |
POST /v1/workspaces/{slug}/connections/{provider} |
API-key flow. Body: { "api_key": "..." }. Validates against the provider before saving; a rejected key returns 422 invalid_api_key. |
GET /v1/workspaces/{slug}/connections |
List the workspace’s connections (tokens scrubbed). Optional ?category= (any catalog category — dev_tool, social, advertising, communication, crm, storage, stock, ai, commerce, cloud) and ?provider= filters. |
PATCH /v1/workspaces/{slug}/connections/{id} |
Set a connection as default. Body: { "is_default": true }. |
POST /v1/workspaces/{slug}/connections/{id}/validate |
Re-check a connection against the provider; always 200 with the updated status in the body. |
DELETE /v1/workspaces/{slug}/connections/{id} |
Disconnect (delete) a connection. |
GET /v1/workspaces/{slug}/stock/search |
Federated stock-photo search across connected stock providers. Query: q, optional provider (unsplash, pexels, pixabay, auto), optional per_page (default 5, max 30). |
A connection in a list response carries account metadata and health only — for example id, provider, category, auth_kind, account_username, account_display_name, status, is_default, and timestamps. The secret is never serialized to the browser.
Actions
Section titled “Actions”The action routes accept either a personal access token or a Connect session cookie. The workspace is always required, as an X-Destesi-Workspace: <slug> header or a ?ws=<slug> query parameter; without one you get 400 missing_workspace. Membership in that workspace is verified on every call, so a token can only ever act on a workspace you belong to.
| Method & path | Purpose |
|---|---|
GET /v1/actions/catalog |
Public, no auth. The catalog’s shape — id, provider, name, title, description, side_effect, scopes, input_schema. No connection state. Built for boot-time tool registration. |
GET /v1/actions |
The same catalog plus a connected flag per action for your workspace. |
POST /v1/actions/{provider}.{action}:execute |
Run one action. Body: {"args": {...}, "idempotency_key": "..."}. |
curl -X POST https://api.connect.destesi.io/v1/actions/slack.post_message:execute \ -H "Authorization: Bearer idn_pat_xxxxxxxx" \ -H "X-Destesi-Workspace: acme" \ -H "Content-Type: application/json" \ -d '{"args":{"channel":"C0123","text":"hello"}}'Action error codes. These are a closed set — you can switch on them:
| Code | HTTP | Meaning |
|---|---|---|
unknown_action |
404 | No such {provider}.{action} |
invalid_args |
422 | Arguments failed the action’s schema, or included one it doesn’t declare |
not_connected |
409 | The workspace has no connection for that provider |
insufficient_scope |
403 | The connection lacks a scope the action requires |
provider_error |
502 | Connect could not complete the call to the provider |
rate_limited |
429 | The provider rate-limited the call |
timeout |
504 | The provider did not respond in time |
A provider returning 4xx or 5xx is not one of these. That comes back as a successful response carrying a non-ok result with the provider’s own status, so you can tell “we couldn’t call them” apart from “they said no”. Response bodies are capped, and a truncated body is flagged as such rather than silently cut.
Triggers
Section titled “Triggers”Same authentication as actions.
| Method & path | Purpose |
|---|---|
POST /v1/triggers/ingest/{token} |
Public. Where the provider posts. Verified against the subscription’s inbound secret. |
POST /v1/triggers/subscriptions |
Create. Returns id, ingest_url, ingest_token, inbound_secret, signing_secret — once. |
GET /v1/triggers/subscriptions |
List, redacted, with each subscription’s last delivery. |
PATCH /v1/triggers/subscriptions/{id} |
{"status":"active"} or {"status":"paused"}. |
DELETE /v1/triggers/subscriptions/{id} |
Delete. |
GET /v1/triggers/subscriptions/{id}/secrets |
Reveal the secrets once; afterwards 410 already_revealed. |
GET /v1/triggers/subscriptions/{id}/deliveries |
Page through deliveries (limit, before, before_id; 30 per page, max 100). |
Creating a subscription rejects a non-GitHub provider with 422 unsupported_provider, and an endpoint the SSRF guard refuses with 422 unsafe_target.
The outbound delivery your endpoint receives:
{ "delivery_id": "...", "subscription_id": "...", "provider": "github", "event_type": "pull_request", "provider_delivery_id": "...", "occurred_at": "2026-08-04T12:00:00Z", "schema_version": 1, "normalized_payload": {}}with these headers:
| Header | Value |
|---|---|
X-Destesi-Delivery |
The delivery id — dedupe on this |
X-Destesi-Timestamp |
Unix seconds, covered by the signature |
X-Destesi-Signature |
sha256=<hex HMAC-SHA256 of "<timestamp>.<body>" using your signing secret> |
Verify by recomputing the HMAC over timestamp + "." + rawBody. Reject a timestamp far from now — that is what makes a captured delivery unusable later.
A 2xx marks the delivery done. Anything else retries with growing backoff, up to six attempts, before the delivery is marked failed.
Controller routes (service-to-service)
Section titled “Controller routes (service-to-service)”These routes let a sibling product backend fetch a workspace’s live credential. They are gated by a shared controller secret sent as Authorization: Bearer <secret>. If the configured secret is empty (the routes are effectively disabled) or your bearer doesn’t match it, the connections, credential, and mark-error routes return 401. A 404 from the credential route means the workspace simply has no connection for that provider.
| Method & path | Purpose |
|---|---|
GET /v1/controller/workspaces/{slug}/connections |
List the workspace’s connections (no secrets). |
GET /v1/controller/workspaces/{slug}/connections/{provider}/credential |
Fetch the live credential. Refreshes a near-expiry OAuth token on demand; on refresh failure it flips the connection to EXPIRED and returns 401 (token_expired) so the caller prompts a reconnect. |
GET /v1/controller/workspaces/{slug}/stock/search |
Federated stock search, bearer-gated variant of the user route. |
POST /v1/controller/workspaces/{slug}/connections/{id}/mark-error |
Flag a connection as errored after the provider rejected its credential. Body: { "error": "..." }. |
The credential response uses an auth-kind-aware envelope, so the same shape covers OAuth bearers and raw API keys:
{ "credential": "<oauth bearer or api key>", "auth_kind": "oauth", "scopes": ["..."], "account_id": "...", "provider": "slack", "category": "dev_tool", "status": "ACTIVE", "metadata": {}, "expires_at": "2026-06-01T00:00:00Z"}Status values are uppercase: ACTIVE, EXPIRED, REVOKED, ERROR. expires_at is present only for OAuth credentials with a known expiry.
Two thin SDKs wrap the controller credential route so a sibling backend can fetch a workspace credential and then call the provider itself. Both cover the same providers with one representative method each — beyond the happy path, construct your own HTTP client with the returned credential. Both surface non-2xx responses as a structured error so you can discriminate “not connected” (404) from “token expired” (401).
Package: @destesi/connect.
import { ConnectClient, SlackClient, ConnectAPIError } from "@destesi/connect";
const client = new ConnectClient({ baseURL: "https://connect.destesi.io", controllerSecret: process.env.IDENTITY_CONTROLLER_SECRET!,});
try { const cred = await client.fetchCredential("acme", "slack"); console.log(cred.credential, cred.auth_kind, cred.status);} catch (err) { if (err instanceof ConnectAPIError && err.status === 404) { // not connected }}
// Typed per-provider client:const sl = await SlackClient.from(client, "acme");await sl.postMessage("C0123", "hello from @destesi/connect");Typed clients: SlackClient, LinearClient, GitHubClient, YouTubeClient, TikTokClient, InstagramClient, PexelsClient, UnsplashClient, ElevenLabsClient. Each exposes a static .from(client, workspaceSlug) factory; the multi-account social clients (YouTubeClient, TikTokClient, InstagramClient) take an optional trailing account id, .from(client, workspaceSlug, accountId?).
Module: github.com/destesi/connect-go.
import connect "github.com/destesi/connect-go"
c := connect.New("https://connect.destesi.io", os.Getenv("IDENTITY_CONTROLLER_SECRET"))
// Universal:cred, err := c.FetchCredential(ctx, "acme", "slack")
// Typed:sl, err := c.Slack(ctx, "acme")_, err = sl.PostMessage(ctx, "C0123", "hello from connect-go")Non-2xx responses come back as *connect.APIError — switch on its HTTP status to handle 404 vs 401.
Environment
Section titled “Environment”Self-hosting connect-api? These are the variables it reads. Copy api/.env.example and fill in the OAuth credentials for the providers you want to enable.
| Variable | Purpose | Default |
|---|---|---|
CONNECT_API_ADDR |
Listen address. | :9105 |
CONNECT_DATABASE_URL |
Postgres DSN (uses the destesi-api instance with search_path=connect). |
— |
IDENTITY_API_URL |
Identity service base URL for session validation and slug→id resolution. | http://localhost:9091 |
IDENTITY_CONTROLLER_SECRET |
Bearer for the /v1/controller/* routes. Unset disables those routes. |
— |
CONNECT_OAUTH_REDIRECT_BASE_URL |
Base URL providers redirect back to. The callback path is /v1/connections/callback?provider=<id>. |
http://localhost:9105 |
CONNECT_WEB_URL |
Default web origin to redirect to after a flow completes. | http://localhost:4331 |
CONNECT_WEB_URL_ALLOWLIST |
Comma-separated exact origins allowed as a post-redeem / return-to redirect target. | — |
CONNECT_CORS_ALLOWED_ORIGINS |
Comma-separated origins permitted as the Origin header on /v1/* requests. |
— |
Provider OAuth credentials
Section titled “Provider OAuth credentials”Each OAuth connector registers only when its credentials are present. The usual pattern is CONNECT_TPA_<PROVIDER>_CLIENT_ID / CONNECT_TPA_<PROVIDER>_CLIENT_SECRET, but several providers need something extra or differently named — a pair alone is not always enough:
| Provider | Also needs |
|---|---|
tiktok |
CONNECT_TPA_TIKTOK_CLIENT_KEY instead of _CLIENT_ID |
vercel |
CONNECT_TPA_VERCEL_SLUG |
google_ads |
CONNECT_TPA_GOOGLE_ADS_DEVELOPER_TOKEN |
meta_ads |
CONNECT_TPA_META_ADS_CONFIG_ID |
whatsapp |
CONNECT_TPA_WHATSAPP_APP_ID, _APP_SECRET, _CONFIG_ID |
aws |
CONNECT_AWS_TRUST_PRINCIPAL_ARN (unset leaves AWS in a stub mode that cannot assume a role) |
# Dev toolsCONNECT_TPA_SLACK_CLIENT_IDCONNECT_TPA_SLACK_CLIENT_SECRETCONNECT_TPA_LINEAR_CLIENT_IDCONNECT_TPA_LINEAR_CLIENT_SECRETCONNECT_TPA_GITHUB_CLIENT_IDCONNECT_TPA_GITHUB_CLIENT_SECRET
# Social — note: TikTok uses CLIENT_KEY, not CLIENT_IDCONNECT_TPA_YOUTUBE_CLIENT_IDCONNECT_TPA_YOUTUBE_CLIENT_SECRETCONNECT_TPA_TIKTOK_CLIENT_KEYCONNECT_TPA_TIKTOK_CLIENT_SECRETCONNECT_TPA_META_CLIENT_ID # Facebook and Threads use the shared Meta app pairCONNECT_TPA_META_CLIENT_SECRETCONNECT_TPA_INSTAGRAM_CLIENT_ID # Instagram has its OWN pair (Instagram Login),CONNECT_TPA_INSTAGRAM_CLIENT_SECRET # not the Meta one
# Gmail — Google OAuth. Falls back to CONNECT_TPA_YOUTUBE_* when unset,# since they can share one Google project + consent screen.CONNECT_TPA_GMAIL_CLIENT_IDCONNECT_TPA_GMAIL_CLIENT_SECRETThe API-key providers use no CONNECT_TPA_* pair at all — the seven AI providers, pexels, pixabay, unsplash, freepik, calcom, wompi, and skydropx each register unconditionally, and a workspace supplies its own key through the Add API key modal (the process-wide stock-search fallback keys below are an optional convenience, not a registration requirement).
The full CONNECT_TPA_* roster lives in api/.env.example. A provider with no credentials configured stays dormant and shows as an unconfigured tile in the catalog — it is logged at startup so you can see at a glance which slots are still empty.