Skip to main content
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.

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.
  • A publishable key (pk_sdbx_… or pk_live_…) for the browser. See 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 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: 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

@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) and return the { token, expiresAt } response unchanged.
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:
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): 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:
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.

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