---
title: Authentication
description: API key management, service authentication, key rotation, and security best practices for the CodeSpar API.
---

import { Callout } from "fumadocs-ui/components/callout";

# Authentication

CodeSpar uses a two-layer authentication model. Your application authenticates to CodeSpar with an **API key** (bearer token), and CodeSpar handles authentication to individual MCP servers (payment providers, shipping carriers, fiscal services) on your behalf using **service auth** credentials you configure once in the dashboard.

This separation means your AI agent never touches raw provider credentials. It only needs the CodeSpar API key.

## API keys

API keys authenticate your application to the CodeSpar API. Every request to `api.codespar.dev` must include a valid key.

### Key formats

CodeSpar issues two types of keys, distinguished by prefix:

| Prefix | Environment | Behavior |
|--------|-------------|----------|
| `csk_live_` | Production | Connects to real provider APIs. Processes real transactions, issues real invoices, sends real messages. |
| `csk_test_` | Sandbox | Connects to mock servers. Returns realistic simulated data. No real money moves, no real documents are issued. |

<Callout type="warn">
The key prefix **must match** the environment of the project it operates on. A `csk_live_` key against a `test` project (or vice-versa) returns `401 unauthorized`. See [Projects → Environments](/docs/concepts/projects#environments-live-vs-test) for why this is enforced and how to debug the mismatch.
</Callout>

### Using API keys with the SDK

```typescript
import { CodeSpar } from "@codespar/sdk";

// Production: real transactions
const codespar = new CodeSpar({
  apiKey: "csk_live_abc123def456ghi789...",
});

// Sandbox: mock data, safe for development
const codespar = new CodeSpar({
  apiKey: "csk_test_abc123def456ghi789...",
});
```

### Using API keys with curl

Pass the key as a Bearer token in the `Authorization` header:

```bash
curl -X POST https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer csk_live_abc123def456ghi789..." \
  -H "Content-Type: application/json" \
  -d '{"servers": ["stripe", "mercadopago"]}'
```

### Creating keys in the dashboard

1. Navigate to [Dashboard → API Keys](https://codespar.dev/dashboard/api-keys)
2. Click **Create Key**
3. Name the key (e.g., "Production Backend", "Staging", "CI/CD Pipeline")
4. The environment defaults to match the active project's `environment` field. To mint a key for a different environment than the active project (cross-project work), flip the explicit toggle on the modal.
5. Optionally restrict the key to specific [scopes](#key-scopes) (choose **Restricted** in the dashboard)
6. Click **Create** and copy the key immediately: it is only shown once

```bash
# Verify your key works
curl https://api.codespar.dev/v1/servers \
  -H "Authorization: Bearer csk_live_your_new_key..."
```

<Callout type="warn">
**Never expose API keys in client-side code.** Keys must stay on the server. Use `process.env.CODESPAR_API_KEY` or your framework's secret management (Vercel Environment Variables, AWS Secrets Manager, etc.).
</Callout>

### Key scopes

By default an API key has full access to your account. You can instead **restrict a key to a subset of operations** when you create it: in the dashboard, choose **Restricted** and check the scopes you want.

| Scope | What it allows |
|-------|---------------|
| `sessions:create` | Create new sessions |
| `sessions:read` | List and retrieve session details |
| `tools:execute` | Post a tool call to `POST /v1/sessions/:id/tool-calls`, or to the MCP POST transport (`POST /v1/sessions/:id/mcp`, `POST /mcp`) |
| `servers:read` | Browse the server catalog |
| `projects:settings` | Read and update a project's settings (`GET` and `PATCH /v1/projects/:id/settings`, `GET /v1/projects/:id/settings/history`) |
| `proxy:execute` | Raw-HTTP proxy to a connected server's upstream with the vault credential injected (`POST /v1/sessions/:id/proxy_execute`). Deliberately separate from `tools:execute`: executing a published tool and dialling an arbitrary endpoint with the account's live credential are different grants |

The backend enforces those six. The dashboard's **Restricted** checkboxes offer
**five** of them: `proxy:execute` is enforced but not grantable anywhere in the
product today, and the create-key endpoint is service-auth, so there is no API
escape hatch either. A key that needs `proxy_execute` therefore has to be a
wildcard key.

**Example:** A key used only by a webhook handler that processes tool results carries only `tools:execute` and `sessions:read`.

A request whose key lacks the required scope returns `403 forbidden`:

```json
{
  "error": "forbidden",
  "message": "API key does not have the 'sessions:create' scope.",
  "status": 403
}
```

<Callout title="Restricted keys guard a mapped subset, not the API" type="warn">
Be concrete about what Restricted buys you today. The scope map holds **14
route entries against roughly 198 routes a scoped key can reach — about 7%**.
On a *mapped* route the check is unconditional: a narrow key without the
required scope gets `403 forbidden`, and that includes `proxy_execute`. On an
*unmapped* route the gap is **recorded, not refused** — the request is served
to any authenticated key, whatever scopes it holds. Refusing unmapped routes is
behind `ROUTE_SCOPE_DENY_UNMAPPED`, which is off by default and off on
CodeSpar's hosted production, because turning it on today would break MCP:
`GET /mcp` and `DELETE /mcp` carry no mapping, so the stream a client opens
right after `initialize` would 403.

The practical consequence: a key with every published scope is still not a
usable MCP credential, and a narrow key is a partial control, not a boundary.
Every key created before scopes shipped, and every service-auth caller, holds
the `*` wildcard and bypasses the check entirely. Treat scopes as
defence-in-depth on the operations in the table, and keep using separate keys
per environment and per consumer for real isolation.

Billing, api-keys, usage, team and project members are operator-only and are
not reachable with an API key at all, regardless of scope — they answer `401`.
</Callout>

## Key rotation

You can create multiple API keys and rotate them without downtime. This is critical for production systems where you cannot afford an outage during credential updates.

### Rotation procedure

1. **Create a new key** in the dashboard with the same settings as the existing key
2. **Deploy the new key** to your application (update environment variables, redeploy)
3. **Verify** that the new key is working by checking API responses and session creation
4. **Revoke the old key** in the dashboard once all instances are using the new key

```bash
# Step 1: Create new key via the dashboard

# Step 2: Test the new key
curl https://api.codespar.dev/v1/servers \
  -H "Authorization: Bearer csk_live_new_key..."
# Should return 200 OK

# Step 3: Revoke the old key via the dashboard
```

<Callout type="warn">
Revoking a key **immediately terminates all active sessions** created with that key. Any in-flight tool calls will fail. Always ensure all traffic has migrated to the new key before revoking.
</Callout>

### Rotation schedule

For production systems, rotate API keys at least every 90 days. Set a calendar reminder or automate it with your secrets management system.

## Service authentication

**Service authentication** (also called **service auth**) is how CodeSpar connects to third-party commerce APIs on your behalf. You store your provider credentials once in the dashboard, and CodeSpar uses them automatically when a [session](/docs/concepts/sessions) connects to that server.

### How it works

1. You store provider credentials (e.g., Stripe secret key, Mercado Pago access token) in the CodeSpar dashboard under **Auth Configs**
2. When a session connects to that server, CodeSpar authenticates using those stored credentials
3. Your agent code never sees the provider credentials; they stay encrypted at rest on CodeSpar's infrastructure

```typescript
// Your agent code does not handle provider auth
const session = await codespar.create("user_123", {
  servers: ["stripe"],
  // CodeSpar uses your stored Stripe credentials automatically
});

// This "just works": auth is handled by CodeSpar
const result = await session.execute("codespar_charge", {
  method: "pix",
  amount: 9990,
  currency: "BRL",
  description: "Pro Plan",
});
```

### Configuring provider credentials

Navigate to [codespar.dev/dashboard/auth-configs](https://codespar.dev/dashboard/auth-configs) and add credentials for each server you plan to use:

| Server | Auth type | What you provide |
|--------|-----------|-----------------|
| Stripe | API key | Secret key (`sk_live_...` or `sk_test_...`) |
| Asaas | API key | API key from the Asaas dashboard |
| Fiscal (NF-e/NFS-e) | API key | API token from the fiscal provider (no `Bearer` prefix on this one) |
| Melhor Envio | OAuth | Authorize once via dashboard; refresh handled automatically |
| Z-API | path_secret | Instance ID + token + Client-Token (see below) |
| Mercado Pago | OAuth token | Access token from the Mercado Pago dashboard |
| Twilio | API key + secret | Account SID + Auth Token |
| NF-e / SEFAZ | Certificate | A1 digital certificate (.pfx) + password |

CodeSpar normalizes provider auth into **eight `auth_type` values** so the dashboard wizard knows what fields to render and the proxy executor knows how to inject creds at request time:

| Auth type | Example providers | What it looks like on the wire |
|-----------|-------------------|-------------------------------|
| `api_key` | Asaas, Stripe, SendGrid | Single secret in a header (`Authorization`, `access_token`, etc) |
| `path_secret` | Z-API, Take Blip, Evolution API | One or more secrets embedded in the URL path, plus an optional companion HTTP header |
| `oauth` | Mercado Pago, Asaas (some configs), Melhor Envio | OAuth 2.0 authorization-code flow with managed callback; sandbox/prod hosts split per environment |
| `cert` | Banco do Brasil, Itaú, Bradesco, Santander, Caixa, Sicoob, Sicredi, C6, Original (9 BR banks, catalog rows wired) | mTLS with X.509 client certificate (PEM blob); operator uploads cert + key + CA via the Provider Connect modal. Needs an ICP-Brasil certificate issued to your CNPJ, so it cannot be connected with a test key |
| `hmac_signed` | Foxbit (and future LATAM crypto exchanges with the same pattern) | Per-request signature over `timestamp + method + path + body`, derived from a shared key + secret pair |
| `jwt_ecdsa` | Coinbase Developer Platform (Trading / Wallets / Payments) | Per-request ES256 JWT signed with an ECDSA P-256 private key; attached as `Authorization: Bearer <jwt>` |
| `two_header` | Cielo, Transbank, Kushki, Payway | Two co-equal credential headers (e.g. `MerchantId` + `MerchantKey`), no `Authorization` Bearer |
| `none` | Brasil API, public endpoints | No credentials; public/free APIs |

#### `api_key` in detail

The most common shape across the catalog. The dashboard prompts for one secret, the vault stores it under a single ref, and the proxy injects it into the request header at execution time. Encodings vary per provider (some use `Authorization: Bearer`, some use a custom `access_token` header, some use HTTP Basic over a single token); the catalog row records which.

#### `path_secret` in detail

Some Brazilian providers (Z-API is the canonical one) embed credentials directly in the request path: `https://api.z-api.io/instances/{instance_id}/token/{instance_token}/send-text`. There's also a **companion header** (`Client-Token`) that has to travel alongside, separate from the path-embedded secrets.

When you connect a `path_secret` provider, the dashboard prompts for each ref by name:

```
Z-API instance_id:    [3F20CB2B72E01124220962A6D661BA0A]
Z-API instance_token: [B132A8A5764B1007900F2C49]
Client-Token:         [F72499aef81b94f25bb41ec5d6016ae80S]
```

CodeSpar stores each secret in the vault under its own ref (`z-api.instance_id`, `z-api.instance_token`, `z-api.client_token`) and reassembles the path + header at proxy time. Your code never sees these values: you call `session.execute("z-api/send_text", { phone, message })` and the runtime handles the encoding.

#### `oauth` in detail

OAuth 2.0 authorization-code flow. CodeSpar hosts the redirect URL, exchanges the code for tokens, and stores access + refresh tokens in the vault. Your agent kicks the flow off via `session.authorize(serverId)`; once consent is recorded the session can transparently call any tool on that provider on the user's behalf. Refresh is automatic.

#### `cert`, `hmac_signed`, `jwt_ecdsa` in detail

These three schemes carry cryptographic key material rather than a pasted token, and each runs a validation step before anything is persisted. They get a dedicated subsection under [Provider auth schemes](#provider-auth-schemes) below.

#### `two_header` in detail

A handful of LATAM acquirers authenticate with **two co-equal static headers** instead of a single Bearer token, for example Cielo's `MerchantId` + `MerchantKey`, or Transbank's `Tbk-Api-Key-Id` + `Tbk-Api-Key-Secret`. The operator pastes both halves into the Provider Connect modal; CodeSpar stores each under its own vault ref and the proxy stamps both headers on every outbound request. Used today by Cielo, Transbank, Kushki, and Payway.

#### `none` in detail

Some catalog entries are public APIs that need no credentials; Brasil API is the canonical example. The connect modal still records a connection row so usage can be metered, but no secret is requested.

<Callout type="info">
Credentials are encrypted with AES-256 at rest and are never returned in API responses. When you view auth configs in the dashboard, only the last 4 characters of each credential are shown.
</Callout>

## Provider auth schemes

Three `auth_type` values involve cryptographic key material instead of a static token: `cert`, `hmac_signed`, and `jwt_ecdsa`. They share a pattern: the operator supplies key material in the Provider Connect modal, the dashboard validates it before persisting, and the proxy executor produces the wire credential at request time. Per-provider specifics (header names, signing recipes, claim shapes) live on the catalog row, so onboarding a new provider with the same shape is a catalog change, not a runtime change.

### mTLS / X.509 (`cert`)

Mutual TLS authenticates both sides of the HTTPS handshake: the server presents its certificate as usual, and the client presents one signed by a CA the server trusts. There is no bearer token on top; the handshake itself is the credential. Nine Brazilian banks have catalog rows on this runtime (Banco do Brasil, Itaú, Bradesco, Santander, Caixa, Sicoob, Sicredi, C6, Original). BCB's open-banking spec mandates mTLS for corporate (B2B) endpoints, and the cert is issued to your CNPJ during onboarding with each bank — which is why these rails cannot be exercised from the test environment and are not connected for you.

The operator uploads three files in the Provider Connect modal:

| Field | Format | What it does |
|-------|--------|--------------|
| `cert` | PEM client certificate (`.pem`, `.crt`) | Identifies your application to the bank. Must match the CN/CNPJ registered during onboarding. |
| `key` | PEM private key (`.key`, `.pem`), RSA or ECDSA | Pair to the client cert. Never leaves the vault, never logged, never echoed back through the API. |
| `ca` | PEM CA bundle (optional) | The bank's CA chain. Only needed when the issuer is not in the system trust store; most BR institutions issue chains rooted in ICP-Brasil. |

The catalog row marks each of these secrets with `kind: "cert"`, which tells the dashboard to render a file uploader instead of a text input and tells the vault to store a multi-line PEM blob.

**Validation.** On upload, the backend parses the certificate with `node:crypto`'s `X509Certificate` and stores the subject CN, issuer, `validFrom` / `validTo`, and SHA-256 fingerprint as `cert_metadata` on the connection. The dashboard reads that metadata to surface expiry warnings: amber at 30 days, red at 7 days, critical when expired. The cert + key pair stays bonded; re-uploading one without the other invalidates the connection.

**Runtime.** The proxy executor caches an `undici` `Agent` per (org, server, environment fingerprint) tuple. The first outbound call parses the PEM, builds the TLS context, and opens the keepalive pool; every later call reuses it, so the handshake is paid once, not per request. Rotation is a full re-upload: the fingerprint changes, which invalidates the cached agent automatically. Deleting the connection wipes the vault entry and drops the agent, and in-flight calls fail closed.

**Companion header (Bradesco style).** Some banks layer a developer key on top of mTLS: the cert proves the corporate entity, the key identifies the calling application. The catalog row declares it with `path_secret_header_ref` plus `auth_header_name` (for Bradesco, `X-Bradesco-Developer-Key`); the dashboard renders one extra text input below the file uploaders and the proxy stamps the header once the TLS connection is up. Debugging rule of thumb: `TLS handshake failed` means a cert/key/CA mismatch, while a `401` after a successful handshake means this companion key is missing or wrong.

### HMAC per-request (`hmac_signed`)

Every outbound request carries a freshly computed HMAC signature alongside the public key, so the credential is a per-call proof rather than a reusable token. The signature covers timestamp, method, path, and body, which means a captured request cannot be replayed against a different endpoint or outside the timestamp window. Foxbit is the canonical provider; other exchanges with the same shape onboard through catalog rows.

The operator stores two values:

| Field | Visibility | What it is |
|-------|-----------|-----------|
| `access_key` | Visible (last 4 in the dashboard) | Public identifier of the key pair, sent in cleartext on every request. Foxbit calls this KEY. |
| `secret` | Masked entirely | Private signing secret. Never sent on the wire; used only to compute the HMAC. Foxbit calls this SECRET. |

**Validation round-trip.** Before persisting, the operator clicks **Validate** and the dashboard fires `POST /v1/connections/hmac-validate`, which signs a noop request with the pasted secret and sends it to the provider's echo or health endpoint. A 2xx returns `{ ok: true }`; a failure surfaces the provider's exact error (`401 signature mismatch`, `401 key invalid`) inline, so a bad pair never reaches the vault.

**Runtime.** On every call the proxy builds the signing string from the catalog row's recipe (for Foxbit: `timestamp + method + path + body`, timestamp in unix milliseconds), computes `hmac_sha256(secret, signing_string)`, and stamps the provider's headers (Foxbit: `X-FB-API-KEY`, `X-FB-API-TIMESTAMP`, `X-FB-API-SIGNATURE`). There is no token to refresh. Replay protection comes from the timestamp tolerance (typically around 5 seconds), which also means clock drift on the proxy host shows up as intermittent `401 signature mismatch`; check NTP before suspecting the key. Header names, concatenation order, timestamp unit, and algorithm all live in the catalog row's `hmac` block, so adding a new exchange is a catalog drop, not a backend change.

### JWT-ES256 (`jwt_ecdsa`)

Every outbound request carries a freshly minted, short-lived JWT signed with an ECDSA P-256 private key. This is asymmetric: the provider holds only the public half, so there is no shared secret to leak. Signing happens at the application layer, not at the TLS handshake (compare `cert`); the wire credential is `Authorization: Bearer <jwt>` on a normal HTTPS call. Coinbase Developer Platform (CDP) is the canonical provider: a single CDP key authenticates against `coinbase-cdp-trading`, `coinbase-cdp-wallets`, and `coinbase-cdp-payments`, so you connect once and the runtime reuses the same vault entry across all three catalog rows.

The operator stores two values:

| Field | Visibility | What it is |
|-------|-----------|-----------|
| `key_name` | Visible | The provider's identifier for the key. For CDP: `organizations/<org>/apiKeys/<id>`. Sent in cleartext as the JWT's `sub` and `kid`. |
| `private_key_pem` | Masked entirely | PEM-encoded ECDSA P-256 private key. Never leaves the vault; used only to sign the per-request JWT. |

**Validation round-trip.** **Validate key** fires `POST /v1/connections/jwt-validate`, which imports the PEM into a Node `crypto.KeyObject` and mints a sample JWT against the provider's expected claim shape. It returns `{ ok: true }` on a clean import, or the exact import error (`invalid_pem`, `unsupported_curve`) on failure. CDP issues PKCS8-encoded P-256 keys; P-384, secp256k1, and RSA keys will not import. Convert older OpenSSL-format keys with `openssl pkcs8 -topk8 -nocrypt`.

**Runtime.** The imported `KeyObject` is cached per (org, server, environment fingerprint) tuple, mirroring the `cert` agent cache, so the PEM parse is paid once. For each request the proxy mints a JWT with claims `sub` and `kid` from `key_name`, `iss: "cdp"`, `aud: ["cdp_service"]`, `nbf: now`, `exp: now + 120`, and a `uri` claim of `<METHOD> <host><path>` that binds the token to the exact request. The JWT header carries `alg: "ES256"`, `typ: "JWT"`, and a random 16-byte nonce. The token is valid for 120 seconds, so clock drift beyond that window produces clustered `401`s that look like a key problem but are almost always time. A `401` on one specific tool usually means the `uri` claim missed: check the catalog row's `base_url` and the tool path. Per-provider claim shapes (issuer, audience, TTL) live in the catalog row's `jwt` block.

## Connect Links: end-user authentication

Service auth covers credentials **you** own (the platform's Stripe key, your Twilio account). For credentials **your end users** own (a merchant's Mercado Pago account, a customer's Shopify store), CodeSpar issues **Connect Links**: hosted OAuth pages that handle consent, token exchange, and vault storage without you building a UI.

```typescript
const { redirectUrl } = await session.authorize("stripe");
if (redirectUrl) window.location.href = redirectUrl;
```

Two integration patterns (in-chat and manual onboarding), the full `session.authorize()` signature, backend endpoints, branding customization, and troubleshooting live in the dedicated [Connect Links](/docs/concepts/connect-links) page.

## Security best practices

1. **Use test keys during development.** `csk_test_` keys connect to mock servers and never process real transactions. There is no reason to use live keys outside of production.
2. **Scope keys narrowly.** Restrict each key to only the operations it needs (Restricted mode), and issue a separate key per service. Scope checks run only on the routes CodeSpar has mapped to a scope, so do not treat a Restricted key as a limit on what a leaked key can reach.
3. **Rotate keys every 90 days.** Automate rotation where possible using your secrets manager.
4. **Never log API keys.** Sanitize logs and error reports to avoid accidentally exposing keys. Mask all but the last 4 characters.
5. **Use environment variables.** Store keys in `process.env`, not in source code. Add `.env` to `.gitignore`.
6. **Monitor key usage.** Check the dashboard regularly for unexpected usage patterns that might indicate a compromised key.
7. **Revoke compromised keys immediately.** If a key is exposed, revoke it in the dashboard and create a new one. Do not wait.

```typescript
// Good: key from environment variable
const codespar = new CodeSpar({
  apiKey: process.env.CODESPAR_API_KEY,
});

// Bad: hardcoded key (will end up in version control)
const codespar = new CodeSpar({
  apiKey: "csk_live_abc123def456...",
});
```

## Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "Connect Links", description: "Hosted OAuth flow for end-user provider connections.", href: "/docs/concepts/connect-links" },
  { label: "CONCEPT", title: "Sessions", description: "Session lifecycle and how stored credentials attach.", href: "/docs/concepts/sessions" },
  { label: "CONCEPT", title: "Billing", description: "How per-settled-transaction pricing is metered.", href: "/docs/concepts/billing" },
  { label: "REFERENCE", title: "Connections API", description: "Manage OAuth connections and re-authorize servers.", href: "/docs/api/connections" },
  { label: "FAQ", title: "FAQ", description: "Common security and compliance questions.", href: "/docs/faq" },
]} />
