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

# Accept and pay out to debit cards

> Register a customer's debit card with the OMS browser SDK, then use it to fund buys and receive sell proceeds.

This guide registers a customer's debit card with the OMS browser SDK, then uses it as the source of a buy and the destination of a sell. For how registration works, the card object, statuses, and errors, see [Debit cards](https://docs.polygon.technology/payments/debit-cards.md).

## Before you start

You need:

* **Cards enabled on your project.** Ask your account manager to enable them. Until they're enabled, card registration, quotes, and transactions fail with `422 CardNotEnabledForProject`.
* **An individual customer with structured names.** Cards are registered under the customer's `firstName` and `lastName`. A customer that is not an individual is rejected with `422 AccountTypeRequirementsNotMet`.
* **A secret API key and secret** (`sk_sdbx_…` or `sk_live_…`) for your server. See [Get started](https://docs.polygon.technology/payments/get-started.md).
* **A publishable key** (`pk_sdbx_…` or `pk_live_…`) for the browser. See [Get a publishable key](#get-a-publishable-key).
* **Access to the browser SDK.** The SDK packages are in early access and are not available from the public npm registry. [Contact us](https://polygon.technology/get-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) to request access.

### Get a publishable key

A publishable key identifies your project in the browser. Unlike your secret key, it is safe to ship in page code.

1. In the OMS Dashboard, open **API Keys**.
2. Create a **Publishable** key for the environment you're building in, sandbox or production.
3. Add the **origin** of each page that shows the card form.

An origin is a scheme, a host, and an optional port, with no path or trailing slash:

| Use | Origin to add |
| - | - |
| Production site | `https://app.example.com` |
| Staging site | `https://staging.example.com` |
| Local development | `http://localhost:5173` (use your dev server's port) |

A key only works in the environment it was created for, so a sandbox publishable key pairs with a sandbox secret key.

## Register a card

The SDK renders the secure card fields, collects the fraud-screening signals, and submits everything in one step.

### 1. Install the packages

```bash theme={null}
npm install @polygonlabs/oms-sdk @polygonlabs/oms-risk-sdk
```

`@polygonlabs/oms-sdk` exports framework-neutral custom elements from its root and React bindings from `@polygonlabs/oms-sdk/react`, which requires React 18 or newer. `@polygonlabs/oms-risk-sdk` is the companion fraud-signal collector.

### 2. Add a customer token route to your server

The SDK calls this route at least twice on every submit: once to start the fraud-screening session and once to authorize the card write. The route receives `{ customerId }` from the browser. It must:

1. Authenticate your own signed-in user, with your usual session and CSRF protections.
2. Refuse the request if `customerId` isn't the OMS customer ID stored on that user's record. An ID sent by the browser is not proof that the user owns it.
3. Create a token for that customer with your project access token (see [Get started](https://docs.polygon.technology/payments/get-started.md)) and return the `{ token, expiresAt }` response unchanged.

```bash theme={null}
curl -X POST https://sandbox-api.polygon.technology/v0.13/customers/{customerId}/token \
  -H "Authorization: Bearer {accessToken}"
# → { "token": "…", "expiresAt": "…" }
```

Tokens expire after one minute, so create a fresh one for every call. Don't prefetch or cache them. Token creation takes no `Idempotency-Key`.

### 3. Create the OMS client and render the form

Create one `OMS` object per page and wrap your app in `OMSProvider`:

```tsx theme={null}
import { createRiskClient } from '@polygonlabs/oms-risk-sdk'
import { createOMS } from '@polygonlabs/oms-sdk'
import { OMSProvider, DebitCardForm } from '@polygonlabs/oms-sdk/react'

const oms = createOMS({
  publishableKey: 'pk_sdbx_…',
  risk: createRiskClient(),
  async getCustomerToken(customerId) {
    const response = await fetch('/api/oms/customer-token', {
      method: 'POST',
      body: JSON.stringify({ customerId }),
    })
    if (!response.ok) throw new Error('Could not authorize the card submission')
    return response.json()
  },
})

export function AddCard({
  customerId,
  onSaved,
}: {
  customerId: string
  onSaved: (externalAccountId?: string) => void
}) {
  return (
    <OMSProvider oms={oms} customerId={customerId}>
      <DebitCardForm onSuccess={({ externalAccountId }) => onSaved(externalAccountId)} />
    </OMSProvider>
  )
}
```

`DebitCardForm` includes the secure card fields, billing address inputs, inline validation, a status message, and submit and clear buttons. Fraud-signal collection starts when `OMSProvider` mounts. One page serves one customer; signing in as someone else needs a page reload.

To build your own layout, render `DebitCard` with `CardFields`, `BillingAddressFields`, and `CardFeedback`, and call `submit()` from the `useDebitCard()` hook. Without React, call `defineDebitCardElements()`, mount an `<oms-debit-card>` element containing `<oms-debit-card-field>` elements of type `number`, `expiry`, and `cvc`, and pass your `OMS` object to its `configure()` method.

### 4. Handle the result

`onSuccess` fires when OMS accepts the card and returns its `ext_` ID. Accepted means stored, not yet usable: wait until the card is `active` before quoting.

The form shows field errors and recovery messages itself. To react in your own UI, pass `onStateChange` and read the next step with `getDebitCardRecovery(state)`:

| Recovery | What to do |
| - | - |
| `correct_fields` | OMS or the form rejected a card or billing value. The customer fixes it and submits again. |
| `restore_authorization` | Your token route failed or the token was rejected. Nothing reached OMS, so the customer can submit again. |
| `retry` | A known, safe rejection. The customer can submit again. |
| `check_outcome` | The card may or may not have been saved, for example after a timeout. Reconcile before allowing another attempt. |
| `clear` | The submission succeeded or the fields can't be reused. Offer the clear button. |
| `reload` | The secure fields failed to load. Reload the page. |
| `blocked` | No action is available yet, for example while the fields are still loading. |

Don't retry automatically. Each customer token carries its own idempotency key, so a retry with a fresh token is a new request and can save a second copy of the card. For `check_outcome`, list the customer's external accounts from your server, keep the entries whose `type` is `card`, and match the last four digits and expiry:

```bash theme={null}
curl "https://sandbox-api.polygon.technology/v0.13/external-accounts?customerId={customerId}" \
  -H "Authorization: Bearer {accessToken}"
```

`GET /external-accounts` has no `type` filter, so filter the results yourself. If the card is there, use it. If not, let the customer try again.

### Content Security Policy

If your site sets a Content Security Policy, allow the OMS API and secure card hosts for your environment, the card vault and fraud-screening hosts the SDK loads, and your own customer token route. The SDK package's README lists the exact `script-src`, `frame-src`, and `connect-src` sources. The fraud-screening collector loads remotely, so check its requests in your deployment before you enforce the policy.

## Wait for the card to activate

A new card starts `pending`. Wait for `active` before quoting: subscribe to `externalAccount.statusChanged` or poll `GET /external-accounts/{id}`. If it lands in `failed` or `rejected`, read `failure.code` and see [Statuses](https://docs.polygon.technology/payments/debit-cards.md#statuses).

## Use a card on a quote

Once the card is `active`, reference it as the quote `source` for a buy or the `destination` for a sell. The card side always uses `asset: usd`, and OMS infers the network.

```json Card buy theme={null}
{
  "customerId": "cst_…",
  "source": {
    "type": "card",
    "details": { "id": "ext_…", "asset": "usd" },
    "amount": "100.00"
  },
  "destination": {
    "type": "walletExternal",
    "details": { "id": "ext_…", "asset": "usdc", "network": "polygon" }
  }
}
```

```json Card sell theme={null}
{
  "customerId": "cst_…",
  "source": {
    "type": "walletCrypto",
    "details": { "id": "wlt_…", "asset": "usdc", "network": "polygon" },
    "amount": "100.00"
  },
  "destination": {
    "type": "card",
    "details": { "id": "ext_…", "asset": "usd" }
  }
}
```

Set `amount` on one side only. OMS calculates the other.

## Execute the transaction

Card transactions are screened for fraud with signals from the customer's browser. In the browser, `oms.confirmCardTransaction({ quoteId, submit })` collects a fresh risk session and calls your `submit` callback with `{ quoteId, riskSession }`. Send both to your server over HTTPS, then create the transaction with `POST /transactions`, passing the two `riskSession` values as top-level fields:

```json theme={null}
{
  "quoteId": "qt_…",
  "webSessionId": "…",
  "deviceFingerprint": "…"
}
```

Pass the values through unchanged. Keep them in memory only, and never log or store them. A card transaction without `webSessionId` returns `422 RiskSessionRequired`. The quote already identifies the card, so the transaction doesn't take a card ID.

Return one of these from `submit`:

* `{ status: 'submitted', transactionId }` when OMS created the transaction. Submitted is not settled.
* `{ status: 'rejected' }` when you know nothing was submitted.
* `{ status: 'unknown' }` when the outcome is uncertain. Reconcile on your server before trying again.

See [Transactions](https://docs.polygon.technology/payments/transactions.md) for statuses and webhooks.

## Test in sandbox and on localhost

Build and test against sandbox. Moving to production is a key swap, not a code change.

| | Sandbox | Production |
| - | - | - |
| REST API | `https://sandbox-api.polygon.technology/v0.13` | `https://api.polygon.technology/v0.13` |
| Secure card host | `https://secure.sandbox-api.polygon.technology` | `https://secure.api.polygon.technology` |
| Keys | `sk_sdbx_…`, `pk_sdbx_…` | `sk_live_…`, `pk_live_…` |
| Cards | Test cards only | Real cards |

**Test card:** `4111 1111 1111 1111`, any future expiry, and any three-digit CVC.

To run the card form on your own machine:

1. Add your dev server's origin, such as `http://localhost:5173`, to your sandbox publishable key.
2. Serve the page from that origin. Opening the HTML as a file (`file://`) won't work.
3. Run your customer token route locally with your sandbox secret key. Keep the secret in an environment file, not in page code.
4. Open the page, type the test card into the secure fields, and submit.
5. Poll `GET /external-accounts/{id}` until the card is `active`.

Keep in mind:

* **The secure host only accepts submissions from the SDK's secure fields.** A `curl` or server-side request to it is rejected.
* **The secure fields are iframes from another origin.** Your page script can't read or set their values, and some browser automation tools can't type into them. End-to-end tests need a tool that can target cross-origin frames.
* **Set `autoComplete="off"`** on test forms so the browser doesn't fill in a saved real card.
* **For component tests,** import `createTestRiskClient()` and `createVaultTestSession()` from `@polygonlabs/oms-sdk/testing`. Pass the risk client as the `risk` option of `createOMS` and the session as the form's `session` prop. Both run in memory with no network calls.

## Idempotency

Server-side `POST` requests, including quotes and transactions, need an `Idempotency-Key` header. `PATCH` and token creation don't. The SDK sets the key for card registration from the customer token, so you never set one in the browser.

* **Same key and body, after the first request finished:** OMS returns the original response.
* **Same key while the first request is still running:** `409` with code `idempotencyKeyInProgress`. Wait, then retry with the same key.
* **Same key, different body:** `422` with code `idempotencyKeyReuse`. Use a new key for a corrected request.
* **No key:** `400` with code `missingIdempotencyKey`.
* **After a timeout:** retry with the same key until you get a definite answer.

These three responses use a short envelope, where `error` is a message and `code` is the string shown:

```json theme={null}
{ "error": "Idempotency key already used with a different request", "code": "idempotencyKeyReuse" }
```

## Related

* [Debit cards](https://docs.polygon.technology/payments/debit-cards.md): the card object, statuses, billing address rules, errors, and events

* [External accounts](https://docs.polygon.technology/payments/external-accounts.md): the shared model for cards, banks, and external wallets

* [Transactions](https://docs.polygon.technology/payments/transactions.md): the quote-and-execute flow

* [Sandbox and testing](https://docs.polygon.technology/payments/core-concepts/sandbox-and-testing.md): how the sandbox environment behaves

* [Receiving webhooks](https://docs.polygon.technology/payments/guides/receiving-webhooks.md): verify and apply `externalAccount.statusChanged` events
