---
name: knot-card-switcher
description: >
  Build a Knot CardSwitcher (Switch) integration end to end: create a card_switcher
  session, open the SDK, handle the AUTHENTICATED webhook, send encrypted card data to
  Switch Card (JWE), and handle CARD_UPDATED / CARD_FAILED. Also covers testing in
  development. Use when: (1) "integrate CardSwitcher",
  (2) "card switching" or "Switch", (3) "update card on file" at merchants, (4) "send card
  data" or calling /card, (5) building a "JWE card" payload, (6) handling "CARD_UPDATED" or
  CARD_FAILED webhooks.
metadata:
  author: Knot
  version: "1.0"
---

# Knot CardSwitcher

CardSwitcher lets your users set your card as the payment method on file at merchants (Amazon, Netflix, Uber, etc.). Your backend creates a session, the user logs in to a merchant in the Knot SDK, Knot sends you an `AUTHENTICATED` webhook, and your backend sends the card to Knot. Knot then updates the card at the merchant and sends `CARD_UPDATED` or `CARD_FAILED`.

For shared setup (credentials, base URLs, `Knot-Version`, webhook signature checks), see the `knot` skill. The short version: use HTTP basic auth with `client_id:secret` from the [Dashboard](https://dashboard.knotapi.com/developers/keys) against `https://development.knotapi.com` or `https://production.knotapi.com`. Keep the secret on your server.

If the Knot docs MCP server (`https://docs.knotapi.com/mcp`) is connected, search it before guessing a field or event name.

## Workflow

### Step 1: Register a webhook

Add a webhook URL for each environment under [Dashboard → Developers → Webhooks](https://dashboard.knotapi.com/developers/webhooks). Return `200` within 10 seconds and do the work asynchronously. Knot retries a failed delivery up to 2 more times, so deduplicate. Verify `Knot-Signature` as described in [Webhooks](https://docs.knotapi.com/webhooks.md).

### Step 2: Create a `card_switcher` session

Call `POST /session/create` from your server every time you open the SDK. `card_id` is required for `card_switcher`. It is your own ID for the card, and Knot echoes it back in the webhooks.

```bash
curl -X POST https://development.knotapi.com/session/create \
  -u "$CLIENT_ID:$SECRET" \
  -H "Content-Type: application/json" \
  -H "Knot-Version: 2.0" \
  -d '{
    "type": "card_switcher",
    "external_user_id": "abc123",
    "card_id": "81n9al10a0ayn13",
    "email": "ada.lovelace@gmail.com",
    "phone_number": "+11234567890",
    "metadata": { "reference_token": "abc123" }
  }'
# => { "session": "915efe72-..." }
```

- `email` and `phone_number` (E.164) are optional. When you send them, Knot detects the user's merchant accounts and puts them first in the SDK merchant list. See [Personalization](https://docs.knotapi.com/card-switcher/personalization.md).
- `processor_token` is optional too. It takes a Plaid processor token, which Knot uses to find merchants where the user spends. The personalized list shows up after a few seconds, not when the SDK first opens. See [Plaid Integration](https://docs.knotapi.com/card-switcher/plaid-integration.md).
- `metadata` takes up to 10 keys with values up to 500 characters. Knot echoes it in the webhooks for this session.
- If you leave out `card_id`, the call returns `400 INVALID_FIELD` ("The card_id field is required when type = card_switcher."). If CardSwitcher isn't enabled for your account, it returns `403 NO_ACCESS`.

Sessions expire after 30 minutes. When the SDK emits `REFRESH_SESSION_REQUEST`, call `POST /session/extend`.

### Step 3: Open the SDK

For install steps and code for each platform, use the `knot-sdk` skill. The product comes from the session `type`. On iOS, the SDK `product` parameter is ignored from version 1.0.11. The minimal config:

```javascript
knotapi.open({
  sessionId,                  // from /session/create
  clientId,                   // must match the session's environment
  environment: "development", // or "production"
  entryPoint: "onboarding",   // returned in AUTHENTICATED as data.entrypoint
  onSuccess: () => {}, onError: () => {}, onEvent: () => {}, onExit: () => {},
});
```

`customerConfiguration.cardName` / `customerName` are optional and change how the card is labeled in the SDK ("Your [customerName] [cardName] was added."). Both values must be allowlisted by Knot first. Otherwise the SDK returns `INVALID_CARD_NAME` / `INVALID_CUSTOMER_NAME`.

Use SDK callbacks for UI only. Webhooks are the source of truth.

### Step 4: Handle `AUTHENTICATED`, then send the card within 15 seconds

`AUTHENTICATED` fires after the user logs in to a merchant. The payload includes `task_id` (integer), `external_user_id`, `merchant.{id,name}`, `session_id`, and `data.{card_id, send_card, entrypoint, metadata}`. See [AUTHENTICATED](https://docs.knotapi.com/link/webhook-events/authenticated.md).

- Send the card **only when `data.send_card` is `true`**. For example, when a user logs in to Google to switch at several merchants, that event has `send_card: false`. You then get one `AUTHENTICATED` with `send_card: true` for each merchant the user picks. See [Mass Switcher](https://docs.knotapi.com/card-switcher/mass-switcher.md).
- Call Switch Card **within 15 seconds** of receiving the webhook. Return the `200` to the webhook right away and send the card from a background job.

### Step 5: Send card data with Switch Card (JWE) (default)

Knot recommends this option. You encrypt the card payload into a JWE with Knot's public key, so plaintext card data never goes to Knot's API.

1. Call `GET /jwe/key` ([Retrieve JWK](https://docs.knotapi.com/api-reference/products/card-switcher/retrieve-jwk.md)) to get an RSA JWK (`alg: RSA-OAEP-256`, plus a `kid`). Cache it, for example for a day, instead of fetching it on every authentication.
2. Encrypt the JSON payload below as a compact JWE: `alg` = the JWK's `alg` (RSA-OAEP-256), `enc` = `A256GCM`, and the JWK's `kid` in the protected header.
3. Call `POST /card` ([Switch Card (JWE)](https://docs.knotapi.com/api-reference/products/card-switcher/switch-card-jwe.md)) with `{ "task_id": "<task_id as string>", "jwe": "<compact JWE>" }`.

Payload (these exact values pass validation in development):

```json
{
  "user": {
    "name": { "first_name": "Ada", "last_name": "Lovelace" },
    "address": {
      "street": "100 Main Street", "street2": "#100", "city": "NEW YORK",
      "region": "NY", "postal_code": "12345", "country": "US"
    },
    "phone_number": "+11234567890"
  },
  "card": { "number": "4242424242424242", "expiration": "08/2030", "cvv": "012" }
}
```

Field rules: `region` is an ISO 3166-2 subdivision code, `country` is ISO 3166-1 alpha-2, `phone_number` is E.164, `postal_code` is 5 to 10 characters, `street`/`street2` are at most 46 characters, `city` is at most 32, `expiration` is `MM/YYYY` or `MM/YY`, and `cvv` is at most 4 digits.

```typescript
import { CompactEncrypt, importJWK, JWK } from "jose";

const BASE = "https://development.knotapi.com";
const auth = "Basic " + Buffer.from(`${CLIENT_ID}:${SECRET}`).toString("base64");

async function getKey(): Promise<JWK> {
  const r = await fetch(`${BASE}/jwe/key`, { headers: { Authorization: auth } });
  if (!r.ok) throw new Error(`JWK ${r.status}`);
  return r.json(); // cache this
}

async function switchCard(taskId: number, cardData: object) {
  const jwk = await getKey();
  const key = await importJWK(jwk, jwk.alg!);
  const jwe = await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(cardData)))
    .setProtectedHeader({ alg: jwk.alg!, enc: "A256GCM", kid: jwk.kid })
    .encrypt(key);

  const r = await fetch(`${BASE}/card`, {
    method: "POST",
    headers: { Authorization: auth, "Content-Type": "application/json", "Knot-Version": "2.0" },
    body: JSON.stringify({ task_id: String(taskId), jwe }),
  });
  const body = await r.json();
  if (!r.ok) throw new Error(`${body.error_code}: ${body.error_message}`);
}
```

[Sending Card Data](https://docs.knotapi.com/card-switcher/sending-card-data.md) has full samples in Python, Go, Java, and VGS StarLarky.

A `200 { "message": "Success" }` means the request **passed validation**, including the JWE. It does not mean the card was switched. Wait for the webhook in Step 6. Common `400` errors: `INVALID_JWE` with a specific message ("The card.number is invalid and does not pass the Luhn check.", "The user.phone number is required.", "The jwe is invalid.", "The card is expired.") and `ONGOING_OPERATION` ("An existing operation is in progress."). `403 NO_ACCESS` means the endpoint isn't enabled for you.

#### Other ways to send card data

- **Switch Card (plaintext JSON).** The same `POST /card` with `user` and `card` objects instead of `jwe` ([Switch Card](https://docs.knotapi.com/api-reference/products/card-switcher/switch-card.md)). It runs on a separate secure host (`https://secure.development.knotapi.com` in development) run by Knot's PCI-compliant vendor. It is meant for customers who already store cards with a PCI-compliant vault provider (for example VGS or Basis Theory) and route the request from that vault. Don't send raw card numbers from your own servers. Use JWE instead. mTLS can be enabled for this endpoint on request. VGS has a "KnotAPI" route template.
- **Processor integrations.** If your issuer processor is Unit or I2C, Knot can fetch the card data directly. See [Unit](https://docs.knotapi.com/card-switcher/processor-digital-banking-integrations/unit.md) and [I2C](https://docs.knotapi.com/card-switcher/processor-digital-banking-integrations/i2c.md).

### Step 6: Handle the result webhooks

**`CARD_UPDATED`** means the card was updated at the merchant. The payload has `task_id`, `external_user_id`, `merchant`, `data.card_id`, `data.metadata`, and optionally `data.subscriptions[].id` (enabled on request). See [CARD_UPDATED](https://docs.knotapi.com/card-switcher/webhook-events/card-updated.md).

**`CARD_FAILED`** means the update failed. `data.reason` gives the cause:

| Reason group | `data.reason` values |
| --- | --- |
| Card or user data | `card`, `card expired`, `card in use`, `insufficient funds` |
| Merchant account state | `account`, `subscription`, `subscription admin`, `third-party payment method on subscription`, `too close to end of billing cycle` |
| Login | `credentials`, `otp`, `too many attempts`, `credentials timeout`, `otp timeout`, `questions timeout`, `zip timeout`, `session not authenticated` |
| Card delivery to Knot | `did not receive payment method information`, `could not handle payment method information`, `could not retrieve payment method information` |
| Unknown | `other` |

If you see `did not receive payment method information`, check that your card send is firing within the 15-second window. See [CARD_FAILED](https://docs.knotapi.com/card-switcher/webhook-events/card-failed.md) for what each reason means.

On these events `session_id` can be `null`. When it is missing, leave it out of the signature hash map.

**`MERCHANT_STATUS_UPDATE`**: you only need this if you show merchants natively in your app. It fires separately for each product `type` (`card_switcher`, ...) and `platform` (`ios`/`android`/`web`), with `status` `UP`/`DOWN` and a minimum `sdk` version. Filter on `data.type == "card_switcher"`. It has no `session_id`. See [MERCHANT_STATUS_UPDATE](https://docs.knotapi.com/link/webhook-events/merchant-status-update.md).

## Testing in development

**Backend only (no SDK).** Call `POST /development/accounts/link` to simulate a login and fire `AUTHENTICATED`:

```bash
curl -X POST https://development.knotapi.com/development/accounts/link \
  -u "$CLIENT_ID:$SECRET" -H "Content-Type: application/json" \
  -d '{ "external_user_id": "abc123", "merchant_id": 19, "card_switcher": true, "card_id": "81n9al10a0ayn13" }'
```

`card_id` is required when `card_switcher` is `true`, and you can't combine `card_switcher` with `transactions`. After the webhook arrives, send the Step 5 test payload within 15 seconds and wait for `CARD_UPDATED`. See [Link Account](https://docs.knotapi.com/api-reference/development/link-account.md).

**End to end (with the SDK).** Create a session, open the SDK, and log in with these test credentials:

| Scenario | Username | Password |
| --- | --- | --- |
| Success | `user_good` | `pass_good` |
| OTP (`1234` valid, `0000` invalid) | `user_good` | `pass_otp` |
| Invalid credentials | `credentials` | `failed` |
| Account / merchant failure | `account` / `merchant` | `failed` |
| Too many attempts | `too many attempts` | `failed` |
| Card not supported / insufficient funds | `card not supported` / `insufficient funds` | `failed` |
| No subscription / not subscription admin | `subscription` / `subscription admin` | `failed` |

See [CardSwitcher Testing](https://docs.knotapi.com/card-switcher/testing.md). It also covers production testing: use US devices and accounts, don't retry the same card or account over and over, and turn off your VPN.

## Pitfalls

- **Sending the card too early or too late.** Send it only after `AUTHENTICATED` with `send_card: true`, and within 15 seconds. `/card` returns `400 ONGOING_OPERATION` ("An existing operation is in progress.") when an operation is already running for the account.
- **Treating the `/card` 200 as success.** It only confirms validation. The switch result comes in `CARD_UPDATED` / `CARD_FAILED`.
- **`task_id` type.** Webhooks send `task_id` as an integer, but `/card` expects a string.
- **Incomplete user data.** Many merchants require name, billing address (AVS-checked), and phone number. Send real values in production. Development accepts the sample payload.
- **Cards that can't be charged.** The card must be active (not locked or frozen). Debit and prepaid cards need funds for small merchant authorization holds.
- **Wrong host for plaintext.** Plaintext `user`/`card` goes to the secure host. `jwe` goes to the standard API host.
- **Reusing sessions.** Create a new session for each SDK open. The session, `clientId`, and `environment` must all belong to the same environment.

## Going live

Work through the [Launch checklist](https://docs.knotapi.com/launch-checklist.md). For CardSwitcher specifically, always call Switch Card within 15 seconds of `AUTHENTICATED`, and handle `MERCHANT_STATUS_UPDATE` if you list merchants natively.
