Skip to main content

Connections

11 operations under /v1/connections (GET POST DELETE PATCH PUT): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

24 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Every operation below requires a Bearer token. See Authentication.

A connection stores a provider credential (encrypted in the vault) plus its merchant config, scoped to your account and project. Connections are org-scoped resources under /v1/connections; sessions read them, they don't own them. The one session-scoped surface is the read-only list of what a given session can currently reach.

Base URL: https://api.codespar.dev

All endpoints require authentication via Bearer token. See Authentication.


POST /v1/connections

Creates a connection (or rotates the credential of an existing one for the same server_id/user_id). The secret is encrypted into the vault; it is never returned by any read endpoint.

Request body

FieldTypeRequiredDescription
server_idstringYesProvider identifier from the servers catalog
secretstring | objectYesThe credential. A string for single-secret providers; an object of named secrets for path-secret providers (each key must match the provider's declared secret names; extra or missing keys are rejected)
display_namestringNoOperator-facing label, max 128 chars
user_idstringNoEnd-user binding for per-user connections
connection_metadataobjectNoProvider-specific merchant config the agent shouldn't have to know (fiscal company id, split wallet id, default customer id). Max 64 keys / 32 KB serialized. Merged into upstream calls at execute time

Response -- 201 Created (or 200 OK on rotation)

{
  "id": "conn_abc123",
  "user_id": null,
  "server_id": "asaas",
  "auth_type": "api_key",
  "status": "connected",
  "display_name": "Asaas production",
  "metadata": {},
  "connection_metadata": { "default_customer_id": "cus_000042" },
  "cert_metadata": {},
  "created_at": "2026-07-01T12:00:00Z",
  "connected_at": "2026-07-01T12:00:00Z",
  "revoked_at": null,
  "expires_at": null
}

metadata is OAuth-derived provider metadata (set by callback flows); connection_metadata is your operator-supplied merchant config; cert_metadata is parsed X.509 info for cert-auth connections.


GET /v1/connections

Lists the project's connections, newest first.

Query parameters

ParameterTypeDescription
limitintPage size
user_idstringFilter by end-user binding
server_idstringFilter by provider
statusstringpending | connected | revoked | expired

Response -- 200 OK

{
  "connections": [ { "id": "conn_abc123", "server_id": "asaas", "status": "connected", "...": "..." } ]
}

GET /v1/connections/:id

Returns one connection (same shape as create). 404 not_found for unknown or cross-tenant ids. The secret is never included.


POST /v1/connections/:id/revoke

Marks the connection revoked AND purges the backing vault rows atomically — after revoke, the credential is gone at both the status gate and the vault lookup. The row itself remains for audit.

Response -- 200 OK

{ "id": "conn_abc123", "status": "revoked" }

Revoking an already-revoked connection returns { "id": "...", "status": "revoked", "already": true }.


PATCH /v1/connections/:id/metadata

Updates connection_metadata (merchant config) without touching the credential. Same 64-key / 32 KB cap as create.

PUT /v1/connections/:id/webhook-secret

Sets or rotates the provider webhook signing secret bound to this connection (used to verify inbound POST /v1/webhooks/:server_id/:connection_id deliveries).

DELETE /v1/connections/:id

Hard-deletes the connection row (vault rows included). Prefer revoke in production — it preserves the audit trail.

Credential validation helpers also live here: POST /v1/connections/hmac-validate and POST /v1/connections/jwt-validate round-trip a candidate credential against the provider before you store it.


GET /v1/sessions/:id/connections

The session-scoped, read-only view: which providers this session can currently reach, with status and tool counts.

Auth required: Yes

curl example

curl https://api.codespar.dev/v1/sessions/ses_abc123/connections \
  -H "Authorization: Bearer csk_live_abc123..."

Response -- 200 OK

{
  "data": [
    {
      "id": "stripe",
      "name": "Stripe",
      "category": "payments",
      "country": "GLOBAL",
      "auth_type": "api_key",
      "connected": true,
      "tools_count": 8,
      "status": "ready"
    },
    {
      "id": "mercado-pago",
      "name": "Mercado Pago",
      "category": "payments",
      "country": "BR",
      "auth_type": "oauth",
      "connected": true,
      "tools_count": 6,
      "status": "ready"
    }
  ],
  "total": 2,
  "session_id": "ses_abc123"
}

End-user OAuth (a customer connecting their own Mercado Pago / Shopify) runs through Connect Links (POST /v1/connect/start plus a hosted callback), not through a session endpoint. See Connect Links.

Error codes

CodeHTTPDescription
invalid_body / invalid_query400Request didn't match the schema
not_found404Connection unknown or cross-tenant
keys_mismatch400Path-secret object keys don't match the provider's declared names

Next steps

Every operation, from the spec

Generated from the published OpenAPI document, so it never drifts from what the API actually serves. The section above is written by hand and carries what a schema cannot: the object model, field rules, and the order to call things in.

GET /v1/connections

GEThttps://api.codespar.dev/v1/connections

List the connections in this project

Active and historical both: a revoked or expired row stays for audit and is returned unless status filters it out. Newest first, capped by limit (default 50, maximum 100). The route does not paginate beyond that cap.

Scope is the calling credential's org AND project. A connection belonging to a sibling project of the same org is absent here and 404s on read, so this list and the point read agree.

Query parameters

NameTypeRequiredDescription
limitintegerno—
server_idstringno—
status"pending" | "connected" | "revoked" | "expired"no—
user_idstringno—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.

Response 200

FieldTypeRequiredDescription
connectionsarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/connections \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/connections HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/connections",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/connections");
Example response 200
application/json
{
  "connections": [
    {
      "id": "obj_0000000000000000",
      "user_id": "user_0000000000000000",
      "server_id": "srv_0000000000000000",
      "auth_type": "string",
      "status": "pending",
      "display_name": "Example",
      "metadata": {},
      "connection_metadata": {},
      "cert_metadata": {},
      "is_shared_sandbox": true,
      "shared_sandbox_tools": [
        "string"
      ],
      "created_at": "2026-01-15T12:00:00.000Z",
      "connected_at": "2026-01-15T12:00:00.000Z",
      "revoked_at": "2026-01-15T12:00:00.000Z",
      "expires_at": "2026-01-15T12:00:00.000Z"
    }
  ]
}

POST /v1/connections

POSThttps://api.codespar.dev/v1/connections

Register a provider key, or rotate the one already there

Encrypts secret and writes the connection in one transaction: either the ciphertext and the row both land, or neither does.

THIS ENDPOINT IS NOT AN INSERT. A status=connected connection for the same (org, project, server) makes the call a ROTATION: the vault entry is overwritten, display_name and connection_metadata are refreshed only when the body carries them, expires_at is cleared, and the EXISTING row comes back with 200. A first connection comes back with 201. user_id is not part of that identity, so a second team member reconnecting the same server rotates the shared credential rather than adding a second one.

secret takes two shapes and the server picks which one is legal from the catalog's auth_type. A plain string is for single-key providers. An object is for the multi-ref kinds (path_secret, cert, hmac_signed, jwt_ecdsa, two_header, cdp) and its keys must match the provider's declared refs EXACTLY: a missing or extra key is 400 path_secret_keys_mismatch with both lists, never a silent drop, because dropping one leaves the provider unusable at call time with no trace of why.

OAuth providers are refused here with 400 not_api_key_server; they go through POST /v1/connections/start. An unknown catalog id is 404 server_unknown. A provider whose catalog row declares no refs is 500 endpoint_missing_refs, which is a seed defect on our side and not a bad request.

Service-auth callers must send x-codespar-user and hold admin or above. Bearer callers are gated by possession of the key alone. The secret is never echoed back by this or any other operation.

Request body

FieldTypeRequiredDescription
connection_metadataobjectno—
display_namestringno—
secretstring | objectyes—
server_idstringyes—
user_idstringno—

Responses

StatusBodyDescription
200objectOK
201objectOK
400objectThe body did not parse, the owner could not be resolved, the server is not key-authenticated, or the secret's shape or keys do not match what the provider declares.
403objectService auth without x-codespar-user, or with a role below admin. Or Coinbase CDP credentials while this deployment holds CDP self-serve connect (CDP_SELF_SERVE_CONNECT off, the default): cdp is refused for every caller, jwt_ecdsa for the dashboard (service auth) only.
404objectNo such server in the catalog.
500objectThe provider's catalog row declares no path-secret refs, so there is nowhere to put the values. A seeding defect, not a bad request.
503objectThe vault or the connection write failed. Nothing was persisted.

Response 200

