---
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 as `data.metadata` in the webhooks for this session. If the session has no `metadata`, the webhooks leave out `data.metadata` entirely, so read it as optional.
- 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: ({ merchant }) => {}, onError: ({ errorCode, errorDescription }) => {},
  onEvent: ({ event, merchant, merchantId, taskId }) => {}, onExit: () => {},
});
```

On the Web SDK, every callback receives a single object, not positional arguments. When testing locally, serve your page over `https://localhost`: Knot's SDK frame only allows `https://localhost:*` as a parent, so on `http://localhost` the SDK shows an endless spinner and no `onError` fires.

`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}`, plus `data.metadata` when the session set it. 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.

This sample is an ES module. Set `"type": "module"` in `package.json` (or use a `.mts` file), or Node fails with "Cannot use import statement outside a module".

```typescript
import { CompactEncrypt, importJWK, type 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.

`task_id` is checked before the JWE is decrypted. An unknown `task_id` returns `400 TASK_NOT_FOUND` ("The task was not found. Please check the ID provided.") and hides any card-data errors, so test JWE validation against a real task.

#### 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` (only when the session set it), 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`. The call returns `200` right away, but the simulated login keeps running for about 10 seconds. A `/card` sent in that window returns `400 ONGOING_OPERATION`. 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. With `user_good` / `pass_good`, `CARD_UPDATED` arrives about 4 seconds after your `/card` call:

| 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.

## Going live

Work through the [Launch checklist](https://docs.knotapi.com/launch-checklist.md). For CardSwitcher specifically:

- Call Switch Card within 15 seconds of `AUTHENTICATED`.
- Send the cardholder's real name, billing address, and phone number. Merchants check them against the card.
- Use an active card. Debit and prepaid cards need enough funds for small merchant authorization holds.
- Handle `MERCHANT_STATUS_UPDATE` if you list merchants natively.
