> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcard.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect users

> Authenticate your server, connect a user, and get the wallet link that opens their wallet.

Before a user can have a wallet in your product, two things happen: your server proves who it is, and the user proves who they are. Both take one call each. This page is the server side of the whole integration.

## Keys and tokens

There are only three credentials in the system, and each one has exactly one place it goes.

| Credential           | What it is                                                         | Who holds it                                                  | Where it goes                          |
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------- | -------------------------------------- |
| **Org credential**   | Your `client_id` and `client_secret`, exchanged for a bearer token | Your server only. Never a browser, never an agent             | Every `/api/v2` call your server makes |
| **Wallet link**      | A short-lived URL that opens one user's wallet                     | Travels to the user: through your frontend, a text, or a chat | The wallet component, on any platform  |
| **Connection token** | One user's session, with a refresh token                           | Your server stores and refreshes it                           | Calls made on behalf of that user      |

The pattern to remember: your org credential stays on your server. The wallet link is the only thing that travels to the user. The connection token comes back when a user verifies, and your server keeps it.

## Authenticate your server

Exchange your credentials for a bearer token. The request is form-encoded, per OAuth:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agentcard.sh/api/v2/oauth/token \
    -d grant_type=client_credentials \
    -d client_id=YOUR_CLIENT_ID \
    -d client_secret=YOUR_CLIENT_SECRET
  ```

  ```javascript Node theme={null}
  const res = await fetch("https://api.agentcard.sh/api/v2/oauth/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: "YOUR_CLIENT_ID",
      client_secret: "YOUR_CLIENT_SECRET",
    }),
  });
  const { access_token } = await res.json();
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.agentcard.sh/api/v2/oauth/token",
      data={
          "grant_type": "client_credentials",
          "client_id": "YOUR_CLIENT_ID",
          "client_secret": "YOUR_CLIENT_SECRET",
      },
  )
  access_token = res.json()["access_token"]
  ```
</CodeGroup>

You get back an `access_token` with an `expires_in`. Cache it and refresh when it expires. Whether it's a sandbox or production token follows the credential, not the URL.

## Connect a user

Send the user a one-time code, on email or phone:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agentcard.sh/api/v2/connect/start \
    -H "Authorization: Bearer $ORG_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"email": "user@example.com", "external_user_id": "your-internal-id"}'
  ```

  ```javascript Node theme={null}
  const res = await fetch("https://api.agentcard.sh/api/v2/connect/start", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${ORG_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email: "user@example.com", external_user_id: "your-internal-id" }),
  });
  const attempt = await res.json();
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.agentcard.sh/api/v2/connect/start",
      headers={"Authorization": f"Bearer {ORG_TOKEN}"},
      json={"email": "user@example.com", "external_user_id": "your-internal-id"},
  )
  attempt = res.json()
  ```
</CodeGroup>

`external_user_id` is optional. It's your own id for the user, and it comes back on webhooks so you can match them up.

Verify the code the user gives you. In sandbox the code is always `111111`:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agentcard.sh/api/v2/connect/verify \
    -H "Authorization: Bearer $ORG_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"connect_id": "CONNECT_ATTEMPT_ID", "code": "111111"}'
  ```

  ```javascript Node theme={null}
  const res = await fetch("https://api.agentcard.sh/api/v2/connect/verify", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${ORG_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ connect_id: "CONNECT_ATTEMPT_ID", code: "111111" }),
  });
  const connection = await res.json();
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.agentcard.sh/api/v2/connect/verify",
      headers={"Authorization": f"Bearer {ORG_TOKEN}"},
      json={"connect_id": "CONNECT_ATTEMPT_ID", "code": "111111"},
  )
  connection = res.json()
  ```
</CodeGroup>

The response is the connection: the user's `user_id`, an `access_token` (the connection token), and a `refresh_token`. Store all three. The `connection.created` webhook fires here.

Then record the user's authorization once:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agentcard.sh/api/v2/connect/consent \
    -H "Authorization: Bearer $ORG_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"user_id": "USER_ID"}'
  ```

  ```javascript Node theme={null}
  const res = await fetch("https://api.agentcard.sh/api/v2/connect/consent", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${ORG_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ user_id: "USER_ID" }),
  });
  const data = await res.json();
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.agentcard.sh/api/v2/connect/consent",
      headers={"Authorization": f"Bearer {ORG_TOKEN}"},
      json={"user_id": "USER_ID"},
  )
  data = res.json()
  ```
</CodeGroup>

Consent is what makes the user's wallet available to your product. Wallet links won't be created without it.

## Create wallet links

With a connected, consented user, your server can create wallet links whenever the wallet should open:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agentcard.sh/api/v2/wallet_links \
    -H "Authorization: Bearer $ORG_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"user_id": "USER_ID"}'
  ```

  ```javascript Node theme={null}
  const res = await fetch("https://api.agentcard.sh/api/v2/wallet_links", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${ORG_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ user_id: "USER_ID" }),
  });
  const link = await res.json();
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.agentcard.sh/api/v2/wallet_links",
      headers={"Authorization": f"Bearer {ORG_TOKEN}"},
      json={"user_id": "USER_ID"},
  )
  link = res.json()
  ```
</CodeGroup>

The response has a `url`. That link opens the user's wallet on any platform: the web embed, the iOS sheet, a text message, or the hosted page. Links are short-lived; create a fresh one each time rather than storing them.

## Keep the session alive

Connection tokens expire. Rotate them with the refresh token:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agentcard.sh/api/v2/connect/refresh \
    -H "Authorization: Bearer $ORG_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"refresh_token": "REFRESH_TOKEN"}'
  ```

  ```javascript Node theme={null}
  const res = await fetch("https://api.agentcard.sh/api/v2/connect/refresh", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${ORG_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ refresh_token: "REFRESH_TOKEN" }),
  });
  const session = await res.json();
  ```

  ```python Python theme={null}
  import requests

  res = requests.post(
      "https://api.agentcard.sh/api/v2/connect/refresh",
      headers={"Authorization": f"Bearer {ORG_TOKEN}"},
      json={"refresh_token": "REFRESH_TOKEN"},
  )
  session = res.json()
  ```
</CodeGroup>

Each refresh returns a new pair and invalidates the old one, so store what comes back.

## Webhooks you will receive

* `connection.created` when a user verifies, with your `external_user_id` on it

## When it fails

* An auth error usually means the wrong credential for the call. Check the table at the top.
* `user_info_required` on wallet links means consent was never recorded for that user. Call `/connect/consent`.
* `subscription_required` in production means your organization hasn't subscribed yet. Sandbox never requires one.
* `invalid_refresh_token` means the refresh token was already used or expired. Reconnect the user.

## Sandbox behavior

Sandbox never sends a real email or SMS, and the code is always `111111`. Sandbox users are isolated: connecting `anyone@example.com` in sandbox can never touch a real account with that email.

Next: [Webhooks](/wallet/webhooks)