FieldTypeRequiredDescription
auth_typestringyesMirrors the catalog's auth_type at connect time: api_key, path_secret, cert, hmac_signed, jwt_ecdsa, two_header, oauth, cdp or none. Left open because the column is plain text.
cert_metadataobjectyesIssuer, subject, validity window and SHA-256 fingerprint parsed from the uploaded PEM at connect time. {} for every non-cert connection.
connected_atstring,null (date-time)yes—
connection_metadataobjectyesOperator-supplied merchant config the router merges into upstream calls. {} when nothing was set.
created_atstring (date-time)yes—
display_namestring,nullyes—
expires_atstring,null (date-time)yes—
idstringyesca_-prefixed. Globally unique, visible only inside the owning org and project.
is_shared_sandboxbooleanyesTrue for the platform's shared sandbox connection a test project is given (one operator credential that every test project uses). On such a row metadata is null, cert_metadata is {} and connection_metadata carries only the keys the project fills itself; the operator's own values are never returned. Revoking or deleting it removes it from this project only.
metadataobject,nullyesProvider metadata the OAuth callback wrote (scope, refresh ref) or the provisioning projection wrote (account_id). Always present; null for a key registered through POST /v1/connections, which never sets it.
revoked_atstring,null (date-time)yes—
server_idstringyesCatalog id of the provider this connection authenticates.
shared_sandbox_toolsarray,nullyesOn a shared sandbox row of a provider the shared-sandbox policy restricts (today Asaas and Melhor Envio), the catalog tools it can run; every other operation, and proxy_execute, is refused with shared_sandbox_operation_refused. null on a shared sandbox row of any other provider: it is not restricted by the shared-sandbox policy. [] on every connection that is not a shared sandbox row.
status"pending" | "connected" | "revoked" | "expired"yesexpired includes an OAuth connection whose access token passed expires_at: nothing refreshes it yet, calls through it answer connection_expired, and the fix is to reconnect. ?status= filters on this same value.
user_idstringyes—
Example request
curl -X POST https://api.codespar.dev/v1/connections \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "server_id": "srv_0000000000000000",
       "secret": "string",
       "display_name": "Example",
       "user_id": "user_0000000000000000",
       "connection_metadata": {}
     }'
POST /v1/connections HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "server_id": "srv_0000000000000000",
  "secret": "string",
  "display_name": "Example",
  "user_id": "user_0000000000000000",
  "connection_metadata": {}
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/connections",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "server_id": "srv_0000000000000000",
      "secret": "string",
      "display_name": "Example",
      "user_id": "user_0000000000000000",
      "connection_metadata": {}
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "server_id": "srv_0000000000000000",
    "secret": "string",
    "display_name": "Example",
    "user_id": "user_0000000000000000",
    "connection_metadata": {}
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/connections", {
  body: {
    server_id: "srv_0000000000000000",
    secret: "string",
    display_name: "Example",
    user_id: "user_0000000000000000",
    connection_metadata: {}
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "user_id": "user_0000000000000000",
  "server_id": "srv_0000000000000000",
  "auth_type": "string",
  "status": "pending",
  "display_name": "Example",
  "metadata": {},
  "connection_metadata": {},
  "cert_metadata": {},
  "is_shared_sandbox": true,
  "shared_sandbox_tools": [
    "string"
  ],
  "created_at": "2026-01-15T12:00:00.000Z",
  "connected_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z"
}

GET /v1/connections/engine/{run_id}/status

GEThttps://api.codespar.dev/v1/connections/engine/{run_id}/status

Poll a browser-driven signup run

Read-only. The run resolves only inside the calling credential's org and project, so a run id from another tenant is 404 rather than 403.

Poll this while status is running. When it becomes needs_verification the customer has to clear the provider's email gate, and verification.mechanism says how: code means a one-time code to relay, link means a link to click in their own inbox. Branch on the mechanism, never on the provider name. A paused run that has expired answers 410 run_stale and cannot be resumed.

On provisioned, connection_id names the connection the run produced, which then behaves like any other row on this resource. On failed, code is drawn from a closed first-party vocabulary and failed_step_id names the step, when the run recorded one; a failed run that recorded no reason reports its status alone. The bounded failure detail, the page URL and the screenshot references are deliberately not selected by this query, so they cannot leak from it.

The 404 body here is { error: { code, message } } with no request_id, unlike the 410 beside it.

Path parameters

NameTypeRequiredDescription
run_idstringyes—

Responses

