Skip to content

Ask AI

Ask anything about Destesi — setup, products, APIs.

Powered by Claude. Answers may be wrong — always verify against the docs.

Reference

The real provider catalog, HTTP API, environment, and SDK surface for Connect.

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.

Connect’s base URL is https://connect.destesi.io. Workspaces are addressed by slug in the path. All responses are JSON.

Method & path Purpose
GET /healthz Liveness — returns {"status":"ok"}.
GET /readyz Readiness — 200 when dependencies are reachable, 503 degraded otherwise.
Method & path Purpose
GET /v1/catalog List every known provider with id, display_name, category, auth_kind, multi_account, scopes, and configured.
Terminal window
curl -s https://connect.destesi.io/v1/catalog | python3 -m json.tool

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.

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": "..."}.
Terminal window
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.

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_secretonce.
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.

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?).

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.

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)
Terminal window
# Dev tools
CONNECT_TPA_SLACK_CLIENT_ID
CONNECT_TPA_SLACK_CLIENT_SECRET
CONNECT_TPA_LINEAR_CLIENT_ID
CONNECT_TPA_LINEAR_CLIENT_SECRET
CONNECT_TPA_GITHUB_CLIENT_ID
CONNECT_TPA_GITHUB_CLIENT_SECRET
# Social — note: TikTok uses CLIENT_KEY, not CLIENT_ID
CONNECT_TPA_YOUTUBE_CLIENT_ID
CONNECT_TPA_YOUTUBE_CLIENT_SECRET
CONNECT_TPA_TIKTOK_CLIENT_KEY
CONNECT_TPA_TIKTOK_CLIENT_SECRET
CONNECT_TPA_META_CLIENT_ID # Facebook and Threads use the shared Meta app pair
CONNECT_TPA_META_CLIENT_SECRET
CONNECT_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_ID
CONNECT_TPA_GMAIL_CLIENT_SECRET

The 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.