---
title: Connect Links
description: Hosted OAuth flow that lets your end users connect their own Stripe, Mercado Pago, Shopify, and other provider accounts — without you building or maintaining a connection UI.
---

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

# Connect Links

<VersionBadge pkg="@codespar/sdk" />

A **Connect Link** is a hosted page that runs the entire OAuth dance for an end user: provider consent, callback handling, token exchange, encrypted storage, and scope checks. Your agent sends the link, the user clicks, and on the next tool call CodeSpar uses the stored credentials automatically.

You never touch raw provider tokens. You never build a connection UI. You never write the refresh logic.

<Callout type="info">
Connect Links are for **end-user** authentication — your customers connecting their own Stripe / Mercado Pago / Shopify accounts. For **your** credentials (the platform's own provider keys shared across all users), use [Service Auth](/docs/concepts/authentication#service-authentication) instead.
</Callout>

Connect Links is the SELL-side identity surface (your customers connect their own provider accounts to get paid). The buy-side analog is [Shopper Identity](/docs/concepts/shopper-identity) (vaulted shopper credentials for `codespar_shop`).

## Lifecycle

A connection moves through a small state machine:

```
     start              authorize            token exchange
  ┌────────┐  link    ┌──────────┐  code   ┌──────────────┐
  │ CLIENT │ ───────▶│ HOSTED   │ ──────▶│ CODESPAR      │
  │        │         │ PAGE     │         │ VAULT         │
  └────────┘         └──────────┘         └──────────────┘
      │                                          │
      │            tools execute                 │
      │ ◀────────────────────────────────────────┘
      │            using stored credential
```

| Status | Meaning |
|--------|---------|
| `pending` | A Connect Link was issued. User has not authorized yet. |
| `connected` | Token stored in vault, scoped to the project. Tools will run. |
| `expired` | Refresh token is dead (user revoked on the provider side, or token TTL elapsed without activity). Re-issue a Connect Link. |
| `revoked` | Your application called `/v1/connections/:id/revoke`. Stored credentials were deleted. |

Credentials are scoped to your **project**, not to individual users. Once a connection is `connected`, every session in that project — regardless of which `userId` you pass to `codespar.create()` — uses the same stored credentials. Tenant isolation is at the project boundary: project A never reaches project B's Stripe account, enforced in the vault.

<Callout type="info">
The `user_id` you pass through OAuth (and store on the connection row) is **audit metadata** — who clicked Connect — not a credential gate. If your product has multiple end-users sharing one merchant account (the typical commerce shape), one Connect Link per project is enough.
</Callout>

## Two integration patterns

Pick the pattern that matches when the user connects.

### Pattern 1 — in-chat

The user is already in a conversation with your agent. The agent tries a tool, CodeSpar notices no connection exists, and the tool result returns a Connect Link that the agent surfaces in the chat.

```typescript
const session = await codespar.create("user_abc", {
  servers: ["stripe"],
  manageConnections: { waitForConnections: false },
});

const result = await session.execute("codespar_checkout", {
  amount: 14900,
  currency: "BRL",
});

if (result.error === "needs_connection") {
  // result.data.connect_url is the hosted page
  // Send it to the user in the conversation
  await chat.send(`Connect your Stripe account to continue: ${result.data.connect_url}`);
}
```

**Use when:** chat-first products where interrupting the flow for an onboarding page hurts conversion. WhatsApp commerce agents, embedded support assistants, in-app copilots.

### Pattern 2 — manual (pre-onboarding)

You want users connected **before** they chat. Your onboarding screen or settings page calls `session.authorize()` and redirects to the Connect Link.

```typescript
const session = await codespar.create("user_abc", {
  servers: ["stripe"],
});

const { connected, redirectUrl } = await session.authorize("stripe");

if (!connected && redirectUrl) {
  window.location.href = redirectUrl;
}
```

After the user authorizes, your configured **success redirect** URL is called. On subsequent agent runs, any session in the same project picks up the stored credential automatically — the `userId` you pass to `codespar.create()` doesn't need to match the one that authorized.

**Use when:** dashboards, settings pages, multi-tenant SaaS products where "connect your accounts" is part of onboarding.

## `session.authorize()`

The SDK entry point for issuing a Connect Link.

```typescript
const result = await session.authorize("stripe", {
  redirectUrl: "https://app.example.com/onboarding/done",
  scopes: ["read_write"], // override defaults
});
```

Returns:

```typescript
interface AuthResult {
  connected: boolean;      // true if already connected — skip redirect
  redirectUrl?: string;    // hosted Connect Link URL
  error?: string;          // if the server does not support this auth type
}
```

If `connected: true` you can skip the redirect entirely — the stored credential is still valid.

## Backend shape

The SDK wraps three HTTP endpoints. You can call them directly from any language.

| Method | Endpoint | Purpose |
|--------|----------|---------|
| `POST` | `/v1/connections/start` | Issue a Connect Link for a user + server |
| `GET` | `/v1/connections` | List this user's connections and statuses |
| `POST` | `/v1/connections/:id/revoke` | Delete the stored credential (end-user opt-out) |

### `POST /v1/connections/start`

```bash
curl -X POST https://api.codespar.dev/v1/connections/start \
  -H "Authorization: Bearer csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user_abc",
    "server_id": "stripe",
    "redirect_url": "https://app.example.com/onboarding/done"
  }'
```

Response:

```json
{
  "link_token": "clt_abc123...",
  "authorize_url": "https://codespar.dev/connect/stripe?token=clt_abc123",
  "expires_at": "2026-04-17T16:00:00Z"
}
```

The `authorize_url` is the Connect Link. `link_token` expires after 15 minutes; re-issue if the user takes longer.

### `GET /v1/connections`

```bash
curl https://api.codespar.dev/v1/connections?user_id=user_abc \
  -H "Authorization: Bearer csk_live_..."
```

Response:

```json
{
  "data": [
    {
      "id": "conn_abc123",
      "server_id": "stripe",
      "user_id": "user_abc",
      "auth_type": "oauth",
      "status": "connected",
      "display_name": "acme-store.myshopify.com",
      "connected_at": "2026-04-17T15:30:00Z",
      "expires_at": "2027-04-17T15:30:00Z"
    }
  ]
}
```

## Customizing the hosted page

Connect Links inherit your account's branding. Configure once in [Settings](https://codespar.dev/dashboard/settings):

| Setting | Effect |
|---------|--------|
| **Logo** | Shown at the top of the hosted page |
| **Accent color** | Primary button and links |
| **Custom domain** | Serve from `connect.yourdomain.com` instead of `codespar.dev/connect/*` (requires CNAME + certificate) |
| **Success redirect** | Default URL after authorization if `redirect_url` is omitted |
| **Legal footer** | Privacy policy and ToS links shown beside provider consent |

<Callout>
The Connect Link page is static HTML + CSS — no third-party scripts, no analytics pixels. It's designed to pass enterprise security reviews on the first pass.
</Callout>

## Troubleshooting

**The user authorized but `status` is still `pending`.** Wait up to 10 seconds — the callback is asynchronous. If it still hasn't flipped, check the [triggers](/docs/concepts/triggers) event stream: a failed token exchange fires `connection.failed` with an `error_code` explaining why (scope rejected, redirect URI mismatch, etc.).

**"redirect_uri_mismatch" from the provider.** Your provider's app config must whitelist `https://codespar.dev/oauth/callback` (or your custom domain's equivalent). See the provider page for the exact URI.

**The Connect Link works in test but 404s in production.** Live keys issue live Connect Links; test keys issue test Connect Links. You cannot hand a `csk_test_`-issued link to a user authenticating against a real Stripe account.

**Expired token after months of inactivity.** Providers revoke refresh tokens when a user hasn't used your app for a provider-defined period (Stripe: 13 months, Shopify: 24 hours of inactivity on some scopes). CodeSpar fires `connection.expired` via a trigger — re-issue a Connect Link when you receive it.

## Next steps

<NextStepsGrid items={[
  { label: "CONCEPT", title: "Authentication", description: "Service auth, API keys, and where Connect Links sit in the overall auth model.", href: "/docs/concepts/authentication" },
  { label: "CONCEPT", title: "Triggers", description: "Listen for connection.connected, connection.expired, connection.failed events.", href: "/docs/concepts/triggers" },
  { label: "REFERENCE", title: "Connections API", description: "Full HTTP reference for the connections endpoints.", href: "/docs/api/connections" },
  { label: "COOKBOOK", title: "Multi-tenant SaaS", description: "End-to-end example using Connect Links for onboarding.", href: "/docs/cookbooks/multi-tenant" },
]} />