StatusBodyDescription
200objectOK
404objectNo run with that id inside the caller's org and project.
410objectThe paused run expired before it was resumed.

Response 200

FieldTypeRequiredDescription
code"flow_failed" | "capacity" | "no_credential_captured" | "probe_failed" | "drive_deadline_exceeded" | "provider_account_not_registered" | "drive_stalled" | "resume_inputs_unavailable"noWhy the run failed. A stored value outside this vocabulary is reported as absent rather than passed through, so a failed run can render as its status alone.
connection_idstringnoThe ca_ id of the connection the run produced. Present once the run reaches provisioned.
failed_step_idstringno—
probe_result—noResult of the post-provision reachability probe, when one ran.
promptstringnoPresent while the run waits on the customer. Fallback copy when the modal has none of its own.
status"running" | "needs_verification" | "provisioned" | "failed"yes—
verificationobjectnoResolved from the provider's public descriptor. Present only while status is needs_verification.
Example request
curl -X GET https://api.codespar.dev/v1/connections/engine/{run_id}/status \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/connections/engine/{run_id}/status HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/connections/engine/{run_id}/status",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/engine/{run_id}/status", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/connections/engine/{run_id}/status", {
  path: {
    run_id: "run_0000000000000000"
  }
});
Example response 200
application/json
{
  "status": "running",
  "prompt": "string",
  "verification": {
    "mechanism": "code",
    "resend": true
  },
  "connection_id": "conn_0000000000000000",
  "code": "flow_failed",
  "failed_step_id": "failedstep_0000000000000000"
}

POST /v1/connections/start

POSThttps://api.codespar.dev/v1/connections/start

Begin the Connect Link OAuth flow for a provider

The entry point for providers that cannot be connected by posting a key. It mints a single-use state token, stores the caller's org, project and environment against it, and returns the provider's authorize URL with that token already embedded.

Send the user to authorize_url. The provider redirects to our callback, which exchanges the code, vaults the tokens under the project recorded at start, revokes any existing connection for the same (project, user, server) and writes the new one. The callback is not part of this document: it is unauthenticated by necessity, since the provider carries no CodeSpar credential.

link_token is the same value as the state parameter inside authorize_url; it is single use and expires at expires_at, ten minutes out. redirect_uri must be https and is where the callback sends the browser back, with status=connected&connection_id=…, status=denied&error=… or status=error&error=… appended.

POST /v1/connect/start is the same handler on a second path. A provider with no OAuth configuration is 404; a missing platform client credential is 500, which is our seeding defect and not the caller's error. Both bodies are bare { error, … }, not the error.code envelope.

Request body

FieldTypeRequiredDescription
redirect_uristring (uri)yes—
scopesstringno—
server_idstringyes—
user_idstringyes—

Responses

StatusBodyDescription
200objectOK
400objectBad Request — the body or query did not match the schema.
404objectThe provider has no OAuth configuration in the catalog.
422objectThe project is in environment="test" and this provider has no test venue, so the flow is refused BEFORE a state token is minted — a refused start leaves no half-begun link behind. Sending a test project to the provider's LIVE consent screen would have it authorise a real account and vault a real token under a project that is not supposed to hold one (ent#722). Not retryable; the same call from a live project succeeds.
500objectThe platform's OAuth client credential is not seeded for this provider. A configuration defect on our side.

Response 200

FieldTypeRequiredDescription
authorize_urlstring (uri)yesSend the user here. Carries client_id, our callback as redirect_uri, the state token, response_type=code and the resolved scopes.
expires_atstring (date-time)yesTen minutes after the call. A callback arriving later is refused.
link_tokenstringyesSingle-use state token, also embedded in authorize_url.
Example request
curl -X POST https://api.codespar.dev/v1/connections/start \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "server_id": "srv_0000000000000000",
       "user_id": "user_0000000000000000",
       "redirect_uri": "https://example.com/hook",
       "scopes": "string"
     }'
POST /v1/connections/start HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "server_id": "srv_0000000000000000",
  "user_id": "user_0000000000000000",
  "redirect_uri": "https://example.com/hook",
  "scopes": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/connections/start",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "server_id": "srv_0000000000000000",
      "user_id": "user_0000000000000000",
      "redirect_uri": "https://example.com/hook",
      "scopes": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/start", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "server_id": "srv_0000000000000000",
    "user_id": "user_0000000000000000",
    "redirect_uri": "https://example.com/hook",
    "scopes": "string"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/connections/start", {
  body: {
    server_id: "srv_0000000000000000",
    user_id: "user_0000000000000000",
    redirect_uri: "https://example.com/hook",
    scopes: "string"
  }
});
// r.status is one of the documented statuses (200, 422),
// each with its own body shape in r.data; nothing here throws on 422.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "link_token": "string",
  "authorize_url": "https://example.com/hook",
  "expires_at": "2026-01-15T12:00:00.000Z"
}

POST /v1/connections/subaccount/{id}/revoke

POSThttps://api.codespar.dev/v1/connections/subaccount/{id}/revoke

Revoke a provisioned subaccount and try to delete it upstream

The revoke for accounts this platform minted on the customer's behalf. It is not POST /v1/connections/{id}/revoke: the generic purge targets the ref keyed on the server id, which is the wrong ref for a per-account subaccount credential, and it touches neither the provisioning record nor the account at the provider.

Local teardown first, atomically: the connection projection flips, the provisioning record flips, and the correct per-account vault ref is purged. A failure in that phase changes nothing and answers 503, so a retry is safe. Only then is a delete attempted at the provider.

READ upstream BEFORE TREATING THIS AS FINISHED. deleted means the account is gone at the provider. not_applicable means the lane has no delete to make. delete_failed and orphaned mean the credential is destroyed on our side but an account may still exist upstream, and closing it is a manual step. All four come back with 200, because the local teardown did succeed.

{id} is a connection id for the customer path, matched only inside the caller's own org and project. It is a provisioning record id for the operator-internal path, which additionally requires service auth. A customer credential can never reach an operator-internal record: it misses the connection lookup and gets 404, never a 403 that would confirm the id exists. A repeated revoke returns already: true and changes nothing.

Path parameters

NameTypeRequiredDescription
idstringyesA ca_ connection id, or a pr_ provisioning record id on the operator-internal path.

Responses

StatusBodyDescription
200objectOK
403objectThis is a write, so it carries a role floor: a service-auth caller must send x-codespar-user and hold admin or above.
404objectNo connection or operator-internal record with that id is reachable by this caller.
500objectThe revoke threw unexpectedly.
503objectThe internal phase failed and left nothing changed. Retry.

Response 200

FieldTypeRequiredDescription
alreadytruenoPresent when the subaccount was already revoked. Nothing changed.
connection_idstringyesThe connection projection's id, or the provisioning record's id when there is no projection.
record_idstringyes—
status"revoked"yes—
upstream"deleted" | "delete_failed" | "not_applicable" | "orphaned"yesWhat happened at the provider. delete_failed and orphaned mean an account may still exist there; the local credential is gone either way.
Example request
curl -X POST https://api.codespar.dev/v1/connections/subaccount/{id}/revoke \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
POST /v1/connections/subaccount/{id}/revoke HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/connections/subaccount/{id}/revoke",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/subaccount/{id}/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/connections/subaccount/{id}/revoke", {
  path: {
    id: "subaccount_0000000000000000"
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "status": "revoked",
  "record_id": "record_0000000000000000",
  "connection_id": "conn_0000000000000000",
  "upstream": "deleted",
  "already": true
}

GET /v1/connections/subaccount/{id}/status

GEThttps://api.codespar.dev/v1/connections/subaccount/{id}/status

Read whether a provisioned subaccount can transact yet

Read-only, with one live call to the provider. It writes nothing and changes no state.

charges_enabled is what the provider says right now, not a cached flag, which is why a provider that is unreachable answers 502 rather than a stale false. The one exception is a revoked record: it short-circuits to status: "revoked" and charges_enabled: false without calling the provider at all, since there is nothing left to poll. A confirmed account that never reached an upstream account ref reports not-yet-chargeable for the same reason.

capabilities carries the provider's per-capability activation states when it reports them, and is absent when it does not. An account can be provisioned and still not chargeable; gate any money movement on charges_enabled, not on the existence of the connection.

Same dual-id resolution and same cross-tenant 404 as the revoke beside it, minus its admin floor: a read does not need one.

Path parameters

NameTypeRequiredDescription
idstringyesA ca_ connection id, or a pr_ provisioning record id on the operator-internal path.

Responses

StatusBodyDescription
200objectOK
404objectNo connection or operator-internal record with that id is reachable by this caller.
500objectThe status read threw unexpectedly.
502objectThe provider read did not produce a status. Usually the live call failed or timed out, and a retry is the right response. TWO OF THE CASES BEHIND THIS CODE ARE NOT TRANSIENT and retrying never clears them: the provider has no subaccount descriptor in this deployment, and no platform account credential is registered for it. Both are configuration defects on our side; error.message distinguishes them from a provider outage.

Response 200

FieldTypeRequiredDescription
account_refstringyesThe upstream account identifier. Empty string when the record never reached one.
capabilitiesobjectnoPer-capability activation states as the provider reports them, for example card_payments: "active".
charges_enabledbooleanyes—
status"provisioned" | "revoked"yes—
Example request
curl -X GET https://api.codespar.dev/v1/connections/subaccount/{id}/status \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/connections/subaccount/{id}/status HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/connections/subaccount/{id}/status",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/subaccount/{id}/status", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/connections/subaccount/{id}/status", {
  path: {
    id: "subaccount_0000000000000000"
  }
});
Example response 200
application/json
{
  "status": "provisioned",
  "charges_enabled": true,
  "capabilities": {},
  "account_ref": "string"
}

GET /v1/connections/{id}

GEThttps://api.codespar.dev/v1/connections/{id}

Read one connection

404 covers three different situations and deliberately does not distinguish them: no such id, an id owned by another org, and an id owned by a sibling project of the same org. Answering 403 for the last two would confirm the id exists.

Path parameters

NameTypeRequiredDescription
idstringyesca_-prefixed connection id

Responses

StatusBodyDescription
200objectOK
404objectNot Found

Response 200

FieldTypeRequiredDescription
auth_typestringyesMirrors the catalog's auth_type at connect time: api_key, path_secret, cert, hmac_signed, jwt_ecdsa, two_header, oauth, cdp or none. Left open because the column is plain text.
cert_metadataobjectyesIssuer, subject, validity window and SHA-256 fingerprint parsed from the uploaded PEM at connect time. {} for every non-cert connection.
connected_atstring,null (date-time)yes—
connection_metadataobjectyesOperator-supplied merchant config the router merges into upstream calls. {} when nothing was set.
created_atstring (date-time)yes—
display_namestring,nullyes—
expires_atstring,null (date-time)yes—
idstringyesca_-prefixed. Globally unique, visible only inside the owning org and project.
is_shared_sandboxbooleanyesTrue for the platform's shared sandbox connection a test project is given (one operator credential that every test project uses). On such a row metadata is null, cert_metadata is {} and connection_metadata carries only the keys the project fills itself; the operator's own values are never returned. Revoking or deleting it removes it from this project only.
metadataobject,nullyesProvider metadata the OAuth callback wrote (scope, refresh ref) or the provisioning projection wrote (account_id). Always present; null for a key registered through POST /v1/connections, which never sets it.
revoked_atstring,null (date-time)yes—
server_idstringyesCatalog id of the provider this connection authenticates.
shared_sandbox_toolsarray,nullyesOn a shared sandbox row of a provider the shared-sandbox policy restricts (today Asaas and Melhor Envio), the catalog tools it can run; every other operation, and proxy_execute, is refused with shared_sandbox_operation_refused. null on a shared sandbox row of any other provider: it is not restricted by the shared-sandbox policy. [] on every connection that is not a shared sandbox row.
status"pending" | "connected" | "revoked" | "expired"yesexpired includes an OAuth connection whose access token passed expires_at: nothing refreshes it yet, calls through it answer connection_expired, and the fix is to reconnect. ?status= filters on this same value.
user_idstringyes—
Example request
curl -X GET https://api.codespar.dev/v1/connections/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/connections/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/connections/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/connections/{id}", {
  path: {
    id: "conn_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "user_id": "user_0000000000000000",
  "server_id": "srv_0000000000000000",
  "auth_type": "string",
  "status": "pending",
  "display_name": "Example",
  "metadata": {},
  "connection_metadata": {},
  "cert_metadata": {},
  "is_shared_sandbox": true,
  "shared_sandbox_tools": [
    "string"
  ],
  "created_at": "2026-01-15T12:00:00.000Z",
  "connected_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z"
}

DELETE /v1/connections/{id}

DELETEhttps://api.codespar.dev/v1/connections/{id}

Delete a revoked connection for good

Hard delete, allowed only once status is revoked or expired. An active connection is refused with 409 rather than deleted, because dropping the row while the credential still works would lose the audit trail and leave the vault entry with nothing pointing at it.

