---
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` | Execute tool calls via `session.execute()` and `session.send()` |
| `servers:read` | Browse the server catalog |

**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 type="info">
Scopes cover the bearer-key surface: sessions, tools, and servers. Billing and API-key management are dashboard/operator-only and aren't reachable with an API key regardless of scope.
</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, live) | mTLS with X.509 client certificate (PEM blob); operator uploads cert + key + CA via the Provider Connect modal |
| `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 run 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.

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), so a leaked key can't do more than its job. Issue a separate key per service.
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" },
]} />