The purge runs inside the same transaction, before the row goes. That matters for an expired connection, which reaches here without ever having been revoked and so has never been purged; for an already-revoked one the purge is a no-op.

The 409 body is a bare { error, message } pair, not the error.code envelope the newer routes use.

Path parameters

NameTypeRequiredDescription
idstringyesca_-prefixed connection id

Responses

StatusBodyDescription
204—No Content
404objectNot Found
409objectThe connection is still active. Revoke it first.
Example request
curl -X DELETE https://api.codespar.dev/v1/connections/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
DELETE /v1/connections/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.delete(
    "https://api.codespar.dev/v1/connections/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/{id}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.delete("/v1/connections/{id}", {
  path: {
    id: "conn_0000000000000000"
  }
});

PATCH /v1/connections/{id}/metadata

PATCHhttps://api.codespar.dev/v1/connections/{id}/metadata

Merge user-editable metadata into a connection

Merges fields into a connection's connection_metadata. This is a merge, not a replacement: the keys sent land on top and the rest stay.

What can be edited is decided by the row itself, by the requires_user_fields list it carries. A key outside that list is refused with fields_not_user_editable, and a connection that carries no list at all refuses EVERY patch with no_user_fields. Its path is to reconnect, not to edit.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
connection_metadataobjectyesAt least one key, and at most 32 KB serialized. The body is MERGED with what is already there, key by key. It does not replace the whole object.

Responses

StatusBodyDescription
200objectOK
400objectThe body is outside the schema (empty, a key that is too long, or above 32 KB), the connection declares no editable fields, or one of the keys is not in its list.
404objectNot Found
409objectThe row disappeared between the read and the write: a race with a revoke or a delete. Read it again before retrying.

Response 200

FieldTypeRequiredDescription
auth_typestringyesMirrors the catalog's auth_type at connect time: api_key, path_secret, cert, hmac_signed, jwt_ecdsa, two_header, oauth, cdp or none. Left open because the column is plain text.
cert_metadataobjectyesIssuer, subject, validity window and SHA-256 fingerprint parsed from the uploaded PEM at connect time. {} for every non-cert connection.
connected_atstring,null (date-time)yes—
connection_metadataobjectyesOperator-supplied merchant config the router merges into upstream calls. {} when nothing was set.
created_atstring (date-time)yes—
display_namestring,nullyes—
expires_atstring,null (date-time)yes—
idstringyesca_-prefixed. Globally unique, visible only inside the owning org and project.
is_shared_sandboxbooleanyesTrue for the platform's shared sandbox connection a test project is given (one operator credential that every test project uses). On such a row metadata is null, cert_metadata is {} and connection_metadata carries only the keys the project fills itself; the operator's own values are never returned. Revoking or deleting it removes it from this project only.
metadataobject,nullyesProvider metadata the OAuth callback wrote (scope, refresh ref) or the provisioning projection wrote (account_id). Always present; null for a key registered through POST /v1/connections, which never sets it.
revoked_atstring,null (date-time)yes—
server_idstringyesCatalog id of the provider this connection authenticates.
shared_sandbox_toolsarray,nullyesOn a shared sandbox row of a provider the shared-sandbox policy restricts (today Asaas and Melhor Envio), the catalog tools it can run; every other operation, and proxy_execute, is refused with shared_sandbox_operation_refused. null on a shared sandbox row of any other provider: it is not restricted by the shared-sandbox policy. [] on every connection that is not a shared sandbox row.
status"pending" | "connected" | "revoked" | "expired"yesexpired includes an OAuth connection whose access token passed expires_at: nothing refreshes it yet, calls through it answer connection_expired, and the fix is to reconnect. ?status= filters on this same value.
user_idstringyes—
Example request
curl -X PATCH https://api.codespar.dev/v1/connections/{id}/metadata \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "connection_metadata": {}
     }'
PATCH /v1/connections/{id}/metadata HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "connection_metadata": {}
}
import os
import requests

res = requests.patch(
    "https://api.codespar.dev/v1/connections/{id}/metadata",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "connection_metadata": {}
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/{id}/metadata", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "connection_metadata": {}
  }),
});

const data = await res.json();
const result = await cs.api.patch("/v1/connections/{id}/metadata", {
  path: {
    id: "conn_0000000000000000"
  },
  body: {
    connection_metadata: {}
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "user_id": "user_0000000000000000",
  "server_id": "srv_0000000000000000",
  "auth_type": "string",
  "status": "pending",
  "display_name": "Example",
  "metadata": {},
  "connection_metadata": {},
  "cert_metadata": {},
  "is_shared_sandbox": true,
  "shared_sandbox_tools": [
    "string"
  ],
  "created_at": "2026-01-15T12:00:00.000Z",
  "connected_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z"
}

POST /v1/connections/{id}/revoke

POSThttps://api.codespar.dev/v1/connections/{id}/revoke

Revoke a connection and destroy its stored credential

Flips the row to revoked AND deletes the vault rows behind it, in one transaction. The row survives because audit needs connected_at after the credential is gone; the ciphertext does not. A revoke that could not purge fails with 503 and leaves the connection connected, which is the honest outcome: reporting revoked while the secret stays decryptable is the failure this route exists to prevent.

A vault entry is dropped only when no other still-connected row in the same org and project would resolve it. Sibling providers can share one bundle, so a shared key survives until the last connection using it is revoked.

TWO DIFFERENT 200 BODIES. A revoke that did work returns the full connection row. A connection already revoked returns the short { id, status, already } form and touches nothing, so a retry is safe. Branch on the presence of already, not on the status code.

Path parameters

NameTypeRequiredDescription
idstringyesca_-prefixed connection id

Responses

StatusBodyDescription
200object | objectOK
404objectNot Found
503objectThe atomic flip and purge failed. Nothing changed; the connection is still usable. Retry.
Example request
curl -X POST https://api.codespar.dev/v1/connections/{id}/revoke \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
POST /v1/connections/{id}/revoke HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/connections/{id}/revoke",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/{id}/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.post("/v1/connections/{id}/revoke", {
  path: {
    id: "conn_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "user_id": "user_0000000000000000",
  "server_id": "srv_0000000000000000",
  "auth_type": "string",
  "status": "pending",
  "display_name": "Example",
  "metadata": {},
  "connection_metadata": {},
  "cert_metadata": {},
  "is_shared_sandbox": true,
  "shared_sandbox_tools": [
    "string"
  ],
  "created_at": "2026-01-15T12:00:00.000Z",
  "connected_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z"
}

PUT /v1/connections/{id}/webhook-secret

PUThttps://api.codespar.dev/v1/connections/{id}/webhook-secret

Seed or rotate the secret that verifies this provider's inbound webhooks

The value a provider signs its callbacks with (a Stripe signing secret, an Asaas access token, a Zoop Basic blob). The vault ref is derived from the provider internally, so a caller never has to know the naming convention.

Write-only: the plaintext is accepted once and no operation returns it. updated is the only thing the response says about the previous state, and it says exactly one thing: true when a secret was already stored under that ref, false when this call seeded the first one.

A provider with no inbound webhook adapter is refused with 400 provider_has_no_inbound_webhooks, since a secret nothing will ever verify is worse than no secret at all. This write is not transactional with anything else: a 503 means the vault write failed and the old secret, if any, is still in place.

Path parameters

NameTypeRequiredDescription
idstringyesca_-prefixed connection id

Request body

FieldTypeRequiredDescription
secretstringyes—

Responses

StatusBodyDescription
200objectOK
400objectThe body did not parse, or the provider has no inbound webhook adapter in this API.
404objectNot Found
503objectThe vault write failed. Any previously stored secret is untouched.

Response 200

FieldTypeRequiredDescription
connection_idstringyes—
server_idstringyes—
updatedbooleanyesTrue when a secret already existed under this ref and was replaced; false when this call seeded the first one.
Example request
curl -X PUT https://api.codespar.dev/v1/connections/{id}/webhook-secret \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "secret": "string"
     }'
PUT /v1/connections/{id}/webhook-secret HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "secret": "string"
}
import os
import requests

res = requests.put(
    "https://api.codespar.dev/v1/connections/{id}/webhook-secret",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "secret": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/connections/{id}/webhook-secret", {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "secret": "string"
  }),
});

const data = await res.json();
const result = await cs.api.put("/v1/connections/{id}/webhook-secret", {
  path: {
    id: "conn_0000000000000000"
  },
  body: {
    secret: "string"
  }
});
Example response 200
application/json
{
  "connection_id": "conn_0000000000000000",
  "server_id": "srv_0000000000000000",
  "updated": true
}
Connections | CodeSpar