# High Wire Developers (/docs)



Welcome to the High Wire Payments developer documentation. The **merchant API** guides below cover
every server-side flow with field tables and complete examples; the generated API reference lists
every endpoint and schema. Browser integrations have their own sections:
[Payment SDK](/docs/payment-sdk) and [highwire.js](/docs/highwire-js).

<Cards>
  <Card title="Introduction" description="What the API offers, the environments, and an index of every endpoint." href="/docs/introduction" />

  <Card title="Authentication" description="HTTP Basic credentials, rotation, and the 401 and 403 responses." href="/docs/authentication" />

  <Card title="Make your first payment" description="Charge a test card, read it back, and refund it in three calls." href="/docs/make-your-first-payment" />

  <Card title="Card payments" description="Authorize and capture, auth then capture or void, 3-D Secure, device data, declines." href="/docs/card-payments" />

  <Card title="Hosted checkout" description="Create a payment link and confirm the payment before you fulfil the order." href="/docs/hosted-checkout" />

  <Card title="Saved payment methods" description="Save a card during a charge and charge it again within its bounds." href="/docs/saved-payment-methods" />

  <Card title="Refunds" description="Full and partial refunds, the refund's own status, and disputes." href="/docs/refunds" />

  <Card title="Payment lifecycle" description="The payment object, every status and transition, and how to poll." href="/docs/payment-lifecycle" />

  <Card title="Account limits" description="Merchant limits and velocity checks, and exactly what they return." href="/docs/account-limits" />

  <Card title="Idempotency key" description="Retry writes safely: scope, replays, 409 and 422, and the 5xx rule." href="/docs/idempotency-key" />

  <Card title="Errors and conventions" description="Every response body shape, status code, and retry rule." href="/docs/errors-and-conventions" />
</Cards>


# API reference (/docs/highwire-js/api-reference)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## `HighWire(options?)` [#highwireoptions]

Creates a `HighWireInstance`. Importing the ESM build and calling `HighWire()` do not access DOM APIs or make network requests. Loading the UMD build registers `window.HighWire`, but does not collect context or make a request until merchant code calls a method.

```ts
HighWire(options?: HighWireOptions): HighWireInstance

interface HighWireOptions {
  apiBase?: string;
}
```

| Argument          | Type              | Required | Description                                                                                     |
| ----------------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `options`         | `HighWireOptions` | No       | Optional compatibility settings. The supported collection flow requires no configuration.       |
| `options.apiBase` | `string`          | No       | v0.2.3 accepts but ignores `apiBase`; it does not override or default an active request target. |

No publishable key is required for the current collection flow.

## `hw.collect(options?)` [#hwcollectoptions]

Gathers the supported device, page, and observed paste-event signals. The supported merchant call remains:

```ts
const deviceData = await hw.collect();
```

The shipped declaration uses this non-exported alias and exported interface:

```ts
collect(options?: CollectOptions): Promise<CollectedPayload | null>

interface CollectOptions {
  pastedFields?: PastedField[];
}

type PastedField = 'number' | 'cvc' | 'exp' | 'zip';
```

There are no supported merchant-settable collection options in v0.2.3. The exposed parameter is a compatibility artifact: caller-provided `pastedFields` is replaced by the instance's `observeField()` snapshot. Call `collect()` with no argument and use `observeField()` as the only supported source of paste names.

Collection has three observable outcomes:

1. When the required IP address and user agent are available, it resolves to `CollectedPayload`.
2. For handled provider or required-context failures, the promise resolves to `null`.
3. A missing or broken platform primitive can reject the promise. Payment code must catch that rejection and continue without `deviceData`.

Collection reads the user agent, attempts ipify, then attempts ifconfig.co if needed, with a five-second timeout per provider. Provider-side CORS can still prevent a permitted origin from returning a readable response.

Forward a successful object without modifying it. For either unavailable outcome, omit `deviceData`; do not send `null`, an empty object, or a substitute. [Device data](/docs/highwire-js/device-data) defines the complete payload.

## `hw.observeField(element, fieldName)` [#hwobservefieldelement-fieldname]

Registers a paste-event observer for a checkout field.

```ts
observeField(element: Element, fieldName: string): Unobserve
```

Guard DOM queries before passing an element:

```ts
const cardNumber = document.querySelector('#card-number');
if (cardNumber) {
  const unobserve = hw.observeField(cardNumber, 'number');
}
```

| Argument    | Type      | Required | Description                                                                                                        |
| ----------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
| `element`   | `Element` | Yes      | The checkout input to observe. The observer listens for `paste` and never reads `element.value` or pasted content. |
| `fieldName` | `string`  | Yes      | A case-sensitive supported name. Unsupported names are silently ignored.                                           |

The returned `Unobserve` function has the type `() => void`. Calling it detaches that listener and stops future events from that element. It does not clear paste names already recorded by the instance.

## Accepted field names [#accepted-field-names]

Only `"number"`, `"cvc"`, `"exp"`, and `"zip"` are recorded. `PastedField` is not importable in v0.2.3, so consumer code that needs a named type should spell out those four literals.

## ESM exports [#esm-exports]

| Export                | Kind                 | Notes                                                                |
| --------------------- | -------------------- | -------------------------------------------------------------------- |
| `HighWire`            | Named runtime export | Recommended factory import.                                          |
| `default`             | Runtime export       | Alias of `HighWire`.                                                 |
| `HIGHWIRE_JS_VERSION` | Named runtime export | Shipped library version string.                                      |
| `HighWireInstance`    | Type export          | Instance contract.                                                   |
| `HighWireOptions`     | Type export          | Factory compatibility options.                                       |
| `CollectOptions`      | Type export          | Exposed compatibility type; no merchant-settable value is supported. |
| `CollectedPayload`    | Type export          | Successful collection payload.                                       |
| `DeviceSignals`       | Type export          | Required device block.                                               |
| `PageSignals`         | Type export          | Optional page block.                                                 |
| `Unobserve`           | Type export          | Observer cleanup function.                                           |

Use the delivered ESM file and adjacent declaration for TypeScript. Typed CommonJS consumption is not documented for v0.2.3 because the shared declaration models the ESM module rather than the callable CommonJS runtime export.

## Browser-only collection [#browser-only-collection]

`collect()` must run in the cardholder's browser. It relies on `globalThis`, `Promise`, `fetch`, `AbortController`, `navigator.userAgent`, and optional page globals; modern Node.js can return valid-looking server context instead of throwing, so server components, API routes, `getServerSideProps`, and other server paths must not call it.

`observeField()` requires an actual browser `Element`/event target to record useful events. Import and construction may remain outside the event handler, but collection and observation belong in client-rendered checkout code. See [Browser support](/docs/highwire-js/security-and-privacy#browser-support) and [React and Next.js](/docs/highwire-js/guides/react-nextjs).


# Device data (/docs/highwire-js/device-data)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## Collected signals [#collected-signals]

`hw.collect()` gathers a fixed set of browser context:

* `device.ip` is the cardholder's public IPv4 address. Collection requests ipify first and attempts ifconfig.co if needed; each provider has a five-second timeout. A merchant CSP can permit an origin, but provider-side CORS still determines whether browser code can read the response.
* `device.userAgent` comes from `navigator.userAgent` and is truncated to 1024 characters.
* `page.referrer` comes from `document.referrer` immediately before payload assembly. It can contain URL data, is truncated to 2048 characters, and is omitted when empty.
* `page.timeOnPageMs` is computed immediately before payload assembly from `performance.timeOrigin`. It is a non-negative integer, includes IP-lookup latency, and is omitted when that browser value is unavailable.
* `pastedFields` reports paste events for checkout fields explicitly registered with `observeField()`. It contains field names only, never values or pasted content, and is omitted when no name has been recorded.

This is the entire signal set. The library does not collect card data, read input values, fingerprint the device, request geolocation, read or set cookies, or use browser storage.

## Payload shape [#payload-shape]

The object returned when collection succeeds has this complete shape. Optional properties are omitted rather than sent with empty values:

```jsonc
{
  "device": {
    "ip": "203.0.113.42",             // required IPv4
    "userAgent": "Mozilla/5.0 …"      // required, at most 1024 characters
  },
  "page": {                            // optional block
    "referrer": "https://example.com/", // optional, at most 2048 characters
    "timeOnPageMs": 5000               // optional, non-negative integer
  },
  "pastedFields": ["cvc", "exp", "number", "zip"] // optional; any subset
}
```

## Collection sequence [#collection-sequence]

Each call follows this order:

1. The instance captures its cumulative paste-name snapshot when `collect()` is invoked.
2. Collection reads the user agent.
3. It requests ipify, then attempts ifconfig.co if needed, with a five-second timeout for each provider.
4. It reads the referrer and computes time on page immediately before assembling the payload.

Because page signals are read after public-IP resolution, `timeOnPageMs` includes IP-lookup latency. A paste event that occurs after the snapshot is absent from that in-flight result but can appear in a later collection on the same instance.

## Required fields [#required-fields]

When `deviceData` is present, `device`, `device.ip`, and `device.userAgent` are required. The IP must be a valid IPv4 address, and the user agent must fit within the 1024-character limit.

The IP represents the cardholder browser's public network vantage point. The gateway does not replace a missing value with an address derived from request headers.

## Optional fields [#optional-fields]

The `page` block is optional. Within it, `referrer` and `timeOnPageMs` are each optional; if neither value is available, the complete block is omitted.

`pastedFields` is optional. Names are deduplicated and sorted lexicographically in the snapshot when `collect()` is invoked. Recorded names are cumulative and retained for the life of that `HighWire` instance. `unobserve()` stops future events but does not clear names already recorded. A new instance starts with no recorded paste names.

## When collection returns null [#when-collection-returns-null]

Handled provider or required-context failures resolve to `null`. This includes invalid responses, timeouts, network failures, blocked requests, an unavailable user agent, or failure to produce a valid IPv4 address. The code attempts ifconfig.co after ipify, but the fallback provider's response can still be unavailable because of provider-side CORS; changing merchant CSP cannot repair provider CORS.

A missing or broken platform primitive, such as `AbortController`, can reject the collection promise instead of resolving to `null`. Payment examples catch either unavailable path and continue without `deviceData`. Do not send `null`, an empty object, or a hand-built substitute.

## Gateway responses [#gateway-responses]

Forward the payload unchanged and handle the payment response according to the
[Gateway API reference](/docs/gateway-payments/payments/authAndCapture_v1).
A failed collection should omit `deviceData`; do not substitute an empty or hand-built object.
A payment decline is separate from a browser collection failure. Do not remove or alter valid
context to bypass a decline.

## Forward the payload unchanged [#forward-the-payload-unchanged]

Send the exact object returned by `hw.collect()` to your backend, then pass that object through as the top-level `deviceData` property on the HighWire direct-API request. Do not select, rename, supplement, or reconstruct its properties.

Follow the browser-to-backend and backend-to-HighWire handoffs in [Integration](/docs/highwire-js/integration).


# Getting started (/docs/highwire-js/getting-started)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## Before you begin [#before-you-begin]

HighWire currently delivers each release directly as a set of files rather than through a public CDN or npm. You need:

* the release bundle and SHA-384 integrity hash from HighWire;
* a browser-based checkout that already sends its payment request to your backend; and
* either a static-asset path for the UMD build or a frontend build pipeline for the ESM build.

No publishable key is required. Your backend continues to use its existing authentication when it calls HighWire.

## What you received [#what-you-received]

The release handoff contains the complete bundle:

1. **`highwire-v<version>.js`** — the UMD build for a plain `<script>` tag.
2. **`highwire-v<version>.js.map`** — the UMD sourcemap for browser debugging. Do not add it to the page as a script.
3. **`highwire-v<version>.es.js`** — the ESM build for frontend projects that bundle imports.
4. **`highwire-v<version>.es.js.map`** — the ESM sourcemap for browser debugging.
5. **`highwire-v<version>.es.d.ts`** — the TypeScript declarations for the ESM build. Keep it beside the `.es.js` file so TypeScript can resolve it. JavaScript-only projects can ignore it.
6. **`INTEGRITY.txt`** — the SHA-384 SRI value for the exact UMD bytes in the handoff.
7. **`docs/public/`** — this complete documentation set, starting at [`docs/public/index.md`](/docs/highwire-js).

Use either UMD or ESM on the checkout page. The handoff always includes both builds and all supporting files so the delivered release remains complete and auditable.

## Choose a build [#choose-a-build]

Use the **UMD build**, `highwire-v<version>.js`, when the checkout loads the library with a plain `<script>` tag. Self-host it on your origin and pair it with the SRI value from `INTEGRITY.txt`.

Use the **ESM build**, `highwire-v<version>.es.js`, when the frontend bundles JavaScript imports. Keep `highwire-v<version>.es.d.ts` beside it in TypeScript projects. Bundled imports do not use the separately delivered SRI value.

Choose one runtime build for the page. Keep the matching sourcemap available for debugging and retain the complete handoff for release auditability.

## Self-host the UMD build [#self-host-the-umd-build]

Copy the delivered UMD file into the static assets served by your checkout, then reference that exact file and hash:

```html
<script
  src="/assets/highwire-v<version>.js"
  integrity="sha384-REPLACE_WITH_THE_HASH_HIGHWIRE_SENT_YOU"
  crossorigin="anonymous"
></script>
```

Do not minify or otherwise transform the delivered file after the hash is generated. The browser rejects it if the bytes and SRI hash do not match. The UMD build registers the `HighWire` factory on `window`.

## Import the ESM build [#import-the-esm-build]

Vendor the ESM file in your frontend source tree and import the factory from that local path:

```ts
import { HighWire } from './vendor/highwire/highwire-v<version>.es.js';
```

Keep the matching TypeScript declaration beside the JavaScript file when your project uses TypeScript. Bundled imports do not use the separately delivered SRI hash.

## Create an instance [#create-an-instance]

Create one instance in browser code after loading or importing the factory:

```js
const hw = HighWire();
```

Importing the ESM build and creating an instance do not access DOM APIs, collect context, or make network requests, but call `collect()` only in the cardholder's browser. Loading the UMD build registers `window.HighWire`. A modern Node.js runtime can supply valid-looking server values instead of failing, so server-side collection would attach the wrong context.

## Collect for the first time [#collect-for-the-first-time]

Call `collect()` from your existing browser-side submit handler and await the result:

```js
async function collectDeviceDataOrNull() {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

async function submitCheckout(existingPaymentRequest) {
  const deviceData = await collectDeviceDataOrNull();

  return fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...existingPaymentRequest,
      ...(deviceData && { deviceData }),
    }),
  });
}
```

Normal provider or required-context failures resolve to `null`. A missing or broken platform primitive can instead reject the promise. The helper treats either outcome as unavailable context, so the checkout can continue without `deviceData`. Do not construct a substitute object.

## Continue the integration [#continue-the-integration]

Next, follow [Integration](/docs/highwire-js/integration) to observe checkout fields, collect during submission, send `deviceData` to your backend, and forward it to HighWire.


# highwire.js (/docs/highwire-js)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## What highwire.js does [#what-highwirejs-does]

`highwire.js` is a small browser library for merchants that accept payments through HighWire's direct API. It collects a fixed set of device and page context signals, plus paste events for checkout fields that you choose to observe, and returns that context as `deviceData`.

The library is a context collector. Your checkout remains responsible for its existing payment form and payment request.

## Who this is for [#who-this-is-for]

Use `highwire.js` when your checkout runs in the cardholder's browser, sends payment details to your own backend, and your backend submits the payment to HighWire.

It is designed to add browser context to that existing direct-API flow without changing where card data is collected or how your backend authenticates to HighWire.

## How it fits into a payment [#how-it-fits-into-a-payment]

The integration has three observable stages:

1. **Browser collection.** Your checkout loads `highwire.js`, creates an instance, optionally observes supported checkout fields for paste events, and calls `collect()` when the cardholder submits the form.
2. **Merchant-backend forwarding.** Your checkout sends the returned `deviceData` to your own backend with its existing payment request. Your backend passes that object through unchanged. If collection is unavailable, omit the field.
3. **HighWire consumption.** Your backend submits the payment to HighWire's direct API. HighWire consumes `deviceData` server-side as additional payment context.

The browser library does not submit the payment to HighWire on its own.

## What it does not do [#what-it-does-not-do]

`highwire.js` does not collect card data or read any checkout input value. Field observation records only that a paste event occurred for a field you explicitly selected.

It also does not fingerprint the device, request geolocation, set cookies, or use browser storage. It is browser-only and does not replace your merchant backend.

## Next steps [#next-steps]

* [Get started](/docs/highwire-js/getting-started) with the delivered build and make a first collection.
* Follow the complete [integration flow](/docs/highwire-js/integration) from checkout submission through your backend.
* Review the collected context and forwarding rules in [Device data](/docs/highwire-js/device-data).
* Check exact signatures, types, and returns in the [API reference](/docs/highwire-js/api-reference).
* Plan CSP and data handling with [Security and privacy](/docs/highwire-js/security-and-privacy).
* Diagnose loading, collection, observation, and gateway responses in [Troubleshooting](/docs/highwire-js/troubleshooting).

## Guides [#guides]

* Use [Plain HTML and JavaScript](/docs/highwire-js/guides/plain-html) for a complete UMD script-tag and form-submit example.
* Use [React and Next.js](/docs/highwire-js/guides/react-nextjs) for client-component lifecycle, field observation, and cleanup.


# Integration (/docs/highwire-js/integration)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## Integration flow [#integration-flow]

A complete integration keeps the existing payment path and adds `deviceData` at each handoff:

1. The checkout initializes `highwire.js` and opts supported fields into paste-event observation.
2. On form submission, the checkout awaits collection and sends the result to the merchant backend with the existing payment fields.
3. The merchant backend forwards the collected object unchanged as `deviceData` on its HighWire direct-API request.

The browser and backend examples below show only the `highwire.js` additions to an already-valid merchant payment request. Your existing payment fields and route plumbing remain governed by your HighWire gateway integration.

## Initialize highwire.js [#initialize-highwirejs]

For a self-hosted UMD build, load the script and create an instance in browser code:

```html
<script
  src="/assets/highwire-v<version>.js"
  integrity="sha384-REPLACE_WITH_THE_HASH_HIGHWIRE_SENT_YOU"
  crossorigin="anonymous"
  defer
></script>
<script src="/assets/checkout-highwire.js" defer></script>
```

Create the instance in the same-origin `/assets/checkout-highwire.js` companion file used by the checkout:

```js
const hw = HighWire();
```

Keeping the initializer in an external file allows a strict `script-src 'self'` policy without `unsafe-inline`. With the ESM build, import `HighWire` from your vendored file and create the same instance in the checkout's client-side module. See [Getting started](/docs/highwire-js/getting-started) for build selection and placement.

## Observe checkout fields [#observe-checkout-fields]

Call `observeField()` after the checkout inputs exist in the DOM. Opt in only the fields your form has:

```js
const observedFields = [
  ['#card-number', 'number'],
  ['#card-cvc', 'cvc'],
  ['#card-exp', 'exp'],
  ['#billing-zip', 'zip'],
];
for (const [selector, fieldName] of observedFields) {
  const element = document.querySelector(selector);
  if (element) hw.observeField(element, fieldName);
}
```

Observation records paste events, not field contents. The library never reads `element.value`. See the [API reference](/docs/highwire-js/api-reference) for the supported calls and field names.

## Collect on submission [#collect-on-submission]

Pass the payment request your checkout already produces into the submit handler. Await collection and add `deviceData` only when collection succeeds:

```js
async function collectDeviceDataOrNull() {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

async function submitCheckout(existingPaymentRequest) {
  const deviceData = await collectDeviceDataOrNull();

  const response = await fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...existingPaymentRequest,
      ...(deviceData ? { deviceData } : {}),
    }),
  });

  if (!response.ok) {
    throw new Error('Payment request failed');
  }

  return response;
}
```

`existingPaymentRequest` represents the complete request your checkout already sends to your backend. `highwire.js` supplies only `deviceData`; it does not read, construct, validate, or submit the merchant-owned payment fields.

Normal provider or required-context failures resolve to `null`. A missing or broken platform primitive can instead reject the promise. The helper treats either outcome as unavailable context, so the checkout can continue without `deviceData`. Do not construct a substitute object.

## Send deviceData to your backend [#send-devicedata-to-your-backend]

Send the collected object to your own checkout endpoint as a top-level `deviceData` field alongside the payment request you already produce. Preserve it as returned rather than selecting, renaming, or supplementing its properties. For details about the collected object, see [Device data](/docs/highwire-js/device-data).

The conditional spread in the browser example omits the field when collection returns `null`:

```js
body: JSON.stringify({
  ...existingPaymentRequest,
  ...(deviceData ? { deviceData } : {}),
})
```

## Forward deviceData to HighWire [#forward-devicedata-to-highwire]

In your existing backend checkout handler, pass the object through unchanged on the direct-API request:

```ts
const paymentAttempt = await loadExistingPaymentAttempt(request);

const gatewayResponse = await fetch(
  'https://api-payments.highwirepayments.com/v1/payments/auth-and-capture',
  {
    method: 'POST',
    headers: {
      Authorization: `Basic ${merchantBasicAuth}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': paymentAttempt.idempotencyKey,
    },
    body: paymentAttempt.serializedGatewayRequestBody,
  },
);
```

`loadExistingPaymentAttempt(request)` represents the merchant's existing server-side attempt store. Before the first gateway call, conditionally add the successful `deviceData` object to `existingPaymentRequest`, serialize that complete gateway request once, and persist both the exact JSON string and one idempotency key. For every retry of that logical attempt, reuse the same key and the same request body by sending `paymentAttempt.serializedGatewayRequestBody` with `paymentAttempt.idempotencyKey`; do not recollect browser context or rebuild or reserialize the body. A new logical payment attempt receives a new key and body record. `merchantBasicAuth`, payment-attempt state, and gateway credentials must remain server-side.

## Handle unavailable device data [#handle-unavailable-device-data]

Normal provider or required-context failures resolve to `null`. A missing or broken platform primitive can instead reject the promise. The helper treats either outcome as unavailable context, so the checkout can continue without `deviceData`. Do not construct a substitute object.

If collection is unexpectedly unavailable, check script loading, SRI, CSP, and IP-provider access in [Troubleshooting](/docs/highwire-js/troubleshooting).

## Clean up in single-page applications [#clean-up-in-single-page-applications]

`observeField()` returns a cleanup function. Keep each function and call it when the checkout form unmounts so that old elements no longer have listeners:

```js
const stopObserving = [
  hw.observeField(cardNumberInput, 'number'),
  hw.observeField(cardCvcInput, 'cvc'),
  hw.observeField(cardExpirationInput, 'exp'),
  hw.observeField(billingZipInput, 'zip'),
];

function unmountCheckout() {
  stopObserving.forEach((stop) => stop());
}
```

Cleanup prevents future events from the detached elements. It does not clear names already captured by that instance; create a new `HighWire()` instance when a new checkout attempt requires a fresh observation history.

Initialize and observe again when a new form instance mounts. See the [React and Next.js guide](/docs/highwire-js/guides/react-nextjs) for component lifecycle examples.


# Security and privacy (/docs/highwire-js/security-and-privacy)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

`highwire.js` collects a fixed set of browser and checkout context signals in the cardholder's browser. Review [Device data](/docs/highwire-js/device-data) for the complete collected-field list, payload shape, and field limits.

## PCI scope [#pci-scope]

`highwire.js` is a context collector, not a card-data collector. It does not read PAN, CVV, card-field values, or pasted content. Card data remains in the merchant's existing checkout and payment path.

Those product facts do not determine a merchant's PCI DSS scope or SAQ eligibility. Confirm the complete integration with your acquirer, payment brands, QSA, or other compliance-accepting entity. PCI SSC [FAQ 1588](https://www.pcisecuritystandards.org/faqs/1588/) describes script-related SAQ A eligibility and directs merchants to the entity accepting their compliance; [FAQ 1092](https://www.pcisecuritystandards.org/faqs/1092/) explains that outsourcing payment processing does not remove merchant responsibilities.

## Data highwire.js does not collect [#data-highwirejs-does-not-collect]

The complete set of collected signals is documented in [Device data](/docs/highwire-js/device-data#collected-signals). Outside that set, the library does not collect card data, input values, pasted content, cookie data, device fingerprints, or geolocation.

It does not use canvas, WebGL, or audio probes; hash a visitor identifier; depend on a fingerprinting library; or trigger the browser's geolocation permission prompt.

The optional `document.referrer` signal can contain URL data from the referring page. Review URL and referrer-policy design so sensitive values are not placed in URLs.

## Input handling [#input-handling]

Field observation is opt-in. For each registered element, the library listens only for a `paste` event and records the supported field name supplied by the merchant. It never reads `element.value` or clipboard contents.

Recorded field names remain in memory for the life of the `HighWire` instance. They are not written to cookies or browser storage.

See [`hw.observeField(element, fieldName)`](/docs/highwire-js/api-reference#hwobservefieldelement-fieldname) for the accepted field names and listener-cleanup behavior.

## Browser storage [#browser-storage]

The library does not read or set cookies, write to `localStorage` or `sessionStorage`, or persist a visitor identifier in the browser.

## Network requests [#network-requests]

The only requests made by the library are unauthenticated public-IP lookups during `collect()`. It requests `GET https://api.ipify.org?format=json`, then attempts ifconfig.co if the first provider fails with `GET https://ifconfig.co/json`.

Each provider has a five-second timeout, so collection makes at most one request to each provider. The library loads no third-party JavaScript at runtime and has no runtime dependencies.

Both requests are unauthenticated and use omitted credentials. Merchant CSP permission controls whether the browser may attempt each origin; provider-side CORS independently controls whether JavaScript can read the response.

`highwire.js` itself calls only the two public-IP providers and does not call HighWire. After collection, merchant-owned browser code sends the payload to the merchant backend, which forwards it through the existing payment path. See [Integration](/docs/highwire-js/integration) for those handoffs.

## Data handling [#data-handling]

The optional referrer and device context may contain personal information. Review your privacy
notice and data-handling obligations for the checkout. Contact HighWire for the data-processing
and retention terms that apply to your account.

## Content Security Policy [#content-security-policy]

The documented UMD setup loads both the self-hosted library and the same-origin companion integration file as external scripts. The CSP baseline required by these examples is:

```text
script-src 'self';
connect-src 'self' https://api.ipify.org https://ifconfig.co;
```

`'self'` in `script-src` permits the two same-origin script files without allowing inline code. `'self'` in `connect-src` preserves the same-origin merchant checkout endpoint used by the examples. If the checkout calls another API origin, preserve every additional merchant API origin when merging these values into the existing policy.

The two provider origins let the browser attempt public-IP lookups. CSP permission does not guarantee success: provider-side CORS can still keep a response unreadable. Do not add `unsafe-inline`; keep integration code in the companion file or use a separately reviewed nonce/hash policy.

## Subresource Integrity [#subresource-integrity]

For a script-tag integration, use the SHA-384 SRI value delivered with that exact UMD file and retain `crossorigin="anonymous"`. The hash covers the exact bytes HighWire shipped. If the file is minified, transformed, or otherwise rewritten, the browser reports an integrity mismatch and refuses to execute it.

Bundled ESM imports do not use the separately delivered SRI hash. See [Choose a build](/docs/highwire-js/getting-started#choose-a-build) and [Self-host the UMD build](/docs/highwire-js/getting-started#self-host-the-umd-build) for the canonical setup.

## Browser support [#browser-support]

The distributed JavaScript targets ES2019 syntax. The collection path requires `globalThis`, `Promise`, `Set`, `fetch`, `AbortController`, and `navigator.userAgent`; paste observation also requires DOM event targets. Referrer and time-on-page are optional and may be omitted when `document.referrer` or `performance.timeOrigin` is unavailable.

This repository does not have a real-browser compatibility matrix, so the documentation does not promise exact browser-version floors. Validate the supported product browser matrix in real checkout browsers, including CSP, CORS, SRI, and the required APIs above.

Importing ESM and constructing the factory do not access DOM APIs, collect context, or make network requests, but `collect()` must run in the cardholder's browser. Loading UMD has the expected effect of registering `window.HighWire`. Review [browser-only collection](/docs/highwire-js/api-reference#browser-only-collection) and [React and Next.js](/docs/highwire-js/guides/react-nextjs).


# Troubleshooting (/docs/highwire-js/troubleshooting)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

Use the browser console and Network panel together with the merchant backend's HighWire response to identify where an integration stopped. The sections below separate browser loading, collection, observation, runtime, and gateway-validation failures.

## `HighWire is not a function` [#highwire-is-not-a-function]

**Likely cause.** In a script-tag integration, the checkout called `HighWire()` before the external UMD script loaded, executed, and registered `window.HighWire`. An incorrect self-hosted asset path or a `script-src` block can leave the factory unregistered for the same reason.

**Check.** In the browser Network panel, confirm that the configured script URL returns the expected JavaScript file. Then check the console for a Content Security Policy error and confirm that initialization runs after the script has loaded.

**Recovery.** Correct the self-hosted asset path or allow the merchant origin for the script. Initialize `HighWire()` only after the external script has loaded and executed, such as from its `load` event or from code that runs later. If the checkout uses the ESM build, import the factory instead of expecting a `window.HighWire` global. Follow the canonical [UMD and ESM setup](/docs/highwire-js/getting-started#choose-a-build) and review the [Content Security Policy](/docs/highwire-js/security-and-privacy#content-security-policy) requirements.

## `The browser reports an integrity mismatch` [#the-browser-reports-an-integrity-mismatch]

**Likely cause.** The `integrity` value does not match the downloaded UMD bytes. The hash may have been copied incorrectly, paired with another release, or invalidated by transforming the delivered file.

**Check.** Confirm that the attribute contains the complete delivered value, including the `sha384-` prefix and any trailing `=` padding. Verify that the script URL serves the matching, unmodified release file.

**Recovery.** Replace both the file and hash with the matching pair delivered by HighWire, and do not minify, transform, or otherwise rewrite the file after the hash is generated. Review [Subresource Integrity](/docs/highwire-js/security-and-privacy#subresource-integrity) and the canonical [self-hosting setup](/docs/highwire-js/getting-started#self-host-the-umd-build).

## `Collection returns null` [#collection-returns-null]

**Likely cause.** Collection could not produce a required browser value. Common causes include both IP providers being blocked, unavailable, timed out, or unable to return a valid IPv4 address; an offline or IPv6-only client; or an unavailable browser user agent.

**Check.** In the Network panel, inspect the requests to `api.ipify.org` and, after a failure, `ifconfig.co`. Check the console for `connect-src` violations and confirm that `navigator.userAgent` is available.

**Recovery.** Preserve the merchant endpoint and both attempted provider origins in `connect-src`. CSP permission does not override provider-side CORS. Whether collection resolves to `null` or rejects unexpectedly, omit `deviceData` and continue the existing payment request.

## `pastedFields` is missing or omitted [#pastedfields-is-missing-or-omitted]

**Likely cause.** No supported name was present in the instance's snapshot. Registration may have run before the element existed, used an unsupported case-sensitive name, or no paste may have occurred before `collect()` was invoked.

**Check.** Confirm that registration runs after the element exists and before the relevant paste. Names are captured at invocation, deduplicated, sorted lexicographically, and retained for the life of the instance.

**Recovery.** Register each relevant field after mount. `unobserve()` stops future events but does not erase a name already recorded; create a new `HighWire()` instance when the checkout needs a fresh paste-history boundary.

## `Collection ran outside the browser` [#collection-ran-outside-the-browser]

**Likely cause.** Server-side code called `collect()`. Modern Node.js may supply `navigator`, `performance`, and `fetch`, so the result can contain valid-looking server IP/user-agent/page context instead of throwing.

**Check.** Trace the call site, not merely the import. Importing ESM and constructing the factory do not access DOM APIs, collect context, or make network requests; collection is the browser-only boundary. Loading UMD has the expected global-registration effect.

**Recovery.** Move `collect()` into a cardholder-browser event handler. In Next.js, use a client component and never collect from a server component, API route, or `getServerSideProps`. See [Browser-only collection](/docs/highwire-js/api-reference#browser-only-collection) and [React and Next.js](/docs/highwire-js/guides/react-nextjs).

## `HighWire rejects deviceData` [#highwire-rejects-devicedata]

**Likely cause.** A present `deviceData` object was modified, reconstructed, or malformed before it reached the HighWire gateway. Unlike an absent payload, a payload that fails validation causes the payment request to return HTTP 400.

**Check.** Inspect the object at the merchant-backend handoff and compare it with the value returned by `collect()`. Confirm it was forwarded unchanged, then review the canonical required fields, optional fields, and accepted values in [Device data](/docs/highwire-js/device-data).

**Recovery.** Forward the successful collection result as the top-level `deviceData` property without selecting, renaming, supplementing, or rebuilding its properties. When collection returns `null`, omit the property entirely. Follow [Forward the payload unchanged](/docs/highwire-js/device-data#forward-the-payload-unchanged) and the [integration handoff](/docs/highwire-js/integration#forward-devicedata-to-highwire).

## HighWire returns a payment decline [#highwire-returns-a-payment-decline]

Treat the gateway's declined payment response as a payment outcome. Check the documented status
and response fields in the [Gateway API reference](/docs/gateway-payments/payments/authAndCapture_v1).
Do not strip or alter valid `deviceData` to bypass a decline. If an expected-valid payment is
unexpectedly declined, contact your HighWire representative with safe diagnostic details.


# API Reference (/docs/payment-sdk/api-reference)



This reference covers the checkout API in **Payment SDK 0.7.0**. `HighWire` is the factory,
`hw` is the initialized instance, and `checkout` is the object returned by `hw.mount()`.

For installation, server binding, and a complete checkout, use the
[Integration guide](/docs/payment-sdk/integration),
[Plain HTML guide](/docs/payment-sdk/guides/plain-html), or
[React / Next.js guide](/docs/payment-sdk/guides/react-nextjs). The separate
`@highwirepayments/payment-sdk/collect` entry, which returns the device-session key for Direct API
charges, is covered in [Device screening](/docs/payment-sdk/device-screening). Coming from 0.6.0?
See [Upgrading from 0.6.0](/docs/payment-sdk/integration#upgrading-from-060).

## Methods at a glance [#methods-at-a-glance]

| Object   | Method                                                  | Returns                            |
| -------- | ------------------------------------------------------- | ---------------------------------- |
| Factory  | [`HighWire.init(config)`](#highwireinit)                | `Promise<HighWireInstance>`        |
| Instance | [`hw.mount(options)`](#hwmount)                         | `Promise<Checkout>`                |
| Instance | [`hw.chargeSavedMethod(context)`](#hwchargesavedmethod) | `Promise<SavedMethodChargeResult>` |
| Checkout | [`checkout.submit(payer?)`](#checkoutsubmit)            | `Promise<PaymentResult>`           |
| Checkout | [`checkout.recheck()`](#checkoutrecheck)                | `Promise<PaymentResult>`           |
| Checkout | [`checkout.unmount()`](#checkoutunmount)                | `void`                             |

## Runtime exports [#runtime-exports]

After loading the [pinned CDN build](/docs/payment-sdk/integration#1-install-and-initialize),
read the exports from `window.HighWire`. The global is a namespace, rather than the factory itself.

```js
const HighWire = window.HighWire.default;
const { HighWireError, SDK_VERSION } = window.HighWire;
```

`default` and the namespace's named `HighWire` export refer to the same factory.
`SDK_VERSION` is `'0.7.0'`. The npm package supplies the matching declarations; use
[type-only imports with the CDN runtime](/docs/payment-sdk/integration#1-install-and-initialize).
Examples below assume this namespace has loaded and run in an async browser function or module.

## HighWire.init [#highwireinit]

```text
HighWire.init(config: HighWireConfig): Promise<HighWireInstance>
```

Initialize once per checkout page. The returned Promise resolves once the device session
settles, or after at most 2 seconds while it keeps loading. A screening failure never rejects it.

### Parameters [#parameters]

`config` is required. It accepts these `HighWireConfig` fields:

| Field            | Type     | Required | Default   | Description                                                                                |
| ---------------- | -------- | -------- | --------- | ------------------------------------------------------------------------------------------ |
| `publishableKey` | `string` | Yes      | None      | Issued `hw_pk_test_…` or `hw_pk_live_…` key, with 32 URL-safe characters after the prefix. |
| `apiBase`        | `string` | Yes      | None      | HighWire gateway origin. HTTPS is required except for localhost development.               |
| `fieldsOrigin`   | `string` | No       | `apiBase` | Origin serving the card field and drop-in. Set only when supplied by HighWire.             |

URLs are normalized to their origins. Use the matching key and gateway from onboarding.
Register the checkout's origins; see [Security & server verification](/docs/payment-sdk/authentication).

### Returns and errors [#returns-and-errors]

Returns a frozen `HighWireInstance` with readonly `mount` and `chargeSavedMethod` methods.
It has no `riskSessionKey()` method; see [Device screening](/docs/payment-sdk/device-screening).
Invalid fields in a provided configuration object throw `HighWireError` with `HW_UNSUPPORTED`
**synchronously**, before a Promise is returned. Omitting the required object
or passing `null` instead causes a plain synchronous `TypeError`. Put the call itself
inside `try` when handling errors.

## hw\.mount [#hwmount]

```text
hw.mount(options: MountOptions): Promise<Checkout>
```

Create or reuse the order's payment and draw the configured checkout at the payment step.
One checkout may be mounted at a time per instance.
The HighWire session chooses the layout; see [Drop-in & wallets](/docs/payment-sdk/drop-in-and-wallets).

### Parameters [#parameters-1]

`options` is required. Every `MountOptions` field is listed below:

| Field                           | Type                              | Required | Default        | Description                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------- | --------------------------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `container`                     | `string \| Element`               | Yes      | None           | CSS selector resolving to an existing element, or the DOM element itself.                                                                                                                                                                                                                                                                                          |
| `amount`                        | `number`                          | Yes      | None           | Positive integer cents. `2999` represents USD 29.99.                                                                                                                                                                                                                                                                                                               |
| `currency`                      | `string`                          | Yes      | None           | Only USD is supported; use `'usd'`.                                                                                                                                                                                                                                                                                                                                |
| `savePaymentMethodForFutureUse` | `boolean`                         | No       | `false`        | Show the shopper an unticked save-card box, in the card field or under a drop-in form that uses your Pay button. Only the shopper's tick saves the card, under the email `submit()` sends. A drop-in form with its own button, Apple Pay, and Google Pay save no card.                                                                                             |
| `billingAddressForm`            | `'none' \| 'full'`                | No       | `'none'`       | Who collects the billing address for a drop-in form that uses your Pay button. `'none'`: your page passes the full address to `submit()`. `'full'`: the form shows its own billing fields; see [billing fields in the form](/docs/payment-sdk/drop-in-and-wallets#collect-the-billing-address-in-the-form). Other checkouts reject `'full'` with `HW_UNSUPPORTED`. |
| `onReady`                       | `() => void`                      | No       | Not registered | Card field or drop-in ready.                                                                                                                                                                                                                                                                                                                                       |
| `onAvailabilityChange`          | `(available: boolean) => void`    | No       | Not registered | A drop-in form that uses your Pay button can, or can no longer, take a submission.                                                                                                                                                                                                                                                                                 |
| `onChange`                      | `(card: CardState) => void`       | No       | Not registered | Card completeness and validation state changed.                                                                                                                                                                                                                                                                                                                    |
| `onProcessing`                  | `(method: PaymentMethod) => void` | No       | Not registered | Payment confirmation began.                                                                                                                                                                                                                                                                                                                                        |
| `onResult`                      | `(result: PaymentResult) => void` | No       | Not registered | Result from any payment method.                                                                                                                                                                                                                                                                                                                                    |
| `onError`                       | `(error: HighWireError) => void`  | No       | Not registered | Operation or form-loading failure.                                                                                                                                                                                                                                                                                                                                 |

See [Callbacks](#callbacks) for their complete payloads. Checkout idempotency keys are generated
by the SDK; `mount()` accepts no shopper email, merchant key, merchant order ID, or individual
payment-method selection. Pass the email to [`submit()`](#checkoutsubmit). In plain JavaScript a
`customerEmail` passed to `mount()` is ignored; TypeScript rejects it.

### Returns and errors [#returns-and-errors-1]

Resolves with a `Checkout` once its parts are mounted. Form readiness and loading errors are
reported separately through callbacks, so a resolved mount does not guarantee a usable card field.

| Checkout member  | Type                                               | Meaning                                                                                                                                                    |
| ---------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentId`      | `readonly string`                                  | Payment charged by this checkout. Bind it to the order on your server before enabling any payment controls.                                                |
| `requiresSubmit` | `readonly boolean`                                 | `true` for layouts with a card field and for a drop-in form that uses your Pay button; show your Pay button. When `false`, the drop-in has its own button. |
| `submit`         | `(payer?: PayerDetails) => Promise<PaymentResult>` | Submit the card field or the drop-in form; see [`checkout.submit()`](#checkoutsubmit).                                                                     |
| `recheck`        | `() => Promise<PaymentResult>`                     | Check an unresolved drop-in payment again; see [`checkout.recheck()`](#checkoutrecheck).                                                                   |
| `unmount`        | `() => void`                                       | Remove checkout UI; see [`checkout.unmount()`](#checkoutunmount).                                                                                          |

Rejections include invalid options (`HW_UNSUPPORTED`), a refused payment
(`HW_DECLINED`), authorization/transport failures, and attempts to mount while another payment
is unresolved. The same rejection reaches `onError` when provided; show it once.
See the [error codes](#error-codes).

`billingAddressForm: 'full'` on any other checkout rejects `HW_UNSUPPORTED` after the payment
is created; mount again with `'none'` to reuse that payment.

Keep the container non-interactive until your backend has
[verified and bound the payment](/docs/payment-sdk/authentication#bind-and-verify-orders-on-your-server).
On the same instance, an open payment is reused for the same amount, currency, and
`savePaymentMethodForFutureUse`. The email plays no part, so two orders with the same amount reuse
one payment, even after `unmount()`. For another order with the same options, use a fresh instance
once your backend authorizes it. Another amount creates another payment; another save
option creates one once the last payment can no longer capture. While an unresolved payment of a
drop-in form that uses your Pay button may still capture, another amount or save option rejects
`HW_TOKENIZE_IN_PROGRESS`. See
[normalization and remounting](/docs/payment-sdk/integration#4-order-changes-and-cleanup).

## checkout.submit [#checkoutsubmit]

```text
checkout.submit(payer?: PayerDetails): Promise<PaymentResult>
```

Pay with the mounted card field, or with a drop-in form that uses your Pay button. Call only when
`requiresSubmit` is `true`, the form can take a payment (`onChange` reports `complete`, or
`onAvailabilityChange` reports `true`), and no operation is running. Wallets and drop-in forms
with their own button use their own controls.

### Parameters [#parameters-2]

`payer` carries the shopper's email, so it is required for a card payment. A drop-in form that
uses your Pay button can omit it only after one of its attempts on this payment was declined, and
only when it collects the billing address itself. Its `PayerDetails` fields are:

| Field            | Type             | Required    | Default | Description                                                                                                                                                                                                                                                                                                                                                           |
| ---------------- | ---------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerEmail`  | `string`         | Conditional | None    | The shopper's email; a saved card is filed under it. Required for a card payment: missing or malformed, `submit()` rejects `HW_INCOMPLETE` before anything is sent. A drop-in form that uses your Pay button falls back to the email of its last declined attempt on this payment; see [email on a drop-in form](#email-on-a-drop-in-form-that-uses-your-pay-button). |
| `customerPhone`  | `string`         | No          | Omitted | Shopper's phone number. Sent with a card payment only.                                                                                                                                                                                                                                                                                                                |
| `billingAddress` | `BillingAddress` | Conditional | Omitted | Billing details, using the fields below. A drop-in form that uses your Pay button needs the full address, with at least `name`, `country`, and `postalCode`, unless mounted with `billingAddressForm: 'full'`, which rejects any `billingAddress`.                                                                                                                    |

Every `BillingAddress` field is an optional string, omitted by default:

| Field        | Description                                                                     |
| ------------ | ------------------------------------------------------------------------------- |
| `name`       | Billing recipient. Separate from the cardholder name entered in the card field. |
| `country`    | Two-letter country code, such as `'US'`.                                        |
| `line1`      | Street address.                                                                 |
| `line2`      | Apartment, suite, or unit.                                                      |
| `city`       | City.                                                                           |
| `state`      | State or region.                                                                |
| `postalCode` | Postal or ZIP code. Pass a string so leading zeros are kept.                    |

Email, phone, and billing strings are trimmed; blank values are omitted. Amount, currency, and
the save option are fixed at mount, and the shopper's tick is read at submit; submit takes no card
values or merchant idempotency key.

A card payment sends `line1`, `city`, `state`, and `postalCode`, and ignores `name`, `country`,
and `line2`. A drop-in form that uses your Pay button sends every address field, uppercases
`country`, and copies the details when called, so later changes to the object do not affect that
attempt. It rejects a non-string field, a malformed email, or a country that is not two letters
with recovery `correct`, before anything is sent. The SDK checks `name` and `country`; the form
validates the rest of the address for the selected country and refuses one without a postal code.

#### Email on a drop-in form that uses your Pay button [#email-on-a-drop-in-form-that-uses-your-pay-button]

HighWire screens and binds the email before the form's processor can charge the card:

* An omitted email falls back to the email of the payment's last declined attempt. Before any
  decline, `submit()` rejects `HW_INCOMPLETE`.
* A corrected email is screened again. One payment takes up to 3 different emails; past that,
  `submit()` rejects `HW_INCOMPLETE` with recovery `correct`, as it does for an email HighWire
  refuses. Submit again with the last email the payment accepted.
* A screening denial rejects `HW_DECLINED` with recovery `recheck`.
* A changed email or save box while the payment is live rejects `HW_TOKENIZE_IN_PROGRESS` with
  recovery `recheck`.

After either `recheck` rejection the payment is kept: a later `submit()` only reads it, and this
checkout takes no other payment.

### Returns and errors [#returns-and-errors-2]

Returns the same [`PaymentResult`](#paymentresult) delivered to `onResult`. A decline resolves;
operational failures reject. Handle the result once. Required 3-D Secure verification opens
within this call; keep checkout mounted during it.

A drop-in form that uses your Pay button resolves `succeeded`, `pending`, or `declined`. A card
decline resolves `declined` and calls `onResult`: with `retryable: true` the shopper can use
another card and submit again; with `retryable: false` the checkout has ended. When the form
refuses the payment details, billing address, or email, `submit()` rejects `HW_INCOMPLETE` with
recovery `correct`, and the shopper can correct them and submit again. Its errors carry
[`recovery`](#checkoutrecovery).

```js
const result = await checkout.submit({
  customerEmail: 'shopper@example.com',
  customerPhone: '+14155550123',
  billingAddress: {
    name: 'Alex Shopper',
    country: 'US',
    line1: '123 Example Street',
    line2: 'Suite 400',
    city: 'San Francisco',
    state: 'CA',
    postalCode: '94105',
  },
});
```

For a lost answer, retry the same checkout's submit to retain the attempt. A card retry resends the
first attempt's email and billing details; edits the shopper made in between are not sent. On a drop-in form
that uses your Pay button, a submit after an unknown outcome, a screening denial, or a refused
email or save-box change only checks the original payment, for up to 3 minutes; it never sends it
again. After unmount, a new submit only rejects, so keep a rejection handler
even with `onError`.
See [Errors](#error-codes) and [retry guidance](/docs/payment-sdk/integration#5-errors-and-retries).

## checkout.recheck [#checkoutrecheck]

```text
checkout.recheck(): Promise<PaymentResult>
```

**Parameters:** none. Read the payment of a drop-in form that uses your Pay button once more,
after an error with recovery `recheck` or a `pending` result. It never creates a payment or
submits again, and it works after `unmount()`.

Resolves `succeeded` when the gateway reports the payment succeeded, which ends the checkout,
and `pending` otherwise. A `succeeded` result carries `customerId` and `paymentMethodId` when the
shopper saved the card. It does not call `onResult`: show its result yourself. A `pending`
answer does not permit another payment; check the order on your server first.

Rejections also reach `onError` until `unmount()`, and after it only while a `submit()` is still
running. Otherwise only the returned promise rejects, so keep a `catch` on it:

| Case                          | Rejection                                                                   |
| ----------------------------- | --------------------------------------------------------------------------- |
| `submit()` is still running   | `HW_TOKENIZE_IN_PROGRESS` with recovery `wait`. Call again once it settles. |
| The payment could not be read | The read's code, such as `HW_NETWORK`, with recovery `recheck`.             |
| No submission left to check   | `HW_NOT_READY` with recovery `unavailable`.                                 |
| Any other checkout            | `HW_UNSUPPORTED`.                                                           |

## checkout.unmount [#checkoutunmount]

```text
checkout.unmount(): void
```

**Parameters:** none. &#x2A;*Returns:** `undefined` (`void`). Remove the checkout's slots and
processing state, and restore the container's previous layout attribute. Use during page/component cleanup.

Unmounting does not cancel a charge; an active payment may still call a callback.
An unanswered card confirmation rejects `HW_DESTROYED`. A submission of a drop-in form that uses
your Pay button whose outcome is unknown, or that was rejected with recovery `recheck`, survives
unmount and blocks another order on the instance; follow
[cleanup and recovery](/docs/payment-sdk/integration#4-order-changes-and-cleanup).
This method returns synchronously without a documented `HighWireError` rejection.

## hw\.chargeSavedMethod [#hwchargesavedmethod]

```text
hw.chargeSavedMethod(context: SavedMethodChargeContext): Promise<SavedMethodChargeResult>
```

Charge a card the shopper saved on an earlier checkout. No mounted form is required, and the
call can be made after a page reload using the saved identifier and your stored charge key.

### Parameters [#parameters-3]

`context` is required. Every `SavedMethodChargeContext` field is required and has no default:

| Field             | Type     | Description                                                                                            |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `paymentMethodId` | `string` | Non-empty saved identifier returned by the original card's `PaymentResult`.                            |
| `amount`          | `number` | Positive integer cents, no higher than the payment that saved the card.                                |
| `idempotencyKey`  | `string` | Non-empty stable key for this add-on charge, stored by your backend and reused on retries and reloads. |

This is the only checkout SDK method that accepts a merchant idempotency key.
Reuse is subject to account limits.

### Returns and errors [#returns-and-errors-3]

Returns `SavedMethodChargeResult` with these fields:

| Field             | Type                                     | Availability                                       |
| ----------------- | ---------------------------------------- | -------------------------------------------------- |
| `status`          | `'succeeded' \| 'declined' \| 'pending'` | Always. A decline resolves.                        |
| `paymentId`       | `string`                                 | Always; the new payment, including a declined one. |
| `amount`          | `number`                                 | Always; charged/requested amount in cents.         |
| `paymentMethodId` | `string`                                 | Always; saved method used.                         |

```js
const addOn = await hw.chargeSavedMethod({
  paymentMethodId: 'SAVED_PAYMENT_METHOD_ID',
  amount: 500,
  idempotencyKey: 'STABLE_KEY_STORED_WITH_THIS_ADD_ON',
});
```

A card saved through a drop-in form that uses your Pay button is collected asynchronously:
expect `pending` and settle it from your server. If its issuer asks for 3-D Secure again, the
charge does not capture, because this call has no shopper verification step.

Verify every result on your backend. One saved-method call can run per instance; another
rejects `HW_TOKENIZE_IN_PROGRESS`. Validation can reject `HW_INCOMPLETE` or `HW_UNSUPPORTED`;
changing the amount under an existing key can also reject `HW_UNSUPPORTED`.
Retain the original key for an unknown outcome. See [saved-card errors](#saved-card-reuse)
and [key recovery rules](/docs/payment-sdk/integration#saving-a-card) before starting a new charge.

## Callbacks [#callbacks]

All callbacks are optional properties of `MountOptions`; omitting one installs no handler.
Their return values are ignored. Card values are never part of a callback payload.

| Callback                          | Payload and behavior                                                                                                                                                                                                                                                                                      |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onReady()`                       | No payload. Card field or drop-in ready; card completeness is reported separately. Called once.                                                                                                                                                                                                           |
| `onAvailabilityChange(available)` | `boolean`. Only for a drop-in form that uses your Pay button: `false` while the form loads, `true` once it can take a submission, and `false` again while it reloads and when the checkout ends. Enable Pay only while `true`. It does not say the details are valid, or that another payment is allowed. |
| `onChange(card)`                  | [`CardState`](#cardstate). A reload that clears the card reports empty state.                                                                                                                                                                                                                             |
| `onProcessing(method)`            | [`PaymentMethod`](#paymentmethod). Disable your Pay button while confirming.                                                                                                                                                                                                                              |
| `onResult(result)`                | [`PaymentResult`](#paymentresult) from card, Apple Pay, Google Pay, or drop-in.                                                                                                                                                                                                                           |
| `onError(error)`                  | [`HighWireError`](#highwireerror). Mount/submit/recheck reject with this same error. Form-load errors can be reported without a mount rejection. Errors from a drop-in form that uses your Pay button carry [`recovery`](#checkoutrecovery).                                                              |

Active payments may call callbacks after unmount. An unanswered card confirm reports
`HW_DESTROYED` through `onError` and submit rejection. Show each error once.

### CardState [#cardstate]

| Field      | Type                       | Meaning                                                                      |
| ---------- | -------------------------- | ---------------------------------------------------------------------------- |
| `complete` | `boolean`                  | Card ready to submit. Also check your busy/closed state before enabling Pay. |
| `empty`    | `boolean`                  | Card field is empty.                                                         |
| `brand`    | `string \| null`           | Detected card brand, or `null` when unknown.                                 |
| `error`    | `{ code: string } \| null` | Field validation error, or `null`. No card values.                           |

### PaymentMethod [#paymentmethod]

```text
'card' | 'applePay' | 'googlePay' | 'dropIn'
```

These are the values passed to `onProcessing`, and the container's `data-hw-processing`
attribute while a payment is being confirmed. A drop-in form that uses your Pay button reports
`'dropIn'` when `submit()` sends a new submission, not when it only checks an earlier one.

### PaymentResult [#paymentresult]

| Field             | Type                                     | Availability and meaning                                                                                                                                                                                       |
| ----------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`          | `'succeeded' \| 'declined' \| 'pending'` | Always. See status handling below.                                                                                                                                                                             |
| `paymentId`       | `string`                                 | Always; the same payment as `checkout.paymentId`.                                                                                                                                                              |
| `retryable`       | `boolean`                                | Present on every decline; absent for other statuses.                                                                                                                                                           |
| `paymentMethodId` | `string`                                 | Optional; saved card identifier, only when the shopper ticked the save box and the card was saved. On a drop-in form that uses your Pay button, only on `succeeded`. A card saved before keeps its identifier. |
| `customerId`      | `string`                                 | Optional; always returned together with `paymentMethodId`.                                                                                                                                                     |
| `maskedCard`      | `string`                                 | Optional; first 6 and last 4 digits when available. Never on a result from a drop-in form that uses your Pay button.                                                                                           |

A reopened 3-D Secure challenge can omit `paymentMethodId`, `customerId`, and `maskedCard`.
There is no decline-reason field.

| Status                         | Checkout state and action                                                                                                                                                                                                                                                              |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `succeeded`                    | Checkout ended. Show confirmation; fulfill only after your server verifies the bound payment's captured status, amount, and currency.                                                                                                                                                  |
| `declined`, `retryable: true`  | Checkout remains open; try another card or method on the same payment. Also applies to a drop-in form that uses your Pay button. A card the gateway's velocity checks refuse resolves this way too, without charging; the same card at the same amount is refused again for 5 minutes. |
| `declined`, `retryable: false` | Checkout ended; reconcile before authorizing another payment.                                                                                                                                                                                                                          |
| `pending`                      | Checkout ended but payment unresolved; read it on your server before taking another payment.                                                                                                                                                                                           |

Fulfillment requires [server verification](/docs/payment-sdk/authentication#bind-and-verify-orders-on-your-server).

## HighWireError [#highwireerror]

`HighWireError` extends `Error` and is exported by the CDN namespace. SDK failures expose:

| Property   | Type                                     | Meaning                                                                                                                                                       |
| ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`     | `readonly HighWireErrorCode`             | Machine-readable code listed below. Use for recovery and your own customer-facing copy.                                                                       |
| `recovery` | `readonly CheckoutRecovery \| undefined` | What the shopper can do next. Present on errors from a drop-in form that uses your Pay button, absent otherwise. See [`CheckoutRecovery`](#checkoutrecovery). |
| `message`  | `string`                                 | Diagnostic message inherited from `Error`; avoid displaying it directly to shoppers.                                                                          |
| `name`     | `string`                                 | `'HighWireError'`.                                                                                                                                            |

The exported constructor is
`new HighWireError(code: HighWireErrorCode, message: string, recovery?: CheckoutRecovery)`.
`code` and `message` are required and have no default. SDK failures already provide instances;
integrations normally inspect rather than construct them.

```js
try {
  await checkout.submit({ customerEmail: 'shopper@example.com' });
} catch (error) {
  if (error instanceof HighWireError) console.error(error.code, error.recovery);
}
```

### CheckoutRecovery [#checkoutrecovery]

```text
'correct' | 'retry' | 'wait' | 'recheck' | 'unavailable'
```

Exported as a type. When an error carries `recovery`, act on it before its code:

| Recovery      | Meaning and action                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `correct`     | The payer details, or the card or billing fields in the form, were refused. This includes an email HighWire refuses, and a fourth different email on one payment. Let the shopper correct them, then submit again.                                                                                                                                                                                                                                       |
| `retry`       | The payment was not sent. Allow another `submit()` while `onAvailabilityChange` reports `true`.                                                                                                                                                                                                                                                                                                                                                          |
| `wait`        | Another operation is still running. Keep Pay disabled; its result or error follows. The `data-hw-processing` attribute stays in place.                                                                                                                                                                                                                                                                                                                   |
| `recheck`     | The payment may still capture: its outcome is unknown, HighWire's screening denied the shopper's email (`HW_DECLINED`), or a changed email or save box was refused while it was live (`HW_TOKENIZE_IN_PROGRESS`). Keep Pay disabled and tell the shopper it is being confirmed. Check it on your server, or call [`checkout.recheck()`](#checkoutrecheck) once the running `submit()` settles. Can arrive through `onError` while `submit()` still runs. |
| `unavailable` | This checkout cannot take a payment. Stop its controls and check the order on your server.                                                                                                                                                                                                                                                                                                                                                               |

A `pending` result or a `recheck` error does not permit another payment. Check the original
payment on your server before starting another checkout, even if a later status read shows it
declined or expired.

## Error codes [#error-codes]

The codes below cover the exported `HighWireErrorCode` union in 0.7.0. Recovery depends on
which method failed; a transport error during mount is different from a lost submit answer.
When an error carries [`recovery`](#checkoutrecovery), follow it before the code.

### Configuration and input [#configuration-and-input]

| Code              | Meaning and action                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HW_UNSUPPORTED`  | Invalid config/options, unsupported checkout, second mount, or submit on a checkout whose `requiresSubmit` is `false`. Also `billingAddressForm: 'full'` on another checkout or with a `billingAddress`, and `recheck()` on another checkout. On a drop-in form that uses your Pay button, also a payer or payer field of the wrong type, with recovery `correct`. A refused card attempt can also report this without charging, leaving checkout open. |
| `HW_UNAUTHORIZED` | Key/origin refused. Check onboarding configuration. Some unreadable key/account refusals appear as `HW_NETWORK` because of CORS.                                                                                                                                                                                                                                                                                                                        |
| `HW_INCOMPLETE`   | Incomplete card, missing/malformed email on `submit()`, missing billing `name` or `country` for a drop-in form that uses your Pay button, details that form refused, or missing saved-method identifier/key. On that form, also an email HighWire refuses, more than 3 different emails on one payment, or a submission HighWire did not take. Fix input before retrying. That form not being ready yet rejects it with recovery `retry`.               |
| `HW_NOT_READY`    | Card field not loaded or reloading. Wait for a complete `onChange`. Also `recheck()` with no submission to check, with recovery `unavailable`.                                                                                                                                                                                                                                                                                                          |
| `HW_DECLINED`     | Payment creation refused. Nothing mounted or charged; show decline copy without looping retries. On a drop-in form that uses your Pay button, `submit()` also rejects it with recovery `recheck` when HighWire's screening denies the shopper's email: the payment is kept, and the checkout takes no other payment.                                                                                                                                    |

### Transport and throttling [#transport-and-throttling]

| Code                     | Meaning and action                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HW_NETWORK`             | Network/CORS failure. From submit, retry the same checkout; from mount, retry mount. Persistent mount failures also require checking the key/account.                                                                                                                                                                                                                                               |
| `HW_TIMEOUT`             | Request, form loading, or 3-D Secure timed out. Retry a rejected mount; retry the same submit after a lost answer. An asynchronous form timeout requires unmount/remount.                                                                                                                                                                                                                           |
| `HW_SERVICE_UNAVAILABLE` | Gateway could not answer. Retain the attempt on submit retries; a rejected mount leaves nothing mounted.                                                                                                                                                                                                                                                                                            |
| `HW_RATE_LIMITED`        | Readable rate/velocity refusal from `mount()`, a status read, `submit()` on a drop-in form that uses your Pay button (recovery `retry`), or `chargeSavedMethod()`. Back off before retrying. A card submit refused by velocity checks resolves `declined` with `retryable: true` instead. For a saved-card velocity refusal retained under its key, reconcile before a later charge with a new key. |

### Form and operation state [#form-and-operation-state]

| Code                      | Meaning and action                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HW_TOKENIZE_IN_PROGRESS` | Another operation is running or an earlier payment is unresolved. Wait or recover the same attempt; do not bypass it with a new instance until your server has reconciled the earlier payment. With recovery `wait`, keep Pay disabled until the running operation settles. On a drop-in form that uses your Pay button, also a refused email or save-box change while its payment is live (recovery `recheck`), and `mount()` with another amount or save option while that payment may still capture. |
| `HW_DESTROYED`            | Checkout unmounted. If card confirmation was sent, it may have charged; remount identical options on the same instance and resubmit.                                                                                                                                                                                                                                                                                                                                                                    |
| `HW_PROTOCOL_MISMATCH`    | Form/SDK protocol incompatibility, reported through `onError`. Unmount/remount; contact HighWire if persistent.                                                                                                                                                                                                                                                                                                                                                                                         |
| `HW_IFRAME_UNRECOVERABLE` | Form cannot recover, reported through `onError`. Unmount/remount the same order.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `HW_TOKEN_EXPIRED`        | Included in the public code union, but has no separately documented checkout recovery contract in 0.7.0. Reconcile payment and contact HighWire if received.                                                                                                                                                                                                                                                                                                                                            |

### Ended checkouts [#ended-checkouts]

| Code                 | Meaning and action                                                                                                                                                                                                                                                                                                         |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HW_SESSION_EXPIRED` | Payment session/check-out ended, or its outcome was not observed. Read payment on your server before offering another attempt; it may have captured. On a drop-in form that uses your Pay button, when the form reports its session expired, `submit()` reads the payment and resolves `succeeded` instead if it was paid. |
| `HW_3DS_ABANDONED`   | Verification dialog closed. Checkout ended, but payment can still capture. Reconcile before remounting.                                                                                                                                                                                                                    |

Reconcile pending, abandoned, or unknown outcomes before remounting. Your backend authorizes
another payment; SDK remount permission does not prove that the previous attempt failed.

### Saved-card reuse [#saved-card-reuse]

| Code                        | Meaning and action                                                                                                               |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `HW_UPSELL_NOT_READY`       | Original payment not settled. Retry shortly with the same add-on key.                                                            |
| `HW_UPSELL_AMOUNT_EXCEEDED` | Amount above the permitted original-payment bound. Reconcile the refusal; a lower amount is a new charge and requires a new key. |
| `HW_UPSELL_REFUSED`         | Saved card cannot be charged again. Do not retry it in a loop.                                                                   |

See the [integration retry guide](/docs/payment-sdk/integration#5-errors-and-retries) for payment
recovery, and [safe diagnostics](/docs/payment-sdk/authentication#safe-diagnostics) for support.


# Security & server verification (/docs/payment-sdk/authentication)



## Browser credentials [#browser-credentials]

Use a publishable key in the browser: `hw_pk_test_…` for sandbox or `hw_pk_live_…` for
production. Keep secret API keys, HTTP Basic credentials, and other server credentials on your
backend. A publishable key identifies the checkout configuration. It does not authorize access to
an order or prove that a payment succeeded.

The publishable key and `apiBase` must belong to the same environment and account. The key does
not redirect a mismatched URL. `fieldsOrigin` defaults to `apiBase`; set it only to the separate
origin issued for your account. Use HTTPS for `apiBase`, `fieldsOrigin`, and the checkout page,
except for localhost development.

## Checkout origins [#checkout-origins]

Provide HighWire with each checkout origin during onboarding. Scheme, hostname, and port matter:
`https://shop.example.com`, `https://staging.shop.example.com`, and `http://localhost:5173` are
separate origins. Preview deployments may need separate registration.

A refused key or unregistered checkout origin can report `HW_UNAUTHORIZED`. Some gateway refusals
are not readable by browser code and can appear as `HW_NETWORK`. For either error, check the full
publishable key, gateway URL, account status, and registered origin before treating it as a network
problem.

## SDK-managed payment sessions [#sdk-managed-payment-sessions]

The mounted checkout runs its own device session and attaches the session key to its own payments.
For a Direct API charge, take the key from the collector's `riskSessionKey()`, created with the same
`publishableKey` and `apiBase` as `HighWire.init()`. Without the key it always resolves `null`;
without `apiBase` it uses the production gateway. It resolves to a string or `null`. Send the string to your backend
and forward it unchanged as the top-level `riskSessionKey` on the authorization or
authorization-and-capture request. Omit the field when the value is `null`. See
[Device screening for Direct API charges](/docs/payment-sdk/device-screening).

Treat the value as optional context, not authentication or proof of payment. Do not log it or put it
in URLs.

## Bind and verify orders on your server [#bind-and-verify-orders-on-your-server]

Use this sequence for every order:

1. Before `mount()`, call your backend's eligibility route (the examples return `canStartPayment`).
   It must authenticate the owning
   account or validate a guest session scoped to the order, load the server-authoritative total and
   currency, and reconcile any existing payment. Start a new checkout only when the backend permits
   it.
2. As soon as `mount()` resolves, send `checkout.paymentId` to the same backend and keep the
   checkout non-interactive until binding succeeds. A wallet or drop-in form can start payment
   without your Pay button.
3. At binding time, repeat the authorization and eligibility checks. Retrieve the payment with
   server credentials, confirm that it belongs to your merchant, and compare it with the order's
   server-side amount and currency.
4. Bind the payment and order in one transaction. Enforce unique constraints so one payment belongs
   to one order and one order has at most one active payment. Treat the same payment-order pair as
   idempotent. Reject a payment bound elsewhere, and reject replacement when the current payment is
   paid or unresolved. After reconciliation, create a replacement through an explicit transition
   that retains the earlier payment history.
5. Fulfill only after your server verifies the bound payment's captured status, amount, and currency
   through `GET /v1/payments/{paymentId}` or a verified gateway webhook. Keep reconciling pending
   orders after the shopper leaves because a payment can complete later.

Browser callbacks, browser amounts, and a browser-supplied payment ID do not authorize fulfillment.

### Protect same-origin binding routes [#protect-same-origin-binding-routes]

For each state-changing JSON route used by the checkout:

* Require an `Origin` header that exactly matches the merchant checkout origin. Reject a missing or
  different origin.
* Require `Content-Type: application/json` before parsing the body.
* Apply the application's normal cookie and session policy, including its `SameSite` setting. If the
  application uses CSRF tokens, validate the token on this route too.

The same-origin JSON `fetch` examples in this guide omit a CSRF token because the route contract
requires the exact origin and JSON content type. Add the application's token whenever its session
policy requires one.

<span id="server-api-credentials" />

Use the [Gateway authentication guide](/docs/authentication) for server credentials.

## Safe diagnostics [#safe-diagnostics]

Share `SDK_VERSION`, browser version, timestamp, checkout origin, and `HighWireError.code` with
support. Do not send secret credentials, card values, session or risk keys, cookies, authorization
headers, or full payment request and response bodies.


# Device screening for Direct API charges (/docs/payment-sdk/device-screening)



## When you need the collector [#when-you-need-the-collector]

`mount()` runs its own device session and attaches the key to the payments it creates. A checkout
that pays only through the Payment SDK needs nothing on this page.

When your backend charges through the Direct API instead, with
[`auth` or `auth-and-capture`](/docs/card-payments), the browser has to supply the screening
context itself. The collector does that: `HighWireCollect` from the
`@highwirepayments/payment-sdk/collect` entry. It loads no checkout code. The Payment SDK
instance does not return the key.

## Load the collector [#load-the-collector]

Load it from the CDN with its integrity hash:

```html
<script src="https://cdn.highwirepayments.com/collect/0.7.0/highwire-collect.js"
        integrity="sha384-TZ+jUu/98zNrFmHe2gs1NDAf2TZVnqI3f9Z3UxvphBa/5DnsPdMy0Cwp8kZ6ygRz"
        crossorigin="anonymous"></script>
```

The script registers `window.HighWireCollect`. A published version is never replaced, so its hash
stays valid for that URL. Ask HighWire for the hash of each version.

Or install the npm package and import the entry. Bundled imports do not use the hash:

```bash
npm install @highwirepayments/payment-sdk@0.7.0
```

```ts
import { HighWireCollect } from '@highwirepayments/payment-sdk/collect';
```

The `collect` entry is declared in the package's `exports` map, so your bundler must support it:
webpack 5, Vite, Rollup, esbuild, and TypeScript's `bundler`, `node16` and `nodenext` resolution all
do; webpack 4 does not.

If your page sets a Content Security Policy, allow:

* `script-src`: `https://cdn.highwirepayments.com/collect/` when you load it from the CDN, plus the
  screening script origins HighWire supplies for your account. The scripts the collector injects
  carry no nonce, so a nonce-based policy must list those hosts.
* `connect-src`: your `apiBase` origin, for the session and the browser's IP; the fallback IP
  lookups `https://api.ipify.org` and `https://ifconfig.co`; and the screening connect origins
  HighWire supplies for your account.

A blocked request makes the affected method resolve `null`.

## Create it early [#create-it-early]

Create one collector in browser code as soon as the checkout page loads, with your publishable key
and the same `apiBase` you pass to `HighWire.init()`:

```js
const collector = HighWireCollect({
  publishableKey: 'REPLACE_WITH_YOUR_TEST_PUBLISHABLE_KEY',
  apiBase: 'https://api-payments-staging.highwirepayments.com',
});
```

Creating it with `publishableKey` starts the device session at once, so screening signals build up
while the shopper fills in the form. A malformed key throws. Without `apiBase`, the collector starts
the session on the production gateway, whatever environment your checkout uses. Without
`publishableKey`, it starts no session and `riskSessionKey()` always resolves `null`.

A page that runs both the Payment SDK and the collector starts two device sessions, one each. The
checkout's own payments use its session; your Direct API charges use the collector's. On some
accounts the second session reports under the first one's key, so both can return the same key.
Forward whatever the collector returns.

## Send the key and device data with the charge [#send-the-key-and-device-data-with-the-charge]

In your submit handler, read both values and send them to your backend with the order:

```js
async function deviceDataOrNull() {
  try {
    return await collector.collect();
  } catch {
    return null;
  }
}

async function submitOrder(order) {
  const deviceData = await deviceDataOrNull();
  const riskSessionKey = await collector.riskSessionKey();

  return fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...order,
      ...(deviceData && { deviceData }),
      ...(riskSessionKey && { riskSessionKey }),
    }),
  });
}
```

Your backend forwards both unchanged on the authorization or authorization-and-capture request:
`riskSessionKey` as a top-level field, a sibling of `deviceData` and never inside it. Omit a field
whose value is `null`. See the [request body](/docs/card-payments#request-body) and
[device data](/docs/card-payments#device-data) rules.

Create the collector with `publishableKey` and call `collect()` only in the shopper's browser. The
constructor reads `document`, so it throws during server rendering, and a server runtime can make
`collect()` return valid-looking server values instead of failing, which attaches the wrong context
to the charge.

## `riskSessionKey()` [#risksessionkey]

```ts
riskSessionKey(): Promise<string | null>
```

It never rejects, and it waits at most 2 seconds:

* It resolves to the key once the session is live, usually a few seconds after the page loads. On a
  page that has been open for more than a moment, awaiting it in the submit handler costs nothing.
* It resolves to `null` when there is no screening: the collector was created without
  `publishableKey`, the session request failed or was refused, screening is off for your account,
  or the screening script was blocked.
* It also resolves to `null` when the session has not gone live within 2 seconds of the call. The
  session keeps loading, so a later call can still return the key.
* On some accounts it resolves a key even when the screening script is blocked or slow. Forward any
  key it returns.

On `null`, omit `riskSessionKey`: the charge proceeds and is screened without device signals.
HighWire ignores a key it did not mint for your account, or minted more than an hour ago, so a wrong value weakens the risk decision
rather than failing the call. Treat the key as optional context, not authentication or proof of
payment. Do not log it or put it in URLs.

## Troubleshooting [#troubleshooting]

* **`HighWireCollect is not defined` or `is not a function`:** the code ran before the CDN script loaded, or the script
  URL or `script-src` blocked it. Create the collector after the script has loaded, or import it
  from npm.
* **The browser reports an integrity mismatch:** the hash belongs to another version, or the file
  was changed after it was hashed. Use the hash for the exact version in the URL, and serve the file
  unchanged.
* **`riskSessionKey()` keeps resolving `null`:** check that the collector was created with your
  publishable key and the same `apiBase` as `HighWire.init()`. In the Network panel, check
  `POST /v1/sdk/payments/session`: a `401` means the key is unknown, often a test key sent to the
  production gateway because `apiBase` is missing; a `403` means your checkout origin is not
  registered with HighWire or your account is disabled or restricted; a `409` with
  `HW_NO_ACTIVE_PROVIDER_ACCOUNT` means your account takes no new payments. Check
  the console for `script-src` or `connect-src` violations against the screening origins HighWire
  supplies for your account. Whatever the cause, omit the field and continue the charge.
* **`collect()` resolves `null`:** the browser's IP or user agent could not be read, often because
  `connect-src` blocks the lookup. Omit `deviceData` and continue the charge.


# Drop-in & wallets (/docs/payment-sdk/drop-in-and-wallets)



<span id="choose-a-surface" />

<span id="drop-in-quick-start" />

<span id="drop-in-options-and-events" />

<span id="separate-wallet-buttons" />

## Gateway-selected layouts [#gateway-selected-layouts]

Call `hw.mount()` once for the whole checkout. The gateway session decides its layout; there
are no individual wallet or drop-in controllers in 0.7.0.

| Layout                                    | Parts added in order                      | `requiresSubmit` |
| ----------------------------------------- | ----------------------------------------- | ---------------- |
| `card_and_wallets`                        | Apple Pay, Google Pay, card               | `true`           |
| `card_only`                               | Card                                      | `true`           |
| `drop_in`, form with its own button       | Apple Pay when offered, then drop-in form | `false`          |
| `drop_in`, form that uses your Pay button | Drop-in form                              | `true`           |

The layout and available methods are configured for your HighWire account. When the session includes
a method list, a method absent from that list gets no slot. When the list is omitted,
`card_and_wallets` offers wallet slots; browser and device availability determine whether they show.
Show your Pay button only when `checkout.requiresSubmit` is `true`; a drop-in form with its own
payment button takes the payment there. Handle every method through `onProcessing`, `onResult`,
and `onError`; see [Integration](/docs/payment-sdk/integration#3-submit-and-handle-results).

<span id="card-data-boundary" />

The [CDN build](/docs/payment-sdk/integration#1-install-and-initialize) includes the wallet loader.
Wallet components run on your page; the card field and drop-in run inside HighWire frames.
Availability also depends on browser, device, and account configuration. An unavailable wallet
can stay hidden without reporting an error.

## Drop-in forms that use your Pay button [#drop-in-forms-that-use-your-pay-button]

Some account configurations use a drop-in form without a payment button of its own. The form takes
cards only: the shopper enters the card in it, and your page supplies the email, the billing details
and the Pay button:

* Enable your Pay button while `onAvailabilityChange` reports `true`. Call
  `checkout.submit({ customerEmail, billingAddress })` with the full billing address: at least the
  `name`, a two-letter `country` and the `postalCode`. The SDK checks the name and country; the form
  validates the rest of the address for that country and rejects one without a postal code.
  `customerPhone` is not sent.
* HighWire checks the email before the card is charged. A payment takes up to 3 different emails, so
  the shopper can correct one. Without `customerEmail`, `submit()` uses the email of the payment's
  last declined attempt; before any decline it rejects `HW_INCOMPLETE`.
* `onProcessing('dropIn')` fires when `submit()` sends a new submission, while HighWire checks the
  email. `submit()` resolves `succeeded`, `pending` or `declined`. After a decline with
  `retryable: true` the shopper can try another card; `retryable: false` ends the checkout.
* Refused card details, billing address or email, a fourth email on the same payment, and a
  submission HighWire did not take reject `HW_INCOMPLETE` with recovery `correct`.
* When HighWire denies the email, `submit()` rejects `HW_DECLINED` with recovery `recheck`. A
  changed email or save box that HighWire refuses while the payment is in progress rejects
  `HW_TOKENIZE_IN_PROGRESS` with recovery `recheck`. Both keep the payment, because an earlier
  attempt may still capture: a later `submit()` only reads its status, for up to 3 minutes, and the
  checkout takes no other payment. Check it on your server before starting another checkout.
* Errors carry [`recovery`](/docs/payment-sdk/api-reference#checkoutrecovery). After an unknown
  outcome, another `submit()` only checks the original payment, and
  [`checkout.recheck()`](/docs/payment-sdk/api-reference#checkoutrecheck) checks it without submitting.
* With `savePaymentMethodForFutureUse: true`, the form shows an unticked save box under the card
  fields. Only the shopper's tick saves the card, under the email `submit()` sends; a `succeeded`
  result then carries `customerId` and `paymentMethodId`. It never carries `maskedCard`.
* The form shows the payment provider's buyer terms under the card fields and runs card verification
  itself. Closing its verification dialog keeps the attempt and offers to finish verification. A
  verification step can also take the shopper's tab to another page and back to a HighWire return
  page; your page then loses its state, so settle the order from your server's view of the payment.

The [Plain HTML](/docs/payment-sdk/guides/plain-html) and
[React / Next.js](/docs/payment-sdk/guides/react-nextjs) guides show complete controls, billing
fields included, that work for this form and every other layout.

### Collect the billing address in the form [#collect-the-billing-address-in-the-form]

Mount with `billingAddressForm: 'full'` to have the shopper enter the billing address in the
drop-in form instead of on your page. `submit()` then takes the email alone:

```js
const checkout = await hw.mount({
  container: '#payment',
  amount: 2999,
  currency: 'usd',
  billingAddressForm: 'full',
  // …your callbacks, unchanged
});

// From your Pay button, enabled while onAvailabilityChange reports true:
await checkout.submit({ customerEmail: 'shopper@example.com' });
```

* `submit()` takes no `billingAddress`. Passing one, even `{}`, rejects `HW_UNSUPPORTED` with
  recovery `correct` before anything is sent.
* When the form's fields are incomplete, `submit()` rejects `HW_INCOMPLETE` with recovery
  `correct` and sends nothing. The form may not mark the missing field, so ask the shopper to
  check both the card and the billing fields.
* The address stays inside the form. Neither the SDK nor the payment status returns it, and it
  cannot be prefilled.
* The form grows taller as the shopper adds address lines. Do not cap the container's height.
* Only a drop-in form that uses your Pay button supports `'full'`, and the checkout's form is known
  only once `mount()` has created the payment. When a `'full'` mount rejects `HW_UNSUPPORTED`,
  mount again with `'none'` and show your own billing fields: the same payment is reused. To
  switch modes on a mounted checkout, `unmount()` it and mount again.
* When HighWire's service cannot show the billing fields, the failure arrives after `mount()`
  resolved: `onError` receives `HW_UNSUPPORTED` with recovery `unavailable`, and `onReady` never fires.

## Apple Pay setup [#apple-pay-setup]

1. Ask HighWire to register your checkout domain for Apple Pay and provide the required domain-association file.
2. Serve the file unchanged at `https://your-domain/.well-known/apple-developer-merchantid-domain-association`, as plain text, with no extension or redirect.
3. Request that URL and verify it returns the file, rather than your site's HTML fallback.
4. Confirm domain verification with HighWire before enabling Apple Pay in production.

Apple Pay renders over HTTPS in Safari. Elsewhere, its slot stays `hidden` or `data-hw-empty`
without calling `onError`. A checkout inside a third-party iframe also needs Safari 17 or later,
`allow="payment"` on that iframe, and both domains registered.

Google Pay needs no domain-association file. Its sheet shows the merchant name from the session.

## Styling and accessibility hooks [#styling-and-accessibility-hooks]

| Hook                                                           | Meaning                                                                                                                                                                                                                                                     |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-hw-layout` on the container                              | Current layout name.                                                                                                                                                                                                                                        |
| `data-hw-slot="applePay"`, `"googlePay"`, `"card"`, `"dropIn"` | Direct child slots appended after any existing children. Reorder with flex/grid `order`.                                                                                                                                                                    |
| `hidden` on a wallet slot                                      | Wallet unavailable or retired after expiry. Do not override it with a `display` rule on the slot.                                                                                                                                                           |
| `data-hw-empty`                                                | Slot currently draws nothing. Collapse spacing, rather than hiding the slot.                                                                                                                                                                                |
| `inert`, `aria-disabled="true"` on wallet/drop-in slots        | Payment in progress or a card-confirm answer still unknown. A drop-in form that uses your Pay button is also locked until it can take a submission, and while a submission runs or its outcome is unknown, except while the form shows a verification step. |
| `data-hw-processing` on the container                          | Method being confirmed, from `onProcessing` until result or error. An error with recovery `wait` leaves it in place.                                                                                                                                        |

```css
#payment { display: flex; flex-direction: column; position: relative; }
#payment > [data-hw-slot] { margin-bottom: 12px; }
#payment > [data-hw-empty] { margin-bottom: 0; }
#payment > [data-hw-slot="card"] { order: -1; }
#payment[data-hw-processing]::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgb(255 255 255 / .7);
}
```

Never set `display: none` on an empty slot: it can no longer measure itself and may never recover.
Use an accessible status message alongside a visual processing overlay.

Drop-in theming and overlay behavior depend on your account configuration.
Confirm supported behavior with HighWire and test the configured checkout in your page layout.
HighWire's card 3-D Secure dialog opens above the page.

`unmount()` removes slots and `data-hw-processing`, and restores the container's original
`data-hw-layout`. A checkout that ends removes its slots automatically.

<span id="context-sessions-and-order-changes" />

## Session lifetime [#session-lifetime]

Payment sessions expire. When the SDK reports `HW_SESSION_EXPIRED`, stop using that checkout and
reconcile its payment on your server before creating a replacement. The payment may have completed
even when the page did not receive a final answer.

Use the shared [result-handling rules](/docs/payment-sdk/integration#3-submit-and-handle-results)
for every method. Browser results guide the UI; fulfillment requires
[server verification](/docs/payment-sdk/authentication#bind-and-verify-orders-on-your-server).

## Content Security Policy [#content-security-policy]

Allow the SDK script, gateway requests, and HighWire frames. Wallet and device-screening
components also run on the merchant page. Obtain the required account-specific sandbox and
production allowlists from HighWire before enforcing CSP.

| Directive     | Starting allowance                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `script-src`  | `'self'` and `https://cdn.highwirepayments.com/sdk/`, plus the wallet and screening script origins supplied by HighWire. |
| `frame-src`   | Your configured HighWire field origin, plus wallet, screening, and verification origins from the account allowlist.      |
| `connect-src` | `'self'` and your HighWire gateway origin, plus the wallet and screening endpoints from the account allowlist.           |

These starting allowances are not a complete policy. Ask HighWire for the origins that apply to
your enabled methods and environment. Do not substitute broad hosting-domain wildcards.
External merchant integration files work with `'self'`; inline scripts need a nonce or hash.
Wallet components can use inline style attributes; if you restrict styles, test the required
`style-src` or `style-src-attr` allowances with HighWire in report-only mode.

3-D Secure requires `frame-src` and, when set, `form-action` to allow the HighWire gateway
and verification pages. Issuer origins vary by card: a fixed list can block challenges. Test
the necessary allowances on your checkout route; some integrations need `https:` for those
two directives to support issuer handoffs.

Run `Content-Security-Policy-Report-Only` through card, wallet, device screening, and challenge
flows, then confirm observed origins with HighWire before enforcement. Also check `default-src`,
`img-src`, and other restricted directives.

Keep the CDN allowance scoped to `/sdk/` or the exact file. SRI verifies the script tag you
provide; it does not secure every script allowed by your policy.

Blocked wallet scripts leave those wallets unavailable while card or drop-in payment can continue.


# Payment SDK (/docs/payment-sdk)



## What the Payment SDK does [#what-the-payment-sdk-does]

The **HighWire Payment SDK 0.7.0** draws the checkout configured for your account into one
container: a secure card field with Apple Pay and Google Pay, the card field alone, or a
drop-in payment form. Your gateway session decides the layout. Every payment method
reports through the same `onResult` callback.

The HighWire card field collects card details in a cross-origin iframe and encrypts them before
sending them to the gateway. Your page receives field state and payment results, rather than
card values. Confirm PCI DSS scope and SAQ eligibility for your complete integration with your
acquirer or QSA.

<SDKDiagram name="card-data-boundary" width="1774" height="887" alt="Card details stay inside the HighWire-hosted field. The merchant page receives state and results, while the gateway receives encrypted card details. The merchant server verifies payment status before fulfilling the order." />

<span id="choose-a-payment-surface" />

## Integration flow [#integration-flow]

1. Await `HighWire.init()` with your publishable key and gateway origin.
2. Call `hw.mount()` when the shopper reaches payment. This creates the payment and draws the checkout.
3. Bind `checkout.paymentId` to the order on your server before enabling payment controls.
4. Show your Pay button only when `checkout.requiresSubmit` is `true`, and call `checkout.submit({ customerEmail, billingAddress })` from it with the shopper's email, plus the billing address on a drop-in form that uses your Pay button. It pays with the card field, or with a drop-in form that uses your Pay button. Wallets and other drop-in forms use their own buttons.
5. Handle `succeeded`, `declined`, and `pending` through `onResult`. Fulfill only from authenticated server-side payment status.

Start with the [Integration guide](/docs/payment-sdk/integration), or follow a framework guide:

* [Plain HTML](/docs/payment-sdk/guides/plain-html): versioned CDN script with integrity verification.
* [React / Next.js](/docs/payment-sdk/guides/react-nextjs): pinned CDN loading, TypeScript types, and component cleanup.

Use the [API Reference](/docs/payment-sdk/api-reference) to look up every checkout method,
parameter, callback, and return type.

See [Drop-in & wallets](/docs/payment-sdk/drop-in-and-wallets) for layout, Apple Pay setup, and CSP.
See [Security & server verification](/docs/payment-sdk/authentication) for the merchant backend contract.
Coming from 0.6.0? Read [Upgrading from 0.6.0](/docs/payment-sdk/integration#upgrading-from-060).

## Payment SDK and the Direct API collector [#payment-sdk-and-the-direct-api-collector]

The checkout SDK runs its own device session and attaches the session key to the payments
`mount()` creates. You do not need a separate collector for `mount()`.

If you also charge through the Direct API, load the collector: the same npm package provides it
as a separate `@highwirepayments/payment-sdk/collect` entry, exporting `HighWireCollect`. It
collects browser context and the device-session key without loading checkout code. See
[Device screening for Direct API charges](/docs/payment-sdk/device-screening). The Payment SDK
instance no longer returns the key.


# Integration guide (/docs/payment-sdk/integration)



Use this guide for the checkout flow. Choose [Plain HTML](/docs/payment-sdk/guides/plain-html)
or [React / Next.js](/docs/payment-sdk/guides/react-nextjs) for a complete example, and use the
[API Reference](/docs/payment-sdk/api-reference) for parameters and callbacks.

<span id="configuration" />

## 1. Install and initialize [#1-install-and-initialize]

Load the pinned CDN build, which includes checkout and wallet loaders:

```html
<script src="https://cdn.highwirepayments.com/sdk/0.7.0/highwire.js"
        integrity="sha384-NZVoZbDdfN3Ix90/7E+1ojIBysBz1Cwhizp9zPhjUxaOVkMdHBk9RSSxMSAO44LU"
        crossorigin="anonymous"></script>
<script src="/checkout.js" defer></script>
```

Inside an async browser function or module, await initialization:

```js
const HighWire = window.HighWire.default;
const hw = await HighWire.init({
  publishableKey: 'REPLACE_WITH_YOUR_TEST_PUBLISHABLE_KEY',
  apiBase: 'https://api-payments-staging.highwirepayments.com',
});
```

`init()` resolves once the checkout's device session is ready, or after at most 2 seconds while it
keeps loading; `mount()` then waits at most 2 seconds more for the session key. A screening failure
never rejects either.

Use the matching key and gateway URL issued during onboarding, and register your checkout
origins. See [Security & server verification](/docs/payment-sdk/authentication).

For TypeScript declarations, install the matching npm version:

```bash
npm install @highwirepayments/payment-sdk@0.7.0
```

With this CDN runtime, use type-only npm imports:

```ts
import type { Checkout, PaymentResult } from '@highwirepayments/payment-sdk';
```

<span id="card-events-and-controller" />

## 2. Mount checkout at the payment step [#2-mount-checkout-at-the-payment-step]

Before mounting, your backend must reconcile any existing order payment and authorize a new
attempt. Then call `hw.mount()` with the server's order amount in integer cents and USD currency.
It creates or reuses a payment; mount one checkout at a time per instance when the shopper reaches payment.

```js
const checkout = await hw.mount({
  container: '#payment',
  amount: 2999,
  currency: 'usd',
  onResult: (result) => console.log(result.status),
  onError: (error) => console.error(error.code),
});
```

This is an API fragment. The framework guides show the complete controls and binding flow.
Keep the checkout container `inert` until your backend has
[verified and bound `checkout.paymentId` to the order](/docs/payment-sdk/authentication#bind-and-verify-orders-on-your-server).
Wallets and drop-in forms with their own button can pay through their own controls.

`mount()` takes no shopper email: pass it to `submit()`. In plain JavaScript, a `customerEmail`
passed to `mount()` is ignored.

The session selects the [checkout layout](/docs/payment-sdk/drop-in-and-wallets).
Mount the whole checkout; there are no individual element controllers.

<span id="submit-context-and-results" />

## 3. Submit and handle results [#3-submit-and-handle-results]

Show your Pay button only when `checkout.requiresSubmit` is `true`: for the card field, and for a
[drop-in form that uses your Pay button](/docs/payment-sdk/drop-in-and-wallets#drop-in-forms-that-use-your-pay-button).
Enable it after binding, while no operation is running, and while the form can take a payment:

| Checkout                               | Enable Pay while                                                    |
| -------------------------------------- | ------------------------------------------------------------------- |
| Card field                             | `onChange` reports `complete: true`.                                |
| Drop-in form that uses your Pay button | `onAvailabilityChange` reports `true`. Disable it again on `false`. |

A checkout reports only one of the two. `onReady` alone does not mean the form can take a
payment; form-loading failures can occur after `mount()` resolves.

Call `checkout.submit({ customerEmail })` from your Pay button with the shopper's email. A card
`submit()` without a valid email rejects `HW_INCOMPLETE` before anything is sent. The
[payer details](/docs/payment-sdk/api-reference#checkoutsubmit) also accept phone and billing address.
A drop-in form that uses your Pay button also needs the full billing address, with at least `name`,
a two-letter `country`, and `postalCode`, unless you mount it with
[`billingAddressForm: 'full'`](/docs/payment-sdk/drop-in-and-wallets#collect-the-billing-address-in-the-form).
Wallets and drop-in forms with their own button share `onResult`.

`submit()` resolves with the same result delivered to `onResult`: handle it once. Declines resolve;
operational errors reject.

| Result                         | Action                                                                        |
| ------------------------------ | ----------------------------------------------------------------------------- |
| `succeeded`                    | Show confirmation; fulfill only after server verification.                    |
| `declined`, `retryable: true`  | Keep checkout open; allow another card or method on the same payment.         |
| `declined`, `retryable: false` | Checkout ended; reconcile before another attempt.                             |
| `pending`                      | Checkout ended with an unresolved payment; check order status on your server. |

A card decline in a drop-in form that uses your Pay button also resolves `declined` and reaches
`onResult`: `retryable: true` keeps the checkout open for another card, and `retryable: false` ends
it. When that form refuses the card details, billing address, or email, `submit()` rejects
`HW_INCOMPLETE` with recovery `correct`: let the shopper correct them and pay again. See
[errors with recovery](#errors-with-recovery).

On that form, HighWire screens the shopper's email and binds it to the payment before the card is
charged. A later `submit()` without an email reuses the email of the payment's last declined
attempt; before any decline, it rejects `HW_INCOMPLETE`. The shopper can correct the email, and
each new one is screened again. A payment accepts at most 3 different emails.

### 3-D Secure [#3-d-secure]

Keep checkout mounted and Pay disabled during verification. Closing the challenge rejects
`HW_3DS_ABANDONED` and ends checkout, but the payment can still capture: reconcile it on your server.
A timeout or challenge that could not open can be retried with `submit()` on the same checkout.
That retry may reopen the original challenge and pay with the original card, even after field
changes; saved-card and masked-card fields can be absent from its result.

A drop-in form that uses your Pay button runs verification inside the form instead, and
`HW_3DS_ABANDONED` does not apply. Closing its verification dialog keeps the attempt, and the form
offers to finish verification. A full-page verification step can take the shopper's tab away and
back to HighWire's return page, so your page loses its state: settle the order from your server's
payment read or webhook.

<span id="payment-sessions-and-order-changes" />

## 4. Order changes and cleanup [#4-order-changes-and-cleanup]

Call `checkout.unmount()` when leaving checkout. It removes UI without cancelling a charge.
After unmounting, an open payment can be reused on the same instance with the same amount,
currency, and `savePaymentMethodForFutureUse`. Currency is case-insensitive, and an omitted save
flag is equivalent to `false`. Changing the amount or currency creates another payment. Changing
only the save option creates another payment once the last one can no longer capture. On a drop-in
form that uses your Pay button, a different amount or save option rejects
`HW_TOKENIZE_IN_PROGRESS` while its payment may still capture.

The SDK accepts no merchant order ID, and the email no longer tells two orders apart: two orders
with the same amount reuse the same payment. For a different business order with identical
options, use a fresh instance after your backend authorizes it. Preserve order/payment bindings across reloads.

If unmounting interrupts an unanswered card confirmation, submit rejects `HW_DESTROYED`.
Recover by remounting the same normalized options on the same instance and retrying submit.
SDK-generated attempt keys do not survive a reload; after reloading, recover payment status on
your server before authorizing another attempt.

A submission from a drop-in form that uses your Pay button survives `unmount()` while its
outcome is unknown. On the same instance, mounting different options then rejects
`HW_TOKENIZE_IN_PROGRESS` until [`checkout.recheck()`](/docs/payment-sdk/api-reference#checkoutrecheck)
reads the payment succeeded; a declined or expired read does not lift it. The same options reuse
its payment for a status check only: `submit()` never sends it again. After a `pending` result,
the same options reject `HW_SESSION_EXPIRED`. Once your server has reconciled that payment and
authorizes a new one, start it on a fresh instance, for example after a reload.

The same holds after that form's `submit()` rejects `HW_DECLINED` with recovery `recheck`, because
HighWire denied the shopper's email, or `HW_TOKENIZE_IN_PROGRESS` with recovery `recheck`, because
HighWire refused a changed email or save box while the payment was in progress. The checkout keeps
the original payment: a later `submit()` only reads it, for up to 3 minutes, resolving `succeeded`
if it captured and `pending` otherwise. A read that fails for good rejects `HW_SERVICE_UNAVAILABLE`
with recovery `recheck`. Even a declined read never permits a replacement payment
on that instance.

After `pending`, abandoned verification, or any unknown outcome, reconcile before remounting.
The SDK permitting a new mount is not proof that the earlier payment failed.

<span id="errors" />

## 5. Errors and retries [#5-errors-and-retries]

Mount and submit rejections also reach `onError`; show each error once. Keep a submit `catch`
because calls after unmount only reject. Use your own shopper-facing copy rather than raw responses.

* **Transient mount failure:** retry `HW_NETWORK`, `HW_TIMEOUT`, or `HW_SERVICE_UNAVAILABLE`.
  Back off on `HW_RATE_LIMITED`; stop on declined, unauthorized, or invalid configuration.
* **Lost submit answer:** retry the same checkout's `submit()` to retain its attempt key.
  Back off on rate limiting. Do not switch instances or methods to bypass an unresolved attempt.
* **Form-loading failure:** unmount the unusable form and reconcile before remounting.
  An asynchronous form `HW_TIMEOUT` needs this recovery, unlike a rejected mount timeout.
* **Ended checkout:** check payment on your server before enabling another attempt.

See the [complete error-code table](/docs/payment-sdk/api-reference#error-codes) for input,
operation-state, and saved-card recovery.

### Errors with recovery [#errors-with-recovery]

Errors from a drop-in form that uses your Pay button carry `error.recovery`. Act on it before
the code:

* **`correct`:** the payer details, the email, or the card or billing fields in the form, were
  refused. This includes an email HighWire refuses and a fourth different email on one payment. Let
  the shopper correct them, then submit again.
* **`retry`:** the payment was not sent. Allow another `submit()` while `onAvailabilityChange` reports `true`.
* **`wait`:** another operation is still running. Keep Pay disabled; its result or error follows.
* **`recheck`:** the payment may still capture: its outcome is unknown, HighWire denied the
  shopper's email (`HW_DECLINED`), or it refused a change during the payment
  (`HW_TOKENIZE_IN_PROGRESS`). Keep Pay disabled and tell the shopper the payment is being
  confirmed. Check it on your server, or call
  [`checkout.recheck()`](/docs/payment-sdk/api-reference#checkoutrecheck) once the running
  `submit()` settles. Another `submit()` only checks the same payment; it never sends it again.
* **`unavailable`:** this checkout can't take a payment. Stop its controls and check the order on your server.

A `recheck` error can reach `onError` while `submit()` is still confirming the payment, so the
result that follows still counts. See [`CheckoutRecovery`](/docs/payment-sdk/api-reference#checkoutrecovery).

<span id="save-a-card-and-charge-it-again" />

## Saving a card [#saving-a-card]

<details>
  <summary>
    Optional: save and reuse a card
  </summary>

  Mount with `savePaymentMethodForFutureUse: true` to show an unticked save box in the card field,
  or under the card form of a drop-in form that uses your Pay button. Only the shopper's tick saves
  the card, under the email `submit()` sends, and HighWire finds or creates the customer then. A
  drop-in form with its own button and wallets show no box and save no card.

  The result carries `customerId` and `paymentMethodId` only when the shopper ticked the box; on a
  drop-in form that uses your Pay button, only on `succeeded`. A card the shopper saved before keeps
  its ID. Changing the box while that form's payment is in progress rejects
  `HW_TOKENIZE_IN_PROGRESS` with recovery `recheck`.

  Use the returned `paymentMethodId` with
  [`hw.chargeSavedMethod()`](/docs/payment-sdk/api-reference#hwchargesavedmethod). A card saved
  through a drop-in form that uses your Pay button can resolve `pending`: settle it from your server.
  If its issuer asks for 3-D Secure again, the charge does not capture, because there is no shopper
  step for it.

  Store a stable idempotency key with each add-on charge and reuse it across retries and reloads.
  An unknown outcome must be recovered with that key. After a confirmed amount refusal, lowering
  the amount is a new charge and needs a new key. A saved-card rate refusal retained under its key
  also requires reconciliation before a later charge with a new key. Verify every result on your server.
</details>

## Upgrading from 0.6.0 [#upgrading-from-060]

Update the script URL, its SRI hash, the npm types, and any `SDK_VERSION` check, then:

* **Move `customerEmail` from `mount()` to `submit()`.** Call `checkout.submit({ customerEmail })`.
  A card `submit()` without an email now rejects `HW_INCOMPLETE`, and TypeScript rejects
  `customerEmail` in `mount()` options.
* **Replace `hw.riskSessionKey()`.** The instance has only `mount()` and `chargeSavedMethod()`.
  `mount()` still attaches the device-session key to its own payments. For Direct API charges,
  take the key from the collector; see
  [Device screening for Direct API charges](/docs/payment-sdk/device-screening).
* **Card saving.** `savePaymentMethodForFutureUse` no longer needs an email. It shows an unticked
  save box, and only the shopper's tick saves the card; see [Saving a card](#saving-a-card).
* **Drop-in forms that use your Pay button.** A card decline now resolves `declined` instead of
  rejecting `HW_INCOMPLETE`. A `submit()` email that differs from an earlier one is screened again
  instead of rejecting `HW_UNSUPPORTED`. Send the full billing address, including `postalCode`.
* `HighWire.init()` resolves within 2 seconds instead of waiting up to about 14 for the device
  session.

0.7.0 needs HighWire's matching service release, so coordinate the upgrade with HighWire:

| Checkout                                                  | 0.6.0 against the new release                                                                | 0.7.0 against an older release                                                               |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Drop-in form that uses your Pay button                    | Fails through `onError` with `HW_UNSUPPORTED` after `mount()` resolves and takes no payment. | Fails through `onError` with `HW_UNSUPPORTED` after `mount()` resolves and takes no payment. |
| Card field saving                                         | Never shows the save box and saves no card.                                                  | Shows the save box, but a tick saves nothing.                                                |
| Card fields, wallets, drop-in forms with their own button | Keep paying.                                                                                 | Keep paying.                                                                                 |

## Upgrading from 0.5.0 [#upgrading-from-050]

This section describes the move to 0.6.0. Upgrade straight to 0.7.0 and apply
[Upgrading from 0.6.0](#upgrading-from-060) as well: 0.7.0 replaces the email rules below.

Card field, wallet, and other drop-in integrations need no code change: update the script URL,
its SRI hash, the npm types, and any `SDK_VERSION` check.

Some account configurations use a drop-in form that takes payment through your Pay button
instead of its own. On those checkouts:

* `checkout.requiresSubmit` is now `true`. Show your Pay button, enable it from
  `onAvailabilityChange`, and call `checkout.submit()`. Without your own Pay button, the checkout
  takes no payment.
* `mount()` requires `customerEmail` and rejects `HW_UNSUPPORTED` without it. `submit()` must use the same email.
* `submit()` requires a `billingAddress` with `name` and a two-letter `country`. Alternatively,
  mount with `billingAddressForm: 'full'` to collect the address inside the form.
* `submit()` resolves `succeeded` or `pending`, and errors carry `recovery`; see
  [errors with recovery](#errors-with-recovery).

These checkouts need SDK 0.6.0 and HighWire's matching service release together. Once your
environment serves that release, 0.5.0 and earlier fail on them through `onError` and take no
payment. 0.6.0 against an environment that doesn't serve it yet fails the same way with
`HW_UNSUPPORTED`. Coordinate the upgrade with HighWire.

<span id="testing-security-and-support" />

## Testing and support [#testing-and-support]

Use the issued test key and staging gateway. Obtain test cards from your integration engineer.
Exercise success, retryable and terminal declines, pending, network retries, verification, cleanup,
and enabled wallets. Check [CSP](/docs/payment-sdk/drop-in-and-wallets#content-security-policy)
before enforcing it, and follow [safe diagnostics](/docs/payment-sdk/authentication#safe-diagnostics)
when contacting support.


# Account limits (/docs/account-limits)



Two kinds of controls can refuse a call before any money moves: **merchant limits**, volume caps
configured per account, and **velocity checks**, which stop unusual bursts of attempts.

## Merchant limits [#merchant-limits]

Each account can carry up to four caps. They are opt-in: a cap that is not configured on your
account is not enforced. Ask High Wire what is configured on yours, and ask ahead of time if you
expect to operate near a ceiling.

| Limit           | What it caps                                 | Window                     |
| --------------- | -------------------------------------------- | -------------------------- |
| Per-transaction | The amount of a single charge or checkout.   | None, checked per request. |
| Daily           | The sum of successful captures in the day.   | `00:00` to `24:00` UTC.    |
| Monthly         | The sum of successful captures in the month. | The calendar month, UTC.   |
| Daily refund    | The sum of successful refunds in the day.    | `00:00` to `24:00` UTC.    |

Windows are computed in UTC, not in your local time. Every surface counts toward the caps: direct
charges, saved-method charges and hosted checkouts alike. Only successful payments count toward
the sell caps, and only successful refunds toward the refund cap.

### What you see when a limit is hit [#what-you-see-when-a-limit-is-hit]

A `400` with the decline body of the endpoint ([charge decline](/docs/errors-and-conventions#charge-decline)
or [refund decline](/docs/errors-and-conventions#refund-decline)) and one of these `declinedReason`
values:

| `declinedReason`                       | Limit            |
| -------------------------------------- | ---------------- |
| `Amount exceeds per-transaction limit` | Per-transaction. |
| `Amount exceeds daily limit`           | Daily.           |
| `Amount exceeds monthly limit`         | Monthly.         |
| `Amount exceeds daily refund limit`    | Daily refund.    |

```json
{
  "amount": 250000,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example",
  "status": "DECLINED",
  "declinedReason": "Amount exceeds per-transaction limit"
}
```

The customer is never debited by a limit decline. The attempt is recorded as a `DECLINED` payment
and the response is stored under the `Idempotency-Key`: a later attempt needs a new key.

## Velocity checks [#velocity-checks]

The card charges, `auth-and-capture`, `auth` and `charge-saved-method`, run velocity checks before
anything else. A refusal is a `429` with `code: "VELOCITY_LIMIT"` and one of two messages:

| `message`                    | Meaning                               |
| ---------------------------- | ------------------------------------- |
| `Velocity limit exceeded`    | Too many attempts in a short time.    |
| `Duplicate purchase blocked` | The same purchase was just attempted. |

```json
{
  "statusCode": 429,
  "message": "Duplicate purchase blocked",
  "code": "VELOCITY_LIMIT"
}
```

Nothing is charged and no payment is recorded. The thresholds are set by High Wire and are not
published. The refusal is stored under the `Idempotency-Key` like a decline, so resending the same
key answers the same `429`: back off, and send a new attempt under a new key later.


# Authentication (/docs/authentication)



Every request to the merchant API carries your account's credentials with **HTTP Basic
authentication**:

```http
Authorization: Basic base64(<username>:<secret>)
```

`curl -u` does the encoding for you:

```bash
curl https://api-payments-staging.highwirepayments.com/v1/payments/01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b \
  -u "YOUR_API_KEY:YOUR_SECRET"
```

## Credentials [#credentials]

* High Wire issues a **username** and a **secret** for your account, per environment, when you
  onboard. There is no self-service or programmatic registration;
  [request access](https://www.highwirepayments.com/#contact) to get started.
* The secret is shown **once**, when it is created or rotated, and cannot be retrieved later.
  Store it in your secrets manager.
* To rotate the secret, ask your High Wire contact. The previous secret stops working the moment
  the new one is issued.
* Credentials are **environment-scoped**: staging credentials work only against the staging host,
  production credentials only against production. See
  [Environments](/docs/introduction#environments).
* **Server-side only.** Never ship these credentials in browser or mobile code. The browser
  surfaces of High Wire (the [Payment SDK](/docs/payment-sdk) and the hosted checkout page)
  identify your account with a **publishable key** (`hw_pk_test_…` or `hw_pk_live_…`) and
  per-session tokens instead; Basic credentials are refused there, and publishable keys are refused
  on this API. See [Payment SDK authentication](/docs/payment-sdk/authentication).

## Request rules [#request-rules]

* Send the header on every request. The API does not send a `WWW-Authenticate` challenge on `401`,
  so a challenge-response client, or a browser prompt, will not work.

## Account states [#account-states]

| State       | What the API allows                                                                                                                                     |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Active      | Everything.                                                                                                                                             |
| Refund-only | Only `POST /v1/payments/{id}/void`, `POST /v1/payments/{id}/refund` and `GET /v1/payments/{id}`. Every other route answers `403`.                       |
| Not active  | Nothing: every call answers `401` `Invalid credentials`, exactly like a wrong secret, so an inactive account cannot be used to confirm a leaked secret. |

A state change is made by High Wire, not through the API. If you unexpectedly receive `401` with
credentials you know are right, or `403`, contact your High Wire contact.

## Error responses [#error-responses]

These answers are never stored under an `Idempotency-Key` and never create a payment. They use the
[plain error](/docs/errors-and-conventions#plain-error) shape.

| Status | `message`                                                                                                                                                               | When                                                                                                                                                                                             |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | `Missing Authorization header`, `Invalid Authorization header format`, `Invalid basic auth credentials format`, `Invalid basic auth credentials`, `Invalid credentials` | The header is missing or malformed, the credentials are not valid, or the account is not active. An unknown username, a wrong secret and an inactive account are deliberately indistinguishable. |
| `403`  | `Merchant account can only process refunds and voids`                                                                                                                   | A refund-only account called a route other than void, refund or retrieve.                                                                                                                        |

```json
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid credentials"
}
```

```json
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Merchant account can only process refunds and voids"
}
```

If a secret leaks, ask High Wire to rotate it immediately; the leaked one stops working at once.


# Card payments (/docs/card-payments)



The direct API takes card details from your server and charges them. Use it only when your
environment is validated to handle card data. Otherwise collect the card in the browser with the
[Payment SDK](/docs/payment-sdk), or hand the shopper a [hosted checkout](/docs/hosted-checkout)
link and never touch the card at all.

<Callout type="warn" title="Card data and PCI DSS">
  Sending card numbers to this API puts your systems in scope for PCI DSS (typically SAQ D). Use
  test cards only in examples and tooling, and never put real card data in a shell, its history,
  logs, tickets or chat. Never log request bodies, never store the security code, and keep card
  data in memory only for as long as the request needs it.
</Callout>

| Flow                            | Endpoints                                                                                        | When                                                                                                |
| ------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Authorize and capture           | `POST /v1/payments/auth-and-capture`                                                             | The common case: charge the card now.                                                               |
| Authorize, then capture or void | `POST /v1/payments/auth`, then `POST /v1/payments/{id}/capture` or `POST /v1/payments/{id}/void` | Hold the funds first (stock check, manual review, charge on shipment), then settle or release them. |

Both charge routes take the same [request body](#request-body) and answer the same shape. Money
that was captured is returned with a [refund](/docs/refunds); an authorization that was not
captured is released with a [void](#void).

**Availability.** Not every account can use the direct card routes; High Wire confirms at
onboarding which surfaces yours supports. A route your account cannot use answers a
[charge decline](/docs/errors-and-conventions#charge-decline) that says so, see [Declines](#declines).

## Request body [#request-body]

Shared by `auth-and-capture` and `auth`.

| Field                           | Type    | Required | Rules                                                                                                                                                                                                                                                                 |
| ------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount`                        | integer | yes      | Cents, at least `1`. `7200` is $72.00.                                                                                                                                                                                                                                |
| `cardNumber`                    | string  | yes      | 12 to 19 digits, no spaces or separators.                                                                                                                                                                                                                             |
| `cardExpiration`                | string  | yes      | `MMYY`: `1133` is November 2033.                                                                                                                                                                                                                                      |
| `cardCvv`                       | string  | yes      | The card's security code. Never stored.                                                                                                                                                                                                                               |
| `cardholderName`                | string  | yes      | Name on the card.                                                                                                                                                                                                                                                     |
| `cardholderEmail`               | string  | yes      | The customer's e-mail, trimmed and lowercased. When the card is saved, this is what identifies the customer. The format is not validated.                                                                                                                             |
| `cardholderPhone`               | string  | yes      | E.164 recommended, for example `+14155550101`.                                                                                                                                                                                                                        |
| `cardholderBillingAddress`      | string  | yes      | Street address.                                                                                                                                                                                                                                                       |
| `cardholderBillingZipcode`      | string  | yes      | Postal code.                                                                                                                                                                                                                                                          |
| `cardholderBillingCity`         | string  | yes      | City.                                                                                                                                                                                                                                                                 |
| `cardholderBillingState`        | string  | yes      | Two-letter US state code.                                                                                                                                                                                                                                             |
| `savePaymentMethodForFutureUse` | boolean | no       | Default `false`. `true` saves the card and returns `customerId` and `paymentMethodId`. Sending it attests that you have the customer's consent. See [Saved payment methods](/docs/saved-payment-methods).                                                             |
| `deviceData`                    | object  | no       | Browser signals for your payment processor, validated strictly when present. See [Device data](#device-data).                                                                                                                                                         |
| `riskSessionKey`                | string  | no       | The value `riskSessionKey()` returns on the Payment SDK's [device-screening collector](/docs/payment-sdk/device-screening) (`HighWireCollect({ publishableKey, apiBase })`), forwarded unchanged (at most 100 characters). Omit it when the collector returns `null`. |

The string fields are checked for presence only, except where a rule is listed. Unknown fields are
dropped silently. A failed rule answers a
[validation error](/docs/errors-and-conventions#validation-error) naming the field.

### Device data [#device-data]

`deviceData` carries signals about the cardholder's browser for your **payment processor**, where
they improve authorization rates and reduce 3-D Secure friction.

```json
{
  "deviceData": {
    "device": {
      "ip": "203.0.113.42",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) …",
      "location": { "lat": 37.7749, "lon": -122.4194 }
    }
  }
}
```

| Path                       | Type   | Required when `deviceData` is present | Rules                                                                                                                                         |
| -------------------------- | ------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `device`                   | object | yes                                   |                                                                                                                                               |
| `device.ip`                | string | yes                                   | A well-formed IPv4 or IPv6 address, at most 45 characters. It must be the cardholder's address as seen from their browser, not your server's. |
| `device.userAgent`         | string | yes                                   | The browser's User-Agent, non-empty, at most 1024 characters.                                                                                 |
| `device.location`          | object | no                                    | `{ "lat": number, "lon": number }` in decimal degrees: `lat` from −90 to 90, `lon` from −180 to 180.                                          |
| `device.deviceFingerprint` | string | no                                    | Reserved for integrations agreed with High Wire; most integrations never set it. Letters, digits, `_` and `-`, at most 128 characters.        |

* Collect the values in the browser with the Payment SDK's
  [device-screening collector](/docs/payment-sdk/device-screening). Any extra keys it adds are
  dropped by the gateway. An embedded Payment SDK checkout sends no `deviceData`: HighWire reads
  the browser's address and location itself.
* Omit `deviceData` entirely when you have nothing to send. Never send an empty object or a
  hand-built substitute.

## Authorize and capture [#authorize-and-capture]

```http
POST /v1/payments/auth-and-capture
```

Idempotency-Key: honoured.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/auth-and-capture \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c2e9-4f9a-7c2f-9c83-1b2b3c4d5e6f" \
  -d '{
    "amount": 7200,
    "cardNumber": "5555555555554444",
    "cardExpiration": "1133",
    "cardCvv": "456",
    "cardholderName": "Jordan Example",
    "cardholderEmail": "jordan@example.com",
    "cardholderPhone": "+14155550101",
    "cardholderBillingAddress": "100 Example Street",
    "cardholderBillingZipcode": "94105",
    "cardholderBillingCity": "Springfield",
    "cardholderBillingState": "CA",
    "riskSessionKey": "01928e1c-2a4d-7c8e-8b1a-1234567890ab"
  }'
```

`201 Created`:

```json
{
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "status": "CAPTURED",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example"
}
```

| Field                           | Type    | Notes                                                                                                                                                                            |
| ------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentId`                     | string  | Persist it. Every later call about this payment (read, capture, void, refund) uses it.                                                                                           |
| `status`                        | enum    | `CAPTURED`: done. `PENDING`: the processor accepted the charge and is still settling it; poll. `REQUIRES_ACTION`: the customer must authenticate, see [3-D Secure](#3-d-secure). |
| `amount`                        | integer | Cents captured.                                                                                                                                                                  |
| `cardNumber`                    | string  | Masked.                                                                                                                                                                          |
| `cardExpiration`                | string  | `MMYY`.                                                                                                                                                                          |
| `cardholderName`                | string  | As sent.                                                                                                                                                                         |
| `customerId`, `paymentMethodId` | string  | Only when `savePaymentMethodForFutureUse` was `true`. See [Saved payment methods](/docs/saved-payment-methods).                                                                  |
| `nextAction`                    | object  | Only when `status` is `REQUIRES_ACTION`.                                                                                                                                         |

| Status       | Body                                                                  | When                                                                                                                                             |
| ------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `201`        | The response above.                                                   | Captured, pending or awaiting the customer.                                                                                                      |
| `400`        | [Validation error](/docs/errors-and-conventions#validation-error)     | A field failed its rule.                                                                                                                         |
| `400`        | [Charge decline](/docs/errors-and-conventions#charge-decline)         | Refused by a limit, by fraud screening or by the issuer. See [Declines](#declines).                                                              |
| `400`        | [Plain error](/docs/errors-and-conventions#plain-error)               | `Failed to register customer for saved payment method.`: the save flag was set and the customer could not be registered. No payment was created. |
| `401`, `403` | [Plain error](/docs/errors-and-conventions#plain-error)               | Invalid credentials or an inactive account (`401`); a refund-only account (`403`).                                                               |
| `409`, `422` | [Plain error](/docs/errors-and-conventions#plain-error)               | [Idempotency key](/docs/idempotency-key) in flight or reused with a different body.                                                              |
| `429`        | [`VELOCITY_LIMIT`](/docs/errors-and-conventions#code-carrying-errors) | A velocity check refused the charge.                                                                                                             |

## Authorize now, capture later [#authorize-now-capture-later]

```http
POST /v1/payments/auth
```

Idempotency-Key: honoured. Same request body and the same responses as above; a success settles
to `AUTHORIZED` instead of `CAPTURED`:

```json
{
  "paymentId": "01929a10-5d2e-7f3b-8c4d-1e2f3a4b5c6d",
  "status": "AUTHORIZED",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example"
}
```

The funds are held on the card. Capture them to settle, or void to release them. An authorization
that is neither captured nor voided eventually lapses at the processor and the payment becomes
`EXPIRED`; how long a hold lasts is decided by the card networks, so capture as soon as you can.

### Capture [#capture]

```http
POST /v1/payments/{id}/capture
```

Idempotency-Key: honoured. No request body. Captures the **full** authorized amount; partial
captures are not supported.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/01929a10-5d2e-7f3b-8c4d-1e2f3a4b5c6d/capture \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Idempotency-Key: 0193c2f1-2b7d-7a4e-8f10-6c7d8e9f0a1b"
```

`201 Created`:

```json
{
  "paymentId": "01929a10-5d2e-7f3b-8c4d-1e2f3a4b5c6d",
  "status": "CAPTURED",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444"
}
```

| Status                     | Body                                                                              | When                                                                                                                                                                                                                                                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`                      | The response above.                                                               | Captured.                                                                                                                                                                                                                                                                                                                         |
| `400`                      | [Plain error](/docs/errors-and-conventions#plain-error)                           | `Payment is not in a capturable state; the authorization must be successful.`, `The authorization is no longer an open hold and cannot be captured.`, `This payment has already been captured.`, `This payment is being voided and can no longer be captured.`, or `Validation failed (uuid v 7 is expected)` for a malformed id. |
| `400`                      | [Void and capture decline](/docs/errors-and-conventions#void-and-capture-decline) | The processor refused the capture.                                                                                                                                                                                                                                                                                                |
| `404`                      | [Plain error](/docs/errors-and-conventions#plain-error)                           | `Payment not found: <id>`: unknown, another account's, or a payment with no authorization to capture. A payment made with `auth-and-capture` answers `404` here, since it was captured on creation.                                                                                                                               |
| `401`, `403`, `409`, `422` | [Plain error](/docs/errors-and-conventions#plain-error)                           | As for a charge. Refund-only accounts cannot capture.                                                                                                                                                                                                                                                                             |
| `500`                      | [Plain error](/docs/errors-and-conventions#plain-error)                           | The capture was accepted but could not be finalized yet: do not retry; poll the payment.                                                                                                                                                                                                                                          |

### Void [#void]

```http
POST /v1/payments/{id}/void
```

Idempotency-Key: ignored. No request body. Cancels, for its full amount, a payment that has not
been captured: an uncaptured `AUTHORIZED` hold, a charge still `PENDING` at the processor, or a
`REQUIRES_ACTION` payment whose customer abandoned the 3-D Secure challenge. A captured payment
cannot be voided; [refund](/docs/refunds) it instead. Accounts limited to refunds and voids may call
this route.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/01929a10-5d2e-7f3b-8c4d-1e2f3a4b5c6d/void \
  -u "YOUR_API_KEY:YOUR_SECRET"
```

`201 Created`:

```json
{
  "paymentId": "01929a10-5d2e-7f3b-8c4d-1e2f3a4b5c6d",
  "status": "VOIDED",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444"
}
```

`cardNumber` is `null` for payments that carry no card number, such as wallet payments made
through a checkout.

| Status | Body                                                                              | When                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------ | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`  | The response above.                                                               | Voided.                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `400`  | [Plain error](/docs/errors-and-conventions#plain-error)                           | `Payment is not in a voidable state; only an in-flight or authorized (uncaptured) payment can be voided. Try to refund the payment instead.`, `The payment can no longer be voided. Try to refund the payment instead.`, `Payment has already been captured. Try to refund the payment instead.`, `Payment has already been voided.`, `A void is already in progress for this payment`, or `Validation failed (uuid v 7 is expected)`. |
| `400`  | [Void and capture decline](/docs/errors-and-conventions#void-and-capture-decline) | The processor refused the void.                                                                                                                                                                                                                                                                                                                                                                                                        |
| `404`  | [Plain error](/docs/errors-and-conventions#plain-error)                           | `Payment not found: <id>`: unknown, another account's, or a payment that never reached the processor (for example a hosted checkout nobody has opened).                                                                                                                                                                                                                                                                                |
| `401`  | [Plain error](/docs/errors-and-conventions#plain-error)                           | Invalid credentials, or an inactive account.                                                                                                                                                                                                                                                                                                                                                                                           |
| `500`  | [Plain error](/docs/errors-and-conventions#plain-error)                           | The void was accepted but could not be finalized yet: do not retry; poll the payment.                                                                                                                                                                                                                                                                                                                                                  |

## 3-D Secure [#3-d-secure]

3-D Secure surfacing is **enabled per account** by High Wire, together with your 3-D Secure
return page (an HTTPS page on your site the customer lands on after the challenge). Until it is
enabled, a charge whose issuer demands a challenge is declined with
`declinedReason: "Additional authentication required"`. Enable it once your integration renders
`nextAction` and polls for the outcome.

When it is enabled and the issuer requires the customer to authenticate, the charge still answers
`201`, with `status: "REQUIRES_ACTION"` and a `nextAction` instead of a settled outcome. Nothing has
been charged yet; the payment waits for the customer.

```json
{
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "status": "REQUIRES_ACTION",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example",
  "nextAction": {
    "kind": "REDIRECT",
    "url": "https://verify.example.com/3ds/s/7Qx2LwP7aZ0cT5uYbR1d",
    "method": "GET"
  }
}
```

| Field         | Notes                                                                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`        | `REDIRECT`: open `url` with a `GET`, in an iframe or by navigating the page. `REDIRECT_FORM`: post `data` to `url` as an auto-submitted HTML form. |
| `url`         | The challenge URL. Opaque and time-limited: do not parse, rewrite or store it beyond the pending window.                                           |
| `method`      | `GET` for `REDIRECT`, `POST` for `REDIRECT_FORM`.                                                                                                  |
| `data`        | `REDIRECT_FORM` only: the form fields to post.                                                                                                     |
| `fallbackUrl` | Sometimes present: an alternate URL when the primary one cannot be rendered.                                                                       |

Integration flow:

1. Render the challenge, typically an iframe whose `src` is `nextAction.url`.
2. The customer completes or fails it. The frame is then sent to your configured return page with a
   few query parameters describing the outcome. Treat them as a hint for your UI only, never as the
   settled outcome: anyone can open that page.
3. Poll [`GET /v1/payments/{id}`](/docs/payment-lifecycle#retrieve-a-payment). It keeps serving the
   pending `nextAction` until the payment settles to `CAPTURED` or `AUTHORIZED` on success,
   `DECLINED` when authentication fails (the transaction's `declinedReason` is `Authentication failed`), or
   `EXPIRED` when the challenge times out unanswered.

Notes:

* A replay under the same `Idempotency-Key` returns the same response, including the same
  challenge URL.
* An abandoned challenge can be cancelled with [void](#void), which settles the payment to
  `VOIDED`.
* When `savePaymentMethodForFutureUse` was `true`, the response carries `customerId` and
  `paymentMethodId`, but the method is a **pending save**: it becomes chargeable only once the
  payment settles successfully, and it is invalidated if the authentication fails, expires or is
  voided. See [Saved payment methods](/docs/saved-payment-methods#obtaining-the-ids).
* `charge-saved-method` never returns `REQUIRES_ACTION`; an issuer that insists on a challenge
  there declines the charge.

## Declines [#declines]

A decline is a `400` with the [charge decline](/docs/errors-and-conventions#charge-decline) body.
The payment is stored with `status: DECLINED` for your reporting. The response is stored under the
`Idempotency-Key`, so a new attempt is a new operation with a new key.

| `declinedReason`                                                                                                                                                                                                                                       | Source                                                                                                                                                            | What to do                                                                                                                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `Card declined`, `Insufficient funds`, `Card expired`, `Incorrect card details`, `Card not supported`, `Duplicate transaction`, `Processing error. Try again later.`, `Payment processing is unavailable. Please contact support.`, `Payment declined` | The card's issuer or the processor refused the charge. `declinedReason` is always one of these fixed strings.                                                     | Show the shopper a generic decline message, keep `declinedReason` for your logs, and let them try another card.                   |
| `Additional authentication required`                                                                                                                                                                                                                   | The issuer wants 3-D Secure and it could not be surfaced: 3-D Secure is not enabled on the account, or the route cannot show a challenge (`charge-saved-method`). | Enable 3-D Secure surfacing, or collect the card through the Payment SDK or the hosted checkout, which run the challenge for you. |
| `Amount exceeds per-transaction limit`, `Amount exceeds daily limit`, `Amount exceeds monthly limit`                                                                                                                                                   | Your [merchant limits](/docs/account-limits#merchant-limits), checked before the processor is contacted.                                                          | Ask High Wire to raise the cap ahead of time.                                                                                     |
| `Transaction flagged as high risk.`                                                                                                                                                                                                                    | The charge was refused; no money moved.                                                                                                                           | Handle it like any other decline.                                                                                                 |
| `auth-and-capture is not available for your account. Please contact support.`, `auth is not available for your account. Please contact support.`                                                                                                       | Your account cannot use the direct card routes.                                                                                                                   | Use the Payment SDK or the hosted checkout.                                                                                       |

A charge the processor accepted is never answered with a decline: it answers `201` with
`status: "PENDING"` and settles shortly after. Do not retry a `PENDING` payment; poll it.


# Errors and conventions (/docs/errors-and-conventions)



This page covers what is common to every endpoint: request conventions, status codes, the exact
JSON shapes the API answers with, and what to do on each. The endpoint guides link here instead of
repeating the shapes.

## Conventions [#conventions]

| Topic                     | Rule                                                                                                                                                                                                     |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base URL                  | `https://api-payments-staging.highwirepayments.com/v1` on staging, `https://api-payments.highwirepayments.com/v1` in production. See [Environments](/docs/introduction#environments).                    |
| Authentication            | HTTP Basic on every request. See [Authentication](/docs/authentication).                                                                                                                                 |
| Format                    | JSON request and response bodies. Send `Content-Type: application/json`. A request body is limited to 100 kB.                                                                                            |
| Success status            | Every `POST` answers `201 Created`, including void, capture and link reissue. `GET` answers `200 OK`.                                                                                                    |
| Amounts                   | Integers in cents, the smallest unit of the currency: `7200` is $72.00. Never decimals, never strings.                                                                                                   |
| Currency                  | USD only. Card charges carry no currency field, the hosted checkout requires `"usd"`, and a retrieved payment always reports `"usd"`.                                                                    |
| Identifiers               | Payment, refund, customer and payment-method ids are UUID v7 strings. Routes that take a payment id in the path answer `400` for anything that is not a UUID v7; the refund route answers `404` instead. |
| Card numbers in responses | Masked as the first six digits, eight `x` and the last four: `555555xxxxxxxx4444`. A card number shorter than 16 digits shows only its last four. The full number is never echoed.                       |
| Timestamps                | ISO 8601 in UTC with milliseconds, for example `2026-10-19T14:05:00.000Z`.                                                                                                                               |
| Unknown fields            | Silently dropped, never rejected. A misspelled optional field is not an error, so check the response rather than relying on a rejection.                                                                 |
| Types                     | Validated strictly. A number sent as a string (`"7200"`) fails validation.                                                                                                                               |
| Optional fields           | Omit the ones you do not use. The hosted checkout also accepts `null` as "absent".                                                                                                                       |
| Versioning                | `/v1` is the current stable version. Future versions will be served alongside it, with notice before any deprecation.                                                                                    |

## HTTP status codes [#http-status-codes]

| Status | Meaning                                                                                                                                                                                                    | Body                                                                                                                                                                                                                                             |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200`  | A read succeeded.                                                                                                                                                                                          | The payment, see [Payment lifecycle](/docs/payment-lifecycle).                                                                                                                                                                                   |
| `201`  | A write succeeded. For a charge this includes `PENDING` (accepted, settling) and `REQUIRES_ACTION` (the customer must authenticate).                                                                       | The endpoint's response object.                                                                                                                                                                                                                  |
| `400`  | Invalid input, a declined charge, refund, void or capture, a business rule, or a feature not enabled for the account.                                                                                      | [Validation error](#validation-error), [Charge decline](#charge-decline), [Refund decline](#refund-decline), [Void and capture decline](#void-and-capture-decline), [Plain error](#plain-error) or [Code-carrying error](#code-carrying-errors). |
| `401`  | Missing or invalid credentials, or an inactive account.                                                                                                                                                    | [Plain error](#plain-error). See [Authentication](/docs/authentication#error-responses).                                                                                                                                                         |
| `403`  | The account is limited to refunds and voids and called another route.                                                                                                                                      | [Plain error](#plain-error).                                                                                                                                                                                                                     |
| `404`  | Unknown id, another account's resource, or a payment that is not in a state the route can act on.                                                                                                          | [Plain error](#plain-error). On `charge-saved-method` the body is a [Charge decline](#charge-decline).                                                                                                                                           |
| `409`  | A rule refused the call before anything was charged, a hosted checkout link cannot be reissued, a request with the same `Idempotency-Key` is still in flight, or no account of yours can take the payment. | [Reason-coded conflict](#reason-coded-conflict), [Plain error](#plain-error) or [Code-carrying error](#code-carrying-errors).                                                                                                                    |
| `422`  | The `Idempotency-Key` was reused with a different request body.                                                                                                                                            | [Plain error](#plain-error).                                                                                                                                                                                                                     |
| `429`  | A velocity check refused a card charge.                                                                                                                                                                    | [Code-carrying error](#code-carrying-errors) with `code: "VELOCITY_LIMIT"`.                                                                                                                                                                      |
| `5xx`  | The gateway failed.                                                                                                                                                                                        | [Plain error](#plain-error). Retry with the same `Idempotency-Key`, unless the operation was accepted; see [Server errors](#server-errors).                                                                                                      |

## Response body shapes [#response-body-shapes]

### Validation error [#validation-error]

`400`, returned when a required field is missing or a value is malformed. `fieldErrors` maps each
offending field path to its problems. Nested paths use dots and array indexes are numbers:
`deviceData.device.ip`, `lineItems.0.name`.

```json
{
  "statusCode": 400,
  "message": "Some fields are invalid, please check the errors",
  "fieldErrors": {
    "amount": ["amount must not be less than 1", "amount must be an integer number"],
    "cardNumber": ["cardNumber must be 12-19 digits with no spaces or separators"]
  }
}
```

A validation error is stored under the `Idempotency-Key` like any other `4xx`: fix the body and
send it under a new key, otherwise the corrected request answers `422`.

### Plain error [#plain-error]

The default shape for everything that is neither a decline nor a validation failure: `401`, `403`,
`404`, business-rule `400`s, malformed ids, idempotency `409`/`422` and `5xx`.

```json
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Payment not found: 01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b"
}
```

`message` is human-readable. It is stable enough to log and to show to an operator, but branch on
the status code and the route, not on the text. Messages you will meet:

| Status | `message`                                                                                                                                    | Where                                                                                                                   |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `400`  | `Validation failed (uuid v 7 is expected)`                                                                                                   | A payment id in the path that is not a UUID v7.                                                                         |
| `400`  | `Idempotency-Key must be 1..255 characters`, `Idempotency-Key header may only appear once per request`                                       | Any idempotent route.                                                                                                   |
| `400`  | `Payment is not in a voidable state; only an in-flight or authorized (uncaptured) payment can be voided. Try to refund the payment instead.` | [Void](/docs/card-payments#void).                                                                                       |
| `400`  | `This payment has already been captured.`                                                                                                    | [Capture](/docs/card-payments#capture).                                                                                 |
| `400`  | `Payment has already been fully refunded`, `A refund is already in progress for this payment`                                                | [Refunds](/docs/refunds).                                                                                               |
| `401`  | `Missing Authorization header`, `Invalid credentials`                                                                                        | [Authentication](/docs/authentication#error-responses).                                                                 |
| `403`  | `Merchant account can only process refunds and voids`                                                                                        | [Authentication](/docs/authentication#error-responses).                                                                 |
| `404`  | `Payment not found: <id>`, `Payment not found or not refundable: <id>`                                                                       | Unknown payment, another account's, or (on refund and capture) a payment without a successful capture or authorization. |
| `409`  | `A request with this Idempotency-Key is already in progress`                                                                                 | [Idempotency key](/docs/idempotency-key).                                                                               |
| `409`  | `The checkout has expired; create a new one`                                                                                                 | [Hosted checkout](/docs/hosted-checkout#reissue-a-link).                                                                |
| `422`  | `Idempotency-Key was previously used with a different request body`                                                                          | [Idempotency key](/docs/idempotency-key).                                                                               |
| `500`  | `Internal server error`                                                                                                                      | Unexpected failure.                                                                                                     |

### Charge decline [#charge-decline]

`400` from `auth-and-capture`, `auth` and `charge-saved-method`. The charge was well-formed and was
refused: by your account limits, by fraud screening, or by the card's issuer. There is &#x2A;*no
`statusCode`** in this body; the HTTP status carries it. The payment is recorded with
`status: DECLINED` so it appears in your reporting, but a declined direct charge does not return
its `paymentId`.

```json
{
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example",
  "status": "DECLINED",
  "declinedReason": "Card declined"
}
```

| Field                                        | Notes                                                                                                                                                          |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cardholderName`                             | Only on the raw-card routes.                                                                                                                                   |
| `paymentId`, `customerId`, `paymentMethodId` | Only on `charge-saved-method` declines, where the declined attempt counts toward the card's attempt limit.                                                     |
| `declinedReason`                             | One of a fixed set of short reasons, listed under [Declines](/docs/card-payments#declines). Log it and decide on `status`; show the shopper a generic message. |

`charge-saved-method` also uses this shape, without card fields, for an unknown payment method
(`404` with `declinedReason: "Payment method not found."`) and for ownership problems (`400`), see
[Saved payment methods](/docs/saved-payment-methods#ownership-and-state-errors).

### Refund decline [#refund-decline]

`400` from `refund` when the processor refused the refund or your daily refund limit blocked it.
`status` is the **refund's** status, not the payment's.

```json
{
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "amount": 500,
  "cardNumber": "555555xxxxxxxx4444",
  "status": "FAILED",
  "declinedReason": "Amount exceeds daily refund limit"
}
```

`cardNumber` is `null` for wallet payments, which carry no card number.
`status` is `CANCELED` only in the rare case where the refund was already cancelled on the
processor's side before the call finished; no money moved either way.

### Void and capture decline [#void-and-capture-decline]

`400` from `void` and `capture` when the processor refused the operation. Here `status: "DECLINED"`
describes the attempt, not the payment: read the payment to see where it stands.

```json
{
  "paymentId": "01929a10-5d2e-7f3b-8c4d-1e2f3a4b5c6d",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "status": "DECLINED",
  "declinedReason": "Capture declined"
}
```

### Reason-coded conflict [#reason-coded-conflict]

`409` with a machine-readable `reason`. The rule fired before anything was charged or recorded, and
the refusal is **not stored** under the `Idempotency-Key`, so the same key can be sent again once
the condition clears.

```json
{
  "statusCode": 409,
  "message": "This saved payment method has reached its reuse limit.",
  "reason": "REUSE_LIMIT"
}
```

| `reason`                                                                                                                             | Route                 | Meaning                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | -------------------------------------------------------------------------------------------------- |
| `ORIGINAL_NOT_SETTLED`, `METHOD_NOT_READY`, `ORIGINAL_NOT_ELIGIBLE`, `WINDOW_CLOSED`, `REUSE_LIMIT`, `ATTEMPT_LIMIT`, `AMOUNT_LIMIT` | `charge-saved-method` | The reuse is outside its bounds. See [Reuse refusals](/docs/saved-payment-methods#reuse-refusals). |
| `PAYMENT_DISPUTED`                                                                                                                   | `refund`              | The payment has an open or lost dispute. See [Refunds and disputes](/docs/refunds#disputes).       |

A `409` **without** `reason` is a plain error: an `Idempotency-Key` still in flight, or a hosted
checkout link that cannot be reissued.

### Code-carrying errors [#code-carrying-errors]

These errors carry a stable `code`.

`HW_UNSUPPORTED` (`400`): the hosted checkout is not enabled for your account. Nothing is written.

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "code": "HW_UNSUPPORTED",
  "message": "Hosted checkout is not enabled for this account"
}
```

`HW_NO_ACTIVE_PROVIDER_ACCOUNT` (`409`): no account of yours takes new payments. On a payment that
already exists, `HW_PROVIDER_ACCOUNT_REFUND_ONLY`, `HW_PROVIDER_ACCOUNT_RESTRICTED`,
`HW_PROVIDER_ACCOUNT_DISABLED` or `HW_PROVIDER_ACCOUNT_UNRESOLVED` (`409`): the account it was
created on no longer takes payments. Neither is stored under an `Idempotency-Key`, so the same key
can be retried once High Wire has resolved the account.

```json
{
  "statusCode": 409,
  "error": "Conflict",
  "code": "HW_NO_ACTIVE_PROVIDER_ACCOUNT",
  "message": "No active provider account can take this payment."
}
```

`VELOCITY_LIMIT` (`429`): a velocity check refused a card charge. Nothing is charged and no payment
is recorded. There is no `Retry-After` header. See [Account limits](/docs/account-limits#velocity-checks).

```json
{
  "statusCode": 429,
  "message": "Velocity limit exceeded",
  "code": "VELOCITY_LIMIT"
}
```

### Server errors [#server-errors]

A `5xx` is a [plain error](#plain-error). The idempotency record for the call is discarded, so
&#x2A;*retry with the same `Idempotency-Key`**; a retry under the same key cannot charge twice. A retry
can answer a plain `400` such as `This payment has already been captured.` when the first attempt
did go through; read the payment to confirm.

A `500` whose message says the operation was accepted (`Capture accepted; finalization pending.
Poll the payment.`, and the same for a void or a refund) means exactly that: do not retry; poll the
payment until it reflects the operation.

## Telling the shapes apart [#telling-the-shapes-apart]

Check the body in this order; the first match wins.

1. `fieldErrors` present: a [validation error](#validation-error).
2. `declinedReason` present: a decline. `status` says which kind: `DECLINED` for a charge, void or
   capture; `FAILED` or `CANCELED` for a refund.
3. `reason` present: a [reason-coded conflict](#reason-coded-conflict).
4. `code` present: a [code-carrying error](#code-carrying-errors).
5. Otherwise a [plain error](#plain-error); use `statusCode`.

## Retrying [#retrying]

| Situation                                      | What to do                                                                                                                                                            |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Network timeout or connection error on a write | Retry with the **same** `Idempotency-Key` and body.                                                                                                                   |
| `5xx`                                          | Retry with the same key; then read the payment. If the message says the operation was accepted, do not retry: poll the payment. See [Server errors](#server-errors).  |
| `409` without `reason`                         | The first call is still running. Retry the same key after a moment to get its outcome.                                                                                |
| `409` with `reason`                            | Act on the reason. Some clear on their own (`ORIGINAL_NOT_SETTLED`, `METHOD_NOT_READY`, `PAYMENT_DISPUTED` once won); the others need a new card or a smaller amount. |
| `422`                                          | A client bug: the key was reused for a different operation. Use a new key for the new operation.                                                                      |
| `429`                                          | Stored under the key. Do not retry the same request; a new attempt needs a new key, later.                                                                            |
| `400` decline                                  | Stored under the key. A new attempt is a new operation with a new key.                                                                                                |
| `400` validation                               | Stored under the key. Fix the body and use a new key.                                                                                                                 |
| `401`, `403`                                   | Fix the credentials or contact High Wire; nothing was stored.                                                                                                         |
| `201` with `status: PENDING`                   | Do not retry. Poll the payment; see [Polling](/docs/payment-lifecycle#polling-for-an-outcome).                                                                        |


# Hosted checkout (/docs/hosted-checkout)



Use the hosted checkout when you want neither card data nor a payment page on your side. Your
server sends the cart and receives a **link**; the shopper pays on a page High Wire serves. Both
calls are server-to-server under your Basic credentials: no publishable key and no browser
integration.

```http
POST /v1/payments/checkout-session
POST /v1/payments/{id}/checkout-session/link
```

**Availability.** The hosted checkout must be enabled on your account. Where it is not, the create
call answers `400` with `code: "HW_UNSUPPORTED"` and writes nothing.

## How it works [#how-it-works]

1. **Your server creates the checkout** and receives `paymentId` and `checkoutUrl`.
2. **You hand the link to the shopper**: a redirect, an e-mail, a QR code, a chat message. It works
   on any device.
3. **The shopper opens the page and pays.** The page shows the order and your account name; a
   declined card can be retried on the same page.
4. **Once the payment is captured** the page sends the shopper to your `successUrl`. Confirm the
   payment on your server before you fulfil the order, see [Return URLs](#return-urls).

## Create a checkout [#create-a-checkout]

```http
POST /v1/payments/checkout-session
```

Idempotency-Key: honoured. A replay answers the same body **without** `checkoutUrl`, see
[The link](#the-link).

| Field                           | Type    | Required | Rules                                                                                                                                                                                                                                                       |
| ------------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lineItems`                     | array   | yes      | 1 to 100 items.                                                                                                                                                                                                                                             |
| `lineItems[].name`              | string  | yes      | 1 to 200 characters. Shown to the shopper.                                                                                                                                                                                                                  |
| `lineItems[].amount`            | integer | yes      | Unit price in cents, at least `1`.                                                                                                                                                                                                                          |
| `lineItems[].quantity`          | integer | yes      | At least `1`.                                                                                                                                                                                                                                               |
| `shippingAmount`                | integer | no       | Cents, at least `0`. Default `0`.                                                                                                                                                                                                                           |
| `taxAmount`                     | integer | no       | Cents, at least `0`. Default `0`.                                                                                                                                                                                                                           |
| `discountAmount`                | integer | no       | Cents, at least `0`, subtracted from the total. Default `0`.                                                                                                                                                                                                |
| `amount`                        | integer | no       | Optional cross-check in cents. The gateway computes the total itself; a value that differs answers `400`.                                                                                                                                                   |
| `currency`                      | string  | yes      | `usd`, case-insensitive. The only currency served.                                                                                                                                                                                                          |
| `customerEmail`                 | string  | no       | A valid e-mail. The page shows it masked only. On most accounts the shopper may leave the page's e-mail field blank to use it; see [What the shopper sees](#what-the-shopper-sees).                                                                         |
| `successUrl`                    | string  | yes      | Where the page sends the shopper once the payment is captured. An absolute `https` URL (`http` only on `localhost`, `127.0.0.1` or `[::1]`), at most 2048 characters.                                                                                       |
| `cancelUrl`                     | string  | yes      | Where the page's "back to the store" link leads a shopper who leaves without paying. Same rules as `successUrl`.                                                                                                                                            |
| `savePaymentMethodForFutureUse` | boolean | yes      | `true` shows the shopper an unticked save-card box with the card form; the card is saved only if they tick it and pay by card. `false` shows no box and saves nothing. No default. Fixed for the life of the link, see [Saving the card](#saving-the-card). |
| `expiresInMinutes`              | integer | no       | How long the link stays payable: `30` to `1440`. Default `1440` (24 hours).                                                                                                                                                                                 |
| `referenceId`                   | string  | no       | Your own reference for the order, 1 to 128 characters.                                                                                                                                                                                                      |
| `metadata`                      | object  | no       | Any JSON object of at most 4096 bytes. Stored with the payment exactly as sent.                                                                                                                                                                             |

An optional field sent as `null` is treated as absent. `referenceId` and `metadata` are stored but
**not returned** by the API today, so correlate the checkout with your order by `paymentId`.
`lineItems[].name`, `referenceId` and `metadata` are stored and shown back (item names to the
shopper): never put card numbers, security codes or other sensitive data in them.

**The total** is computed on the server, in integer cents: the line items (`amount × quantity`)
plus `shippingAmount` and `taxAmount`, minus `discountAmount`. It must be between `2` and
`2147483647`. Anything else answers a plain `400` that states the computed total.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/checkout-session \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c2e9-4f9a-7c2f-9c83-1b2b3c4d5e6f" \
  -d '{
    "lineItems": [
      { "name": "Trail Runner (size 42)", "amount": 2500, "quantity": 2 },
      { "name": "Merino socks", "amount": 1500, "quantity": 1 }
    ],
    "shippingAmount": 500,
    "taxAmount": 600,
    "discountAmount": 1000,
    "amount": 6600,
    "currency": "usd",
    "customerEmail": "jordan@example.com",
    "successUrl": "https://merchant.example.com/order/complete",
    "cancelUrl": "https://merchant.example.com/cart",
    "savePaymentMethodForFutureUse": false,
    "expiresInMinutes": 1440,
    "referenceId": "order-1042",
    "metadata": { "cartId": "c_123" }
  }'
```

`201 Created`:

```json
{
  "paymentId": "0192a0c4-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "status": "PENDING",
  "amount": 6600,
  "currency": "usd",
  "checkoutUrl": "https://checkout-staging.highwirepayments.com/0192a0c4-1b2c-7d3e-9f4a-5b6c7d8e9f0a#hw_hc_…",
  "expiresAt": "2026-10-20T14:05:00.000Z"
}
```

| Field         | Type    | Notes                                                                            |
| ------------- | ------- | -------------------------------------------------------------------------------- |
| `paymentId`   | string  | Persist it: you confirm the payment, reissue the link and refund with it.        |
| `status`      | enum    | Always `PENDING` at creation.                                                    |
| `amount`      | integer | The total in cents, as computed above.                                           |
| `currency`    | string  | `usd`.                                                                           |
| `checkoutUrl` | string  | The link to hand the shopper. **Returned once**; absent on an idempotent replay. |
| `expiresAt`   | string  | When the link stops being payable, ISO 8601.                                     |

The checkout host is `checkout-staging.highwirepayments.com` on staging and
`checkout.highwirepayments.com` in production. Hand the URL over exactly as returned: do not parse
it, rebuild it or shorten it.

| Status       | Body                                                                                 | When                                                                                                                                                                                                     |
| ------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`        | The response above.                                                                  | Created.                                                                                                                                                                                                 |
| `400`        | [Validation error](/docs/errors-and-conventions#validation-error)                    | A field failed its rule, including a `currency` other than `usd`, and `savePaymentMethodForFutureUse: true` on an account whose hosted page cannot save cards (see [Saving the card](#saving-the-card)). |
| `400`        | [Plain error](/docs/errors-and-conventions#plain-error)                              | The total disagrees with `amount`, or is out of range.                                                                                                                                                   |
| `400`        | [`HW_UNSUPPORTED`](/docs/errors-and-conventions#code-carrying-errors)                | The hosted checkout is not enabled on your account.                                                                                                                                                      |
| `401`        | [Plain error](/docs/errors-and-conventions#plain-error)                              | Invalid credentials or a disabled account.                                                                                                                                                               |
| `409`, `422` | [Plain error](/docs/errors-and-conventions#plain-error)                              | [Idempotency key](/docs/idempotency-key) in flight or reused with a different body.                                                                                                                      |
| `409`        | [`HW_NO_ACTIVE_PROVIDER_ACCOUNT`](/docs/errors-and-conventions#code-carrying-errors) | No account of yours takes new payments. Not stored under the `Idempotency-Key`, so the same key can be retried once High Wire has resolved it.                                                           |

## The link [#the-link]

* **It is shown once.** A replay of the create call under the same `Idempotency-Key` returns the
  same body without `checkoutUrl`.
* **Send it to the shopper and nowhere else.** Whoever has the link can pay the checkout, so keep
  it out of logs and analytics.
* **Reissue it on demand.** If you lost the link, or it may have reached someone it should not
  have, ask for a new one. The previous link stops working at once; only a payment already in
  progress on the old page (a wallet confirmation, for example) can still complete.

## Saving the card [#saving-the-card]

With `savePaymentMethodForFutureUse: true` the page shows an unticked "Save this card for future
purchases" box with the card form. The card is saved only if the shopper ticks it and pays by card.

* **Wallets never save.** A payment with Apple Pay or Google Pay saves nothing, ticked or not.
* **Whose card.** It is saved for the e-mail the payer used on the page.
* **What it allows.** The bounds of every [saved card](/docs/saved-payment-methods): charged again
  only with the customer present, within 24 hours, at most twice, each time for at most the amount
  of the payment that first saved the card: this checkout's total, unless the card was already
  saved. Saving it again opens nothing new. It never allows charging the customer later without
  them.
* **The ids.** No response returns `customerId` or `paymentMethodId`: not the create, not the page,
  not [`GET /v1/payments/{id}`](/docs/payment-lifecycle#retrieve-a-payment). Whoever holds the link
  could charge the card with them. The saved card is reported through the reconciliation channels
  set up for your account with High Wire at onboarding.
* **Fixed for the link.** A [reissued link](#reissue-a-link) keeps the setting; to change it,
  create a new checkout. The same `Idempotency-Key` sent with the other value answers `422`.
* **Not every account can save.** Where your account's hosted page cannot save cards, the create
  answers `400` and writes nothing. If your account's page changes after the link was created, the
  page shows no box.

```json
{
  "statusCode": 400,
  "message": "Saving cards is not available on this account's hosted checkout",
  "fieldErrors": {
    "savePaymentMethodForFutureUse": [
      "Saving cards is not available on this account's hosted checkout"
    ]
  }
}
```

## Reissue a link [#reissue-a-link]

```http
POST /v1/payments/{id}/checkout-session/link
```

Idempotency-Key: ignored. No request body. Every call issues a new link and revokes the previous
one. The expiry does not move.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/0192a0c4-1b2c-7d3e-9f4a-5b6c7d8e9f0a/checkout-session/link \
  -u "YOUR_API_KEY:YOUR_SECRET"
```

`201 Created`:

```json
{
  "paymentId": "0192a0c4-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "checkoutUrl": "https://checkout-staging.highwirepayments.com/0192a0c4-1b2c-7d3e-9f4a-5b6c7d8e9f0a#hw_hc_…",
  "expiresAt": "2026-10-20T14:05:00.000Z"
}
```

It works while the checkout can still be paid and has not expired: `PENDING`, `REQUIRES_ACTION`, or
`DECLINED` with a card attempt left. Anything else answers `409`.

| Status | Body                                                                         | When                                                                                                                                                                                                                                                           |
| ------ | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`  | The response above.                                                          | A new link was issued; the old one is revoked.                                                                                                                                                                                                                 |
| `400`  | [Plain error](/docs/errors-and-conventions#plain-error)                      | `Validation failed (uuid v 7 is expected)`: malformed id.                                                                                                                                                                                                      |
| `404`  | [Plain error](/docs/errors-and-conventions#plain-error)                      | `Payment not found: <id>`: unknown, another account's, or not a hosted checkout.                                                                                                                                                                               |
| `409`  | [Plain error](/docs/errors-and-conventions#plain-error)                      | `The checkout has expired; create a new one`; `The checkout can no longer be paid (payment CAPTURED); create a new one` (the status in parentheses is the payment's); or `The checkout changed while its link was being reissued; read the payment and retry`. |
| `409`  | [`HW_PROVIDER_ACCOUNT_*`](/docs/errors-and-conventions#code-carrying-errors) | The account the checkout was created on no longer takes payments.                                                                                                                                                                                              |
| `401`  | [Plain error](/docs/errors-and-conventions#plain-error)                      | Invalid credentials or a disabled account.                                                                                                                                                                                                                     |

## What the shopper sees [#what-the-shopper-sees]

* **The order**: the line items, the subtotal, shipping, tax, discount and total, and your
  account's name. The e-mail you sent as `customerEmail` appears masked (`j***@e***.com`).
* **The payment form**: a secure card field. Wallet buttons may also be offered depending on your
  account's configuration and the shopper's device; confirm with High Wire before relying on them.
* **The payer's details**: on most accounts, an e-mail, the name on the card, and, for a card payment, the billing
  address: country, street address and city, plus the state and the postal code in the United
  States and Canada. You do not send these; the page points out a missing or malformed field next
  to it. The e-mail is optional when you sent `customerEmail` and a blank one means yours; otherwise
  it is required. On some accounts the page always asks for it: if you sent `customerEmail`, the
  shopper must type that address (in any case); if you did not and the payment page refuses the
  address, the shopper can enter another, up to three per checkout. An address refused by fraud
  screening declines the checkout for good.
* **The save box**: an unticked "Save this card for future purchases" box when you created the
  checkout with `savePaymentMethodForFutureUse: true`, see [Saving the card](#saving-the-card).
* **Declines**: a declined card can be retried on the same page, up to five card attempts per
  checkout.
* **3-D Secure**: when [3-D Secure surfacing](/docs/card-payments#3-d-secure) is enabled on your
  account and the issuer asks for it, a verification dialog opens on the page. Without it, such a
  card is declined and the shopper can try another.
* **Later visits**: a link reopened after the payment was captured says so and offers the way back
  to your store; an expired unpaid link says it has expired. On some accounts, a link created
  before an upgrade of the page says it is no longer supported: read the payment, and once it is
  reconciled, create a new checkout for the shopper.

## Return URLs [#return-urls]

When the payment is captured, the page sends the shopper to `successUrl` with the payment id added
as a query parameter, `paymentId=<paymentId>`. If your URL contains the literal `{PAYMENT_ID}`, that
placeholder is replaced with the id as well. `cancelUrl` is where the page's "back to the store"
link leads, exactly as you sent it.

**The return is not proof of payment.** Anyone can open your success URL. Before you fulfil the
order, read the payment on your server with
[`GET /v1/payments/{id}`](/docs/payment-lifecycle#retrieve-a-payment) and check that `status` is
`CAPTURED` and `amount` is the total you expect.

## Statuses [#statuses]

| `status`                         | For a hosted checkout                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PENDING`                        | Created and not paid yet, opened or not.                                                                                                                                                                                                                                                                                                                                                                           |
| `REQUIRES_ACTION`                | A card attempt is waiting on a 3-D Secure verification; the shopper can finish it from the link.                                                                                                                                                                                                                                                                                                                   |
| `DECLINED`                       | The latest attempt was declined. **Not necessarily final**: the shopper can pay again while attempts remain and the link is live, and the same payment moves on to `CAPTURED`. A decline when the checkout was first used, by one of your limits or by fraud screening, is final. On some accounts a `DECLINED` payment can still settle `CAPTURED` later, so a decline alone does not make collecting again safe. |
| `CAPTURED`                       | Paid. From here the payment is refunded like any other.                                                                                                                                                                                                                                                                                                                                                            |
| `PARTIALLY_REFUNDED`, `REFUNDED` | A captured checkout you then refunded, in part or in full.                                                                                                                                                                                                                                                                                                                                                         |
| `EXPIRED`                        | Ended unpaid once the link expired. Allow up to an hour past `expiresAt`; on some accounts a checkout with an attempt still being reconciled stays unresolved longer.                                                                                                                                                                                                                                              |

`AUTHORIZED` and `VOIDED` do not arise from the page's own flow, which captures in one step.
`CAPTURED` is the status that means the payment succeeded. The payment's
[`transactions`](/docs/payment-lifecycle#items-in-transactions) carry each attempt and its
`declinedReason`.

## Expiry [#expiry]

A link stays payable until `expiresAt` (`expiresInMinutes` after creation, 24 hours by default) or
until it is paid or finally declined, whichever comes first. Reissuing the link does not extend it. A checkout that is
not paid in time becomes `EXPIRED`; create a new one if the shopper still wants to pay.


# Idempotency key (/docs/idempotency-key)



Send an `Idempotency-Key` header on every call that creates or moves money. If the connection drops
before you read the answer, you resend the same request with the same key and get the original
outcome back instead of a second charge or refund.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/auth-and-capture \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c2e9-4f9a-7c2f-9c83-1b2b3c4d5e6f" \
  -d '{
    "amount": 7200,
    "cardNumber": "5555555555554444",
    "cardExpiration": "1133",
    "cardCvv": "456",
    "cardholderName": "Jordan Example",
    "cardholderEmail": "jordan@example.com",
    "cardholderPhone": "+14155550101",
    "cardholderBillingAddress": "100 Example Street",
    "cardholderBillingZipcode": "94105",
    "cardholderBillingCity": "Springfield",
    "cardholderBillingState": "CA"
  }'
```

Sending that request again with the same key and body, within 24 hours, replays the original
response byte for byte: same status, same `paymentId`.

## The header [#the-header]

* Name: `Idempotency-Key`. Optional, but strongly recommended on every write that accepts it.
* Value: 1 to 255 characters, any string. A fresh UUID (v4 or v7) per logical operation is the
  recommended format. There is no character-set rule.
* Send it once. A request carrying the header twice answers `400`
  `Idempotency-Key header may only appear once per request`; a value longer than 255 characters
  answers `400` `Idempotency-Key must be 1..255 characters`.
* Scope: your account, the endpoint and the path parameters together. The same key string on
  `POST /v1/payments/{a}/refund` and `POST /v1/payments/{b}/refund` is two different operations,
  and another account's keys never interfere with yours.
* Lifetime: 24 hours from the first call. After that the same string can be used for a new
  operation.
* Never put card data, e-mails or other personal identifiers in the key. It may appear in logs and
  support tools.

## Which endpoints honour it [#which-endpoints-honour-it]

| Endpoint                                       | `Idempotency-Key`                                                                                                                                        |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/payments/auth-and-capture`           | Honoured.                                                                                                                                                |
| `POST /v1/payments/auth`                       | Honoured.                                                                                                                                                |
| `POST /v1/payments/charge-saved-method`        | Honoured. A `409` refusal with a `reason` is not stored, so the same key is evaluated again next time.                                                   |
| `POST /v1/payments/checkout-session`           | Honoured. A replay answers **without** `checkoutUrl`: the link is shown once. See [The link](/docs/hosted-checkout#the-link).                            |
| `POST /v1/payments/{id}/checkout-session/link` | Ignored. Every call issues a new link and revokes the previous one.                                                                                      |
| `POST /v1/payments/{id}/capture`               | Honoured.                                                                                                                                                |
| `POST /v1/payments/{id}/refund`                | Honoured. The `409` for a disputed payment is not stored.                                                                                                |
| `POST /v1/payments/{id}/void`                  | Ignored. A void is a state transition: a second void of the same payment answers `400` `Payment has already been voided.`, and nothing is charged twice. |
| `GET /v1/payments/{id}`                        | Not applicable.                                                                                                                                          |

## What a repeated call gets [#what-a-repeated-call-gets]

| Situation                                      | Response                                                                                                                                                                      |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| First call with this key                       | Processed normally. The response, status included, is stored for 24 hours.                                                                                                    |
| Same key, same body, first call finished       | The stored response, verbatim. The processor is not called again. Nothing marks it as a replay.                                                                               |
| Same key, different body                       | `422` `Idempotency-Key was previously used with a different request body`. Almost always a client bug.                                                                        |
| Same key while the first call is still running | `409` `A request with this Idempotency-Key is already in progress`. Wait, then resend the same key to get the outcome. Do not switch to a new key until you know the outcome. |
| First call answered a `4xx`                    | The `4xx` is stored too, validation errors and `429` included. A corrected or new attempt needs a **new** key.                                                                |
| First call answered a `409` with a `reason`    | Not stored. The same key is evaluated again on the next call.                                                                                                                 |
| First call answered a `5xx`                    | The record is discarded. Retry with the same key, unless the operation was accepted; see [After a 5xx](#after-a-5xx).                                                         |
| First call answered `401` or `403`             | Nothing is stored: authentication runs before the key is read.                                                                                                                |

## After a 5xx [#after-a-5xx]

A `500` that says the operation was accepted is the exception: `Capture accepted; finalization
pending. Poll the payment.`, and the same message for a void or a refund. The processor has
already taken the operation and only its recording is late. Do not retry it;
[poll the payment](/docs/payment-lifecycle#polling-for-an-outcome) until it reflects the operation.
A retry would only answer a plain `400` such as `This payment has already been captured.`

For any other `5xx`:

1. Retry with the **same** key and body. The record was discarded, so the retry is processed, and
   a retry under the same key cannot create a duplicate charge: if the first attempt went through,
   you get that outcome back.
2. Expect that the retry may answer a plain `400` such as `This payment has already been captured.`
   or `A refund is already in progress for this payment` when the first attempt went through but
   could not be recorded. Read the payment with
   [`GET /v1/payments/{id}`](/docs/payment-lifecycle#retrieve-a-payment) to confirm.
3. If retries keep failing, treat it as an outage and contact High Wire with the key and a
   timestamp so the operation can be reconciled. Do not move to a fresh key: a fresh key has no
   relation to the first attempt and can charge twice.

## Choosing a key [#choosing-a-key]

* One key per logical operation: one per charge, one per refund of a given payment, one per
  checkout. Reuse it only to retry that same operation.
* Generate the key when the operation starts and keep it, with the request, in memory for the
  retry loop only. Never write card data (number, expiry, security code) to disk, a queue or a log
  in order to retry later. If your process is lost mid-request, read the payment back; if nothing
  was recorded, collect the card again.
* Composite keys such as `order-8812-refund-1` work, but they make accidental reuse across
  operations easy, and that reuse answers `422` or, worse, replays an unrelated stored response.


# Introduction (/docs/introduction)



The High Wire **merchant API** is a payments gateway. From your server you authorize, capture, void
and refund card payments, charge saved cards again, create hosted checkout links, and read any
payment back with its full history. Every call is a JSON request over HTTPS, authenticated with
[HTTP Basic](/docs/authentication), under `/v1`.

This site has two halves. The **guides** (this section) explain each flow with field tables and
complete examples, and are the place to start. The **API reference** is generated from the
OpenAPI document and lists every endpoint and schema; the same document is served at
[`/openapi/gateway-payments.json`](/openapi/gateway-payments.json), and plain-text versions of
these pages at [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt).

## Three ways to take a payment [#three-ways-to-take-a-payment]

| Surface                 | Where the card is entered                                                                                                                             | Guide                                    |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| **Direct card charges** | On your server. You send the card number and charge it in one call, or authorize now and capture later. For environments allowed to handle card data. | [Card payments](/docs/card-payments)     |
| **Hosted checkout**     | On a page High Wire hosts. Your server sends the cart, receives a link, and hands it to the shopper. No card data and no browser code on your side.   | [Hosted checkout](/docs/hosted-checkout) |
| **Payment SDK**         | In your own page, inside secure fields High Wire serves. Your server never sees the card; the browser confirms the payment.                           | [Payment SDK](/docs/payment-sdk)         |

All three produce the same **payment** object, which you read, refund and reconcile through this
API. A card saved during a direct charge or an SDK checkout can be
[charged again](/docs/saved-payment-methods) with the customer present. Which surfaces your account
can use is part of its configuration; High Wire confirms it at onboarding.

## Environments [#environments]

| Environment | API base URL                                           | Hosted checkout host                    |
| ----------- | ------------------------------------------------------ | --------------------------------------- |
| Staging     | `https://api-payments-staging.highwirepayments.com/v1` | `checkout-staging.highwirepayments.com` |
| Production  | `https://api-payments.highwirepayments.com/v1`         | `checkout.highwirepayments.com`         |

Staging and production are separate systems with separate credentials. Staging processes test
cards only; High Wire gives you test card numbers for your account at onboarding. Every example in
these guides targets staging.

## A request in one minute [#a-request-in-one-minute]

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/auth-and-capture \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c2e9-4f9a-7c2f-9c83-1b2b3c4d5e6f" \
  -d '{ "amount": 7200, "cardNumber": "5555555555554444", "cardExpiration": "1133", "cardCvv": "456", "cardholderName": "Jordan Example", "cardholderEmail": "jordan@example.com", "cardholderPhone": "+14155550101", "cardholderBillingAddress": "100 Example Street", "cardholderBillingZipcode": "94105", "cardholderBillingCity": "Springfield", "cardholderBillingState": "CA" }'
```

* **Authentication**: HTTP Basic with your username and secret, server-side only.
* **Amounts** are integers in cents; the currency is USD.
* **Every `POST` answers `201`**, with the object it created or acted on. Reads answer `200`.
* **Ids** are UUID v7 strings. Persist the `paymentId` every call returns.
* **Idempotency**: send an `Idempotency-Key` on every write so a retry can never charge twice.
* **Errors** come in a handful of JSON shapes, told apart by their fields. A declined charge is a
  `400` whose body carries `status: "DECLINED"` and a `declinedReason`.

The details are in [Errors and conventions](/docs/errors-and-conventions).

## Endpoints [#endpoints]

| Method and path                                | What it does                                               | Guide                                                            | Reference                                                                         |
| ---------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `POST /v1/payments/auth-and-capture`           | Charge a card now.                                         | [Card payments](/docs/card-payments#authorize-and-capture)       | [authAndCapture](/docs/gateway-payments/payments/authAndCapture_v1)               |
| `POST /v1/payments/auth`                       | Hold funds on a card for later capture.                    | [Card payments](/docs/card-payments#authorize-now-capture-later) | [auth](/docs/gateway-payments/payments/auth_v1)                                   |
| `POST /v1/payments/{id}/capture`               | Capture an authorized payment, in full.                    | [Card payments](/docs/card-payments#capture)                     | [capture](/docs/gateway-payments/payments/capture_v1)                             |
| `POST /v1/payments/{id}/void`                  | Cancel an authorized or in-flight payment.                 | [Card payments](/docs/card-payments#void)                        | [void](/docs/gateway-payments/payments/void_v1)                                   |
| `POST /v1/payments/charge-saved-method`        | Charge a saved card again.                                 | [Saved payment methods](/docs/saved-payment-methods)             | [chargeSavedMethod](/docs/gateway-payments/payments/chargeSavedMethod_v1)         |
| `POST /v1/payments/checkout-session`           | Create a hosted checkout link.                             | [Hosted checkout](/docs/hosted-checkout#create-a-checkout)       | [createCheckoutSession](/docs/gateway-payments/payments/createCheckoutSession_v1) |
| `POST /v1/payments/{id}/checkout-session/link` | Reissue a hosted checkout link, revoking the previous one. | [Hosted checkout](/docs/hosted-checkout#reissue-a-link)          | [reissueCheckoutLink](/docs/gateway-payments/payments/reissueCheckoutLink_v1)     |
| `POST /v1/payments/{id}/refund`                | Refund a captured payment, in full or in part.             | [Refunds](/docs/refunds)                                         | [refund](/docs/gateway-payments/payments/refund_v1)                               |
| `GET /v1/payments/{id}`                        | Read a payment with its history.                           | [Payment lifecycle](/docs/payment-lifecycle#retrieve-a-payment)  | [getPayment](/docs/gateway-payments/payments/getPayment_v1)                       |

## Guides [#guides]

<Cards>
  <Card title="Authentication" href="/docs/authentication">
    Credentials, rotation, and the 401 and 403 responses.
  </Card>

  <Card title="Make your first payment" href="/docs/make-your-first-payment">
    Charge a test card, read it back, refund it.
  </Card>

  <Card title="Card payments" href="/docs/card-payments">
    Authorize and capture, auth then capture or void, 3-D Secure, device data, declines.
  </Card>

  <Card title="Hosted checkout" href="/docs/hosted-checkout">
    Create a payment link and confirm the payment before you fulfil.
  </Card>

  <Card title="Saved payment methods" href="/docs/saved-payment-methods">
    Save a card on a charge and charge it again within its bounds.
  </Card>

  <Card title="Refunds" href="/docs/refunds">
    Full and partial refunds, refund status, disputes.
  </Card>

  <Card title="Payment lifecycle" href="/docs/payment-lifecycle">
    The payment object, every status, and how to poll.
  </Card>

  <Card title="Account limits" href="/docs/account-limits">
    Merchant limits and velocity checks, and what they return.
  </Card>

  <Card title="Idempotency key" href="/docs/idempotency-key">
    Retry writes safely.
  </Card>

  <Card title="Errors and conventions" href="/docs/errors-and-conventions">
    Every response shape, status code and retry rule.
  </Card>
</Cards>


# Make your first payment (/docs/make-your-first-payment)



You need staging credentials (see [Authentication](/docs/authentication)) and a test card number.
Staging processes test cards only; High Wire gives you the numbers for your account at onboarding.
The examples use a common test number. Use test cards only, and never paste real card data into a
shell, a log or a ticket.

## 1. Charge the card [#1-charge-the-card]

The simplest flow is **auth-and-capture**: authorize and capture the funds in one call. Send an
`Idempotency-Key` so a retry can never charge twice.

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/auth-and-capture \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c2e9-4f9a-7c2f-9c83-1b2b3c4d5e6f" \
  -d '{
    "amount": 7200,
    "cardNumber": "5555555555554444",
    "cardExpiration": "1133",
    "cardCvv": "456",
    "cardholderName": "Jordan Example",
    "cardholderEmail": "jordan@example.com",
    "cardholderPhone": "+14155550101",
    "cardholderBillingAddress": "100 Example Street",
    "cardholderBillingZipcode": "94105",
    "cardholderBillingCity": "Springfield",
    "cardholderBillingState": "CA"
  }'
```

`201 Created`:

```json
{
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "status": "CAPTURED",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example"
}
```

Store `paymentId`. `status: "CAPTURED"` means the money moved. A `400` with
`status: "DECLINED"` and a `declinedReason` is a decline; a `400` with `fieldErrors` is a
validation error. Both are explained in [Errors and conventions](/docs/errors-and-conventions).

## 2. Read the payment back [#2-read-the-payment-back]

```bash
curl https://api-payments-staging.highwirepayments.com/v1/payments/01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b \
  -u "YOUR_API_KEY:YOUR_SECRET"
```

`200 OK` (abridged):

```json
{
  "id": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "object": "payment",
  "status": "CAPTURED",
  "currency": "usd",
  "amount": 7200,
  "paymentMethodType": "CARD",
  "paymentMethodDetails": { "brand": "mastercard", "bin": "555555", "last4": "4444", "expirationMonth": 11, "expirationYear": 2033 },
  "transactions": [
    { "id": "0192a6b3-2c1d-7e4f-8a9b-3c4d5e6f7a8b", "object": "transaction", "type": "AUTH_AND_CAPTURE", "status": "SUCCEEDED", "amount": 7200, "createdAt": "2026-10-19T14:05:00.000Z", "updatedAt": "2026-10-19T14:05:01.000Z" }
  ],
  "amounts": { "requested": 7200, "authorized": 7200, "captured": 7200, "voided": null, "refunded": null },
  "createdAt": "2026-10-19T14:05:00.000Z",
  "authorizedAt": "2026-10-19T14:05:01.000Z",
  "capturedAt": "2026-10-19T14:05:01.000Z",
  "voidedAt": null,
  "updatedAt": "2026-10-19T14:05:01.000Z"
}
```

This read is the source of truth for a payment's state. See
[Payment lifecycle](/docs/payment-lifecycle) for every field and status.

## 3. Refund it [#3-refund-it]

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b/refund \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c30a-1f2e-7b6c-8d9a-0e1f2a3b4c5d" \
  -d '{ "amount": 500, "reason": "customer request" }'
```

`201 Created`:

```json
{
  "refundId": "01928e55-3c4d-7c8e-8b1a-fedcba987654",
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "status": "SUCCEEDED",
  "amount": 500,
  "remainingRefundableAmount": 6700,
  "cardNumber": "555555xxxxxxxx4444"
}
```

Omit `amount` to refund the remaining balance in full. Reading the payment again now shows
`status: "PARTIALLY_REFUNDED"` and the refund at the top of `transactions`.

## Where to go next [#where-to-go-next]

* Hold funds first and settle later, handle 3-D Secure, or send browser signals with the charge:
  [Card payments](/docs/card-payments).
* Take payments without touching card data: [Hosted checkout](/docs/hosted-checkout).
* Charge a saved card again right after the purchase: [Saved payment methods](/docs/saved-payment-methods).
* Make every write safe to retry: [Idempotency key](/docs/idempotency-key).


# Payment lifecycle (/docs/payment-lifecycle)



A **payment** is the object every call creates or acts on. Its `status` tells you where the money
is. Reading the payment is the one authoritative source of truth: redirects, return pages and
browser events are hints, the payment is the fact.

## Retrieve a payment [#retrieve-a-payment]

```http
GET /v1/payments/{id}
```

Returns the payment with its money roll-up and its complete history: every transaction **and**
every refund, in one `transactions` array. Accounts limited to refunds and voids may call this
route.

```bash
curl https://api-payments-staging.highwirepayments.com/v1/payments/01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b \
  -u "YOUR_API_KEY:YOUR_SECRET"
```

`200 OK`, here for a payment that was captured and then partially refunded:

```json
{
  "id": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "object": "payment",
  "status": "PARTIALLY_REFUNDED",
  "currency": "usd",
  "amount": 7200,
  "paymentMethodType": "CARD",
  "paymentMethodDetails": {
    "brand": "mastercard",
    "bin": "555555",
    "last4": "4444",
    "expirationMonth": 11,
    "expirationYear": 2033
  },
  "transactions": [
    {
      "id": "01928e55-3c4d-7c8e-8b1a-fedcba987654",
      "object": "refund",
      "type": "REFUND",
      "status": "SUCCEEDED",
      "amount": 500,
      "parentTransactionId": "0192a6b3-2c1d-7e4f-8a9b-3c4d5e6f7a8b",
      "reason": "customer request",
      "refundKind": "PARTIAL",
      "createdAt": "2026-10-20T09:12:00.000Z",
      "updatedAt": "2026-10-20T09:12:01.000Z"
    },
    {
      "id": "0192a6b3-2c1d-7e4f-8a9b-3c4d5e6f7a8b",
      "object": "transaction",
      "type": "AUTH_AND_CAPTURE",
      "status": "SUCCEEDED",
      "amount": 7200,
      "createdAt": "2026-10-19T14:05:00.000Z",
      "updatedAt": "2026-10-19T14:05:01.000Z"
    }
  ],
  "amounts": {
    "requested": 7200,
    "authorized": 7200,
    "captured": 7200,
    "voided": null,
    "refunded": 500
  },
  "createdAt": "2026-10-19T14:05:00.000Z",
  "authorizedAt": "2026-10-19T14:05:01.000Z",
  "capturedAt": "2026-10-19T14:05:01.000Z",
  "voidedAt": null,
  "updatedAt": "2026-10-20T09:12:01.000Z"
}
```

| Field                                                              | Type           | Notes                                                                                                                                                                              |
| ------------------------------------------------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                                               | string         | The payment id.                                                                                                                                                                    |
| `object`                                                           | string         | Always `payment`.                                                                                                                                                                  |
| `status`                                                           | enum           | See [Statuses](#statuses).                                                                                                                                                         |
| `currency`                                                         | string         | Always `usd`.                                                                                                                                                                      |
| `amount`                                                           | integer        | The requested amount in cents.                                                                                                                                                     |
| `paymentMethodType`                                                | string or null | `CARD`, `APPLE_PAY` or `GOOGLE_PAY`; `null` when no payment method was captured yet (an unpaid hosted checkout).                                                                   |
| `paymentMethodDetails`                                             | object or null | Tokenized, PCI-safe details of the method: `brand`, `bin`, `last4`, `expirationMonth`, `expirationYear`. Treat any other key as informational. `null` when no method was captured. |
| `transactions`                                                     | array          | Refunds first, then transactions, newest first within each group. See below.                                                                                                       |
| `amounts`                                                          | object         | Cents, rolled up: `requested`, `authorized`, `captured`, `voided`, `refunded`. A stage that never happened is `null`, not `0`.                                                     |
| `createdAt`, `authorizedAt`, `capturedAt`, `voidedAt`, `updatedAt` | string or null | ISO 8601. `null` until or unless the stage happens.                                                                                                                                |
| `nextAction`                                                       | object         | Present only while `status` is `REQUIRES_ACTION`: the pending customer-verification step, same shape as on the charge response. See [3-D Secure](/docs/card-payments#3-d-secure).  |

Not returned: customer and payment-method ids, `referenceId`, `metadata`, line items, checkout
links. Correlate a payment with your order by its `id`, which you stored when you created it.

### Items in `transactions` [#items-in-transactions]

| Field                    | Notes                                                                                                                                               |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                     | The transaction's or refund's own id.                                                                                                               |
| `object`                 | `transaction` or `refund`.                                                                                                                          |
| `type`                   | `AUTH`, `CAPTURE`, `AUTH_AND_CAPTURE`, `VOID` or `REFUND`.                                                                                          |
| `status`                 | Transactions: `PENDING`, `SUCCEEDED`, `FAILED`, `CANCELED` or `EXPIRED`. Refunds: `PENDING`, `SUCCEEDED`, `FAILED` or `CANCELED`.                   |
| `amount`                 | Cents for this entry.                                                                                                                               |
| `parentTransactionId`    | For a refund, the captured transaction it refunds. For a capture, its authorization.                                                                |
| `declinedReason`         | Why this entry failed. Omitted on success.                                                                                                          |
| `reason`                 | The free-text reason sent with a refund. Refunds only.                                                                                              |
| `refundKind`             | `FULL` when that single refund equals the captured amount, otherwise `PARTIAL`. A second partial refund that closes the balance is still `PARTIAL`. |
| `createdAt`, `updatedAt` | ISO 8601.                                                                                                                                           |

Items are sparse: a field whose value would be `null` is omitted entirely.

| Status | Body                                                    | When                                                                                         |
| ------ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `200`  | The payment.                                            |                                                                                              |
| `400`  | [Plain error](/docs/errors-and-conventions#plain-error) | `Validation failed (uuid v 7 is expected)`: the id is not a UUID v7.                         |
| `401`  | [Plain error](/docs/errors-and-conventions#plain-error) | Invalid credentials, or an inactive account.                                                 |
| `404`  | [Plain error](/docs/errors-and-conventions#plain-error) | `Payment not found: <id>`. Unknown and another account's are deliberately indistinguishable. |

## Statuses [#statuses]

| `status`             | Meaning                                                                                                          | Final?                                                                                                                                         |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING`            | In flight. A charge the processor is still settling, or a hosted checkout that has not been paid yet.            | No.                                                                                                                                            |
| `AUTHORIZED`         | Funds are held on the card and not yet captured.                                                                 | No: capture, void, or it lapses to `EXPIRED`.                                                                                                  |
| `CAPTURED`           | The money moved. The success state of a sale.                                                                    | Yes for the sale; refunds and disputes can still follow.                                                                                       |
| `PARTIALLY_REFUNDED` | Part of the captured amount was returned.                                                                        | Further refunds possible.                                                                                                                      |
| `REFUNDED`           | The full captured amount was returned.                                                                           | Yes.                                                                                                                                           |
| `VOIDED`             | An authorization or in-flight charge was cancelled before capture.                                               | Yes.                                                                                                                                           |
| `DECLINED`           | The charge was refused: by a limit, by fraud screening, by the issuer, or by a failed 3-D Secure authentication. | For a direct charge, yes. A hosted checkout can be paid again while card attempts remain.                                                      |
| `EXPIRED`            | An authorization lapsed uncaptured, a 3-D Secure challenge timed out, or a hosted checkout link expired unpaid.  | Yes.                                                                                                                                           |
| `REQUIRES_ACTION`    | The customer must complete 3-D Secure. `nextAction` is present.                                                  | No.                                                                                                                                            |
| `DISPUTED`           | The cardholder disputed the payment with their bank.                                                             | Permanent: the status never changes back, whether the dispute is won or lost. A refund that settles afterwards is recorded on the refund only. |

## Transitions [#transitions]

| Trigger                                       | From                                         | To                                                                        |
| --------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| `POST /v1/payments/auth-and-capture`          | new                                          | `CAPTURED`, `PENDING`, `REQUIRES_ACTION` or `DECLINED`                    |
| `POST /v1/payments/auth`                      | new                                          | `AUTHORIZED`, `PENDING`, `REQUIRES_ACTION` or `DECLINED`                  |
| `POST /v1/payments/charge-saved-method`       | new                                          | `CAPTURED`, `PENDING` or `DECLINED` (never `REQUIRES_ACTION`)             |
| `POST /v1/payments/checkout-session`          | new                                          | `PENDING`                                                                 |
| `POST /v1/payments/{id}/capture`              | `AUTHORIZED`                                 | `CAPTURED`                                                                |
| `POST /v1/payments/{id}/void`                 | `AUTHORIZED`, `PENDING`, `REQUIRES_ACTION`   | `VOIDED`                                                                  |
| `POST /v1/payments/{id}/refund` that succeeds | `CAPTURED`, `PARTIALLY_REFUNDED`             | `PARTIALLY_REFUNDED` or `REFUNDED`; a `DISPUTED` payment stays `DISPUTED` |
| The processor settles a charge                | `PENDING`                                    | `CAPTURED`, `AUTHORIZED` or `DECLINED`                                    |
| The customer finishes 3-D Secure              | `REQUIRES_ACTION`                            | `CAPTURED` or `AUTHORIZED`; `DECLINED` on failure; `EXPIRED` on timeout   |
| The authorization lapses                      | `AUTHORIZED`                                 | `EXPIRED`                                                                 |
| The shopper retries on a hosted checkout      | `DECLINED`                                   | `PENDING`, then `CAPTURED`                                                |
| A hosted checkout link expires unpaid         | `PENDING`, `REQUIRES_ACTION`, `DECLINED`     | `EXPIRED`                                                                 |
| The cardholder opens a dispute                | `CAPTURED`, `PARTIALLY_REFUNDED`, `REFUNDED` | `DISPUTED`                                                                |

## Polling for an outcome [#polling-for-an-outcome]

Some outcomes arrive after the call that started them. Poll `GET /v1/payments/{id}` until the
status is one you can act on:

* **`PENDING` after a charge**: the processor accepted the charge and is settling it. It usually
  moves within seconds. Do not retry the charge.
* **`REQUIRES_ACTION`**: keep polling while the customer is in the challenge. It ends in
  `CAPTURED` or `AUTHORIZED`, `DECLINED` or `EXPIRED`. Several minutes are normal.
* **A hosted checkout**: it stays `PENDING` until the shopper pays. Poll when the shopper reaches
  your success URL, and before you fulfil the order check that `status` is `CAPTURED` and that
  `amount` is what you expect. A checkout nobody pays becomes `EXPIRED` up to an hour after its
  `expiresAt`.
* **A `PENDING` refund**: it appears in `transactions` with `object: "refund"` and resolves to
  `SUCCEEDED`, `FAILED` or `CANCELED`. The payment's own status moves only when the refund
  succeeds.

Polling every few seconds is fine. Stop on a final status. Beyond polling, reconciliation channels
for your account are set up with High Wire at onboarding.


# Refunds (/docs/refunds)



```http
POST /v1/payments/{id}/refund
```

Refunds a captured payment. A refund may be full or partial, and several partial refunds are allowed
on the same payment as long as their sum does not exceed the captured amount. Accounts limited to
refunds and voids may call this route.

To cancel a payment that was **not** captured yet, [void](/docs/card-payments#void) it instead.

## Eligibility [#eligibility]

* The payment has a successful capture: it is `CAPTURED`, `PARTIALLY_REFUNDED`, or `DISPUTED` with a
  dispute that does not block (see [Disputes](#disputes)). Anything else answers `404`.
* No other refund of the same payment is in progress.
* `amount`, when given, is at least `1` and no more than the remaining refundable balance. Omit it
  to refund the whole remaining balance.
* Your daily refund limit, if one is configured, is not exceeded. See
  [Account limits](/docs/account-limits#merchant-limits).

## Request [#request]

Idempotency-Key: honoured. The `409` for a disputed payment is not stored under the key.

| Field    | Type    | Required | Rules                                                                                                                                                                         |
| -------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | integer | no       | Cents to refund, at least `1` and at most the remaining refundable balance. Omit for a full refund of the remaining balance.                                                  |
| `reason` | string  | no       | Free text recorded against the refund, at most 255 characters. It is returned on the payment's history. Never put card numbers, security codes or other sensitive data in it. |

```bash
# Full refund of the remaining balance
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b/refund \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c30a-1f2e-7b6c-8d9a-0e1f2a3b4c5d" \
  -d '{}'

# Partial refund with a reason
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b/refund \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c30b-7a8b-7c9d-8e0f-1a2b3c4d5e6f" \
  -d '{
    "amount": 500,
    "reason": "customer request"
  }'
```

An empty body (`{}`) is a full refund; the `Content-Type` header is still required.

## Response [#response]

`201 Created`:

```json
{
  "refundId": "01928e55-3c4d-7c8e-8b1a-fedcba987654",
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "status": "SUCCEEDED",
  "amount": 500,
  "remainingRefundableAmount": 6700,
  "cardNumber": "555555xxxxxxxx4444"
}
```

| Field                       | Type           | Notes                                                                                                                                                                                                                                                              |
| --------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `refundId`                  | string         | The refund's own id. It appears in the payment's `transactions` with `object: "refund"`.                                                                                                                                                                           |
| `paymentId`                 | string         | The payment that was refunded.                                                                                                                                                                                                                                     |
| `status`                    | enum           | **The refund's status**, not the payment's. `SUCCEEDED`: the money is on its way back. `PENDING`: the processor accepted the refund and is still settling it; it resolves to `SUCCEEDED`, `FAILED` or `CANCELED` asynchronously, visible on the payment's history. |
| `amount`                    | integer        | Cents refunded by this call.                                                                                                                                                                                                                                       |
| `remainingRefundableAmount` | integer        | Cents still available for further refunds, computed as if this refund succeeds.                                                                                                                                                                                    |
| `cardNumber`                | string or null | Masked card number of the payment. `null` for wallet payments, which carry no card number.                                                                                                                                                                         |

The payment's own status moves only when a refund **succeeds**: to `PARTIALLY_REFUNDED`, or to
`REFUNDED` once the refunded total reaches the captured amount. A `DISPUTED` payment keeps that
status; the refund is recorded on its history. See [Payment lifecycle](/docs/payment-lifecycle).

| Status | Body                                                                        | When                                                                                                                                                                                                  |
| ------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`  | The response above.                                                         | Refunded, or accepted and settling (`PENDING`).                                                                                                                                                       |
| `400`  | [Validation error](/docs/errors-and-conventions#validation-error)           | `amount` is not a positive integer, or `reason` is too long.                                                                                                                                          |
| `400`  | [Plain error](/docs/errors-and-conventions#plain-error)                     | `A refund is already in progress for this payment`, `Payment has already been fully refunded`, or `amount exceeds remaining refundable balance (6700)`.                                               |
| `400`  | [Refund decline](/docs/errors-and-conventions#refund-decline)               | The processor refused the refund, or `Amount exceeds daily refund limit`.                                                                                                                             |
| `404`  | [Plain error](/docs/errors-and-conventions#plain-error)                     | `Payment not found or not refundable: <id>`: unknown, another account's, or a payment without a successful capture (`AUTHORIZED`, `DECLINED`, `VOIDED`, `PENDING`). A malformed id answers `404` too. |
| `409`  | [Reason-coded conflict](/docs/errors-and-conventions#reason-coded-conflict) | `reason: "PAYMENT_DISPUTED"`, see below; or (without `reason`) an `Idempotency-Key` in flight.                                                                                                        |
| `422`  | [Plain error](/docs/errors-and-conventions#plain-error)                     | The key was reused with a different body.                                                                                                                                                             |
| `401`  | [Plain error](/docs/errors-and-conventions#plain-error)                     | Invalid credentials, or an inactive account.                                                                                                                                                          |
| `500`  | [Plain error](/docs/errors-and-conventions#plain-error)                     | The refund was accepted but could not be finalized yet: do not retry; poll the payment.                                                                                                               |

## Disputes [#disputes]

A cardholder can dispute a payment with their bank. The payment becomes `DISPUTED`, permanently.
While the dispute still holds the funds, a refund would pay the cardholder twice, so it is refused:

```json
{
  "statusCode": 409,
  "message": "Payment has an open or lost dispute and cannot be refunded.",
  "reason": "PAYMENT_DISPUTED"
}
```

* A chargeback, or a later stage, that is open or was lost blocks refunds.
* An inquiry (the bank asking for information, no funds moved) does not block, and neither does a
  dispute that was won or closed.
* The `409` is not stored under the `Idempotency-Key`: the same key can be sent again once the
  dispute is won.


# Saved payment methods (/docs/saved-payment-methods)



A card saved during a checkout can be charged again **with the customer present**: an upsell or an
add-on right after the purchase that saved it. There is no hosted page, no redirect, and no card
number flows through your servers; you send the two opaque ids the original save returned.

```http
POST /v1/payments/charge-saved-method
```

This is not a tool for unattended renewals: every reuse is bounded by the payment that first saved
the card, and a charge weeks later is refused with `WINDOW_CLOSED`.

## Obtaining the ids [#obtaining-the-ids]

Set `savePaymentMethodForFutureUse: true` on a
[card charge](/docs/card-payments#request-body). Sending the flag attests that you have the
customer's consent to save the card. The `201` then carries two extra fields:

```json
{
  "paymentId": "01928e3e-7b6c-7c8e-9b3a-5b9f8b9b9b9b",
  "status": "CAPTURED",
  "amount": 7200,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "cardholderName": "Jordan Example",
  "customerId": "0192a7d1-4e2b-7f08-9a6c-2d3e4f5a6b7c",
  "paymentMethodId": "0192a7d1-8c3f-7a21-b5d4-6e7f8a9b0c1d"
}
```

| Field             | Notes                                                                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId`      | Identifies the customer on your account, keyed on `cardholderEmail` (trimmed and lowercased). Persist it alongside your own customer record. |
| `paymentMethodId` | Identifies the saved card. Pair it with `customerId` to charge again.                                                                        |

* The ids are scoped to your account. Another account cannot use them, and a wrong pairing of
  customer and method is rejected.
* Treat both ids as confidential: with your publishable key they are enough to charge the card from
  a browser. Keep them out of URLs, logs and client-side storage.
* The ids are chargeable once the payment that saved the card is &#x2A;*`CAPTURED`**: at once when the
  charge answered `CAPTURED`, after the capture when you
  [authorized first](/docs/card-payments#authorize-now-capture-later), and once a `PENDING` charge
  settles. Until then `charge-saved-method` answers `409` with `reason: "ORIGINAL_NOT_SETTLED"`.
* A charge that came back as a 3-D Secure challenge (`status: "REQUIRES_ACTION"`) returns a
  **pending save** instead: `charge-saved-method` answers `409` with
  `reason: "METHOD_NOT_READY"` until the payment settles successfully. If the authentication fails,
  expires or is voided, the id is invalidated and a later charge answers `404`.
* Saving a card that is already on file for the same customer returns the **existing**
  `paymentMethodId`. The bounds below belong to the card, not to the id: a re-save opens nothing
  new.
* A checkout through the [Payment SDK](/docs/payment-sdk) can save a card too; it returns the same
  kind of ids, which this endpoint accepts. A [hosted checkout](/docs/hosted-checkout#saving-the-card)
  created with `savePaymentMethodForFutureUse: true` can save one as well, but no API response
  returns its ids.

## Charge a saved method [#charge-a-saved-method]

Idempotency-Key: honoured. A `409` refusal with a `reason` is not stored under the key.

| Field             | Type    | Required | Rules                                                                                   |
| ----------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
| `amount`          | integer | yes      | Cents, at least `100`, and at most the amount of the payment that first saved the card. |
| `customerId`      | string  | yes      | UUID v7 returned by the original save.                                                  |
| `paymentMethodId` | string  | yes      | UUID v7 returned by the original save. Must belong to `customerId` and to your account. |

```bash
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/charge-saved-method \
  -u "YOUR_API_KEY:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0193c2fa-8e1b-7d5c-9a2f-3b4c5d6e7f80" \
  -d '{
    "amount": 1500,
    "customerId": "0192a7d1-4e2b-7f08-9a6c-2d3e4f5a6b7c",
    "paymentMethodId": "0192a7d1-8c3f-7a21-b5d4-6e7f8a9b0c1d"
  }'
```

`201 Created`. The response is deliberately slim: your application already has the card details
from the original save.

```json
{
  "paymentId": "0192a3f7-9c0d-7e1f-8a2b-3c4d5e6f7a8b",
  "status": "CAPTURED",
  "amount": 1500,
  "customerId": "0192a7d1-4e2b-7f08-9a6c-2d3e4f5a6b7c",
  "paymentMethodId": "0192a7d1-8c3f-7a21-b5d4-6e7f8a9b0c1d"
}
```

| Field                           | Notes                                                                                                                                                                                                                            |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentId`                     | The new payment. Persist it for refunds.                                                                                                                                                                                         |
| `status`                        | `CAPTURED` when the charge settled synchronously. `PENDING` when your account collects asynchronously: the payment settles to `CAPTURED` or `DECLINED` shortly after; [poll it](/docs/payment-lifecycle#polling-for-an-outcome). |
| `amount`                        | Cents charged.                                                                                                                                                                                                                   |
| `customerId`, `paymentMethodId` | Echo the request.                                                                                                                                                                                                                |

This route never returns `REQUIRES_ACTION`: an issuer that insists on a 3-D Secure challenge
declines the charge with `declinedReason: "Additional authentication required"`, and the attempt
counts toward the attempt limit.

## Bounds [#bounds]

Every reuse is bounded by the saved card and by the payment that first saved it (its *original
payment*), whichever of the card's ids is charged. Reuses made from the browser through the Payment
SDK count toward the same bounds.

| Bound           | Rule                                                                                                                                                                    |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Window          | Accepted up to **24 hours** after the card was first saved, that is, after it became chargeable. The window never restarts, not even when the same card is saved again. |
| Approved reuses | At most **2** per card. A reuse still in progress holds its place; a declined one does not count.                                                                       |
| Attempts        | At most **5** per card, declined ones included.                                                                                                                         |
| Amount          | Each reuse is at most the **original payment's amount**, per charge, not cumulative.                                                                                    |

## Reuse refusals [#reuse-refusals]

A reuse outside its bounds is refused with `409` and a machine-readable `reason` **before** the
processor is contacted: nothing is charged, no payment is recorded, your limits are not consumed,
and the refusal is not stored under the `Idempotency-Key`.

```json
{
  "statusCode": 409,
  "message": "This saved payment method has reached its reuse limit.",
  "reason": "REUSE_LIMIT"
}
```

| `reason`                | `message`                                                                              | When                                                                                                   | What to do                                                 |
| ----------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `ORIGINAL_NOT_SETTLED`  | `The original payment has not settled yet; retry once it is captured.`                 | The payment that first saved the card is still `PENDING`, `REQUIRES_ACTION` or `AUTHORIZED`.           | Retry once it is captured; the same key is fine.           |
| `METHOD_NOT_READY`      | `The saved payment method is not ready yet; retry once its checkout is captured.`      | The method is a pending save whose checkout has not settled (an SDK checkout, a 3-D Secure challenge). | Retry once the checkout is captured; the same key is fine. |
| `ORIGINAL_NOT_ELIGIBLE` | `The original payment is not in a state that allows charging this saved method again.` | The original payment was refunded in full, disputed, voided, declined or expired.                      | Do not retry; collect the card again.                      |
| `WINDOW_CLOSED`         | `The 24-hour window for charging this saved method again has closed.`                  | More than 24 hours since the card was first saved.                                                     | Do not retry; collect the card again.                      |
| `REUSE_LIMIT`           | `This saved payment method has reached its reuse limit.`                               | The card was already charged again twice, approved or in progress, from either surface.                | Do not retry; collect the card again.                      |
| `ATTEMPT_LIMIT`         | `This saved payment method has reached its limit of upsell attempts.`                  | Five attempts were already made with the card, declined ones included.                                 | Do not retry; collect the card again.                      |
| `AMOUNT_LIMIT`          | `An upsell cannot exceed the amount of the original payment.`                          | `amount` exceeds the original payment's amount.                                                        | A smaller amount may pass.                                 |

A `409` **without** `reason` is an `Idempotency-Key` still in flight.

## Ownership and state errors [#ownership-and-state-errors]

These are refused before the processor is contacted, with the
[charge decline](/docs/errors-and-conventions#charge-decline) shape reduced to `amount`, `status`
and `declinedReason`. No payment is recorded and your limits are not consumed.

| Status | `declinedReason`                                                                                | When                                                                                                                                                                                     |
| ------ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404`  | `Payment method not found.`                                                                     | The `paymentMethodId` does not exist, belongs to another account, or came from a checkout that expired or was abandoned before it settled. The cases are deliberately indistinguishable. |
| `400`  | `Payment method does not belong to the given customer.`                                         | Both ids are yours, but the method belongs to a different `customerId`. Usually a pairing bug on your side.                                                                              |
| `400`  | `Payment method is removed.`, `Payment method is expired.`, `Payment method is not chargeable.` | The saved card is no longer chargeable and will not become chargeable again. Save the card again the next time the customer is present.                                                  |
| `400`  | `Payment method is not chargeable on this account.`                                             | The saved card can no longer be charged on your account. Save it again the next time the customer is present.                                                                            |

```json
{
  "amount": 1500,
  "status": "DECLINED",
  "declinedReason": "Payment method not found."
}
```

## Declines [#declines]

When the issuer or your limits refuse the charge, the answer is `400` with the full
[charge decline](/docs/errors-and-conventions#charge-decline) shape. The payment is recorded as
`DECLINED`, `paymentId` names it, and the attempt counts toward the five attempts (not toward the two
reuses).

```json
{
  "amount": 1500,
  "cardNumber": "555555xxxxxxxx4444",
  "cardExpiration": "1133",
  "customerId": "0192a7d1-4e2b-7f08-9a6c-2d3e4f5a6b7c",
  "paymentMethodId": "0192a7d1-8c3f-7a21-b5d4-6e7f8a9b0c1d",
  "paymentId": "0192a3f7-9c0d-7e1f-8a2b-3c4d5e6f7a8b",
  "status": "DECLINED",
  "declinedReason": "Card declined"
}
```

| Status       | Body                                                                        | When                                                                               |
| ------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `201`        | The slim response above.                                                    | Charged, or accepted and settling (`PENDING`).                                     |
| `400`        | [Validation error](/docs/errors-and-conventions#validation-error)           | `amount` below `100`, or an id that is not a UUID v7.                              |
| `400`        | [Charge decline](/docs/errors-and-conventions#charge-decline)               | An ownership or state error (above), a limit, or the issuer.                       |
| `404`        | [Charge decline](/docs/errors-and-conventions#charge-decline)               | `Payment method not found.`                                                        |
| `409`        | [Reason-coded conflict](/docs/errors-and-conventions#reason-coded-conflict) | A reuse refusal, or (without `reason`) an `Idempotency-Key` in flight.             |
| `422`        | [Plain error](/docs/errors-and-conventions#plain-error)                     | The key was reused with a different body.                                          |
| `429`        | [`VELOCITY_LIMIT`](/docs/errors-and-conventions#code-carrying-errors)       | A velocity check refused the charge.                                               |
| `401`, `403` | [Plain error](/docs/errors-and-conventions#plain-error)                     | Invalid credentials or an inactive account (`401`); a refund-only account (`403`). |

## From the browser [#from-the-browser]

The same charge can be made from your checkout page through the Payment SDK with the
publishable key, so it works after a page reload. It is bounded by the same window, limits and
amount, and the counts are shared: a reuse from the browser and one from this endpoint are the
card's two. A reuse the browser gets back as `PENDING` is followed from your server with
[`GET /v1/payments/{id}`](/docs/payment-lifecycle#retrieve-a-payment); the browser holds no
credential to read it. See [`hw.chargeSavedMethod()`](/docs/payment-sdk/api-reference#hwchargesavedmethod)
in the Payment SDK reference.


# Plain HTML and JavaScript (/docs/highwire-js/guides/plain-html)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## Before you begin [#before-you-begin]

Use this guide when an existing browser checkout uses a plain HTML form and loads JavaScript with a `<script>` tag. Complete the artifact placement and integrity setup in [Getting started](/docs/highwire-js/getting-started) first.

Your checkout remains responsible for its inputs, validation, and payment request. This guide uses `buildExistingPaymentRequest(form)` to represent the existing merchant-owned function that validates the form and returns the complete request body already sent to your backend. Keep that function and its payment fields outside the highwire.js integration.

## Complete example [#complete-example]

The inputs below provide stable selectors for paste-event observation. `highwire.js` observes only whether a paste occurs; it never reads their values.

```html
<form id="checkout">
  <label>
    Card number
    <input id="card-number" autocomplete="cc-number" required>
  </label>
  <label>
    Expiration
    <input id="card-exp" autocomplete="cc-exp" required>
  </label>
  <label>
    Security code
    <input id="card-cvc" autocomplete="cc-csc" required>
  </label>
  <label>
    Billing postal code
    <input id="billing-zip" autocomplete="postal-code" required>
  </label>
  <button type="submit">Pay</button>
</form>

<script
  src="/assets/highwire-v<version>.js"
  integrity="sha384-REPLACE_WITH_THE_HASH_HIGHWIRE_SENT_YOU"
  crossorigin="anonymous"
  defer
></script>
<script src="/assets/checkout-highwire.js" defer></script>
```

Place the integration code in /assets/checkout-highwire.js on the same origin:

```js
const form = document.getElementById('checkout');

if (!(form instanceof HTMLFormElement)) {
  throw new Error('Checkout form not found');
}

const hw = HighWire();
const observedFields = [
  ['#card-number', 'number'],
  ['#card-cvc', 'cvc'],
  ['#card-exp', 'exp'],
  ['#billing-zip', 'zip'],
];
observedFields.forEach(([selector, fieldName]) => {
  const element = form.querySelector(selector);
  if (!element) throw new Error(`Missing observed field: ${selector}`);
  hw.observeField(element, fieldName);
});

async function collectDeviceDataOrNull() {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

form.addEventListener('submit', async (event) => {
  event.preventDefault();

  const existingPaymentRequest = buildExistingPaymentRequest(form);
  const deviceData = await collectDeviceDataOrNull();
  const response = await fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...existingPaymentRequest,
      ...(deviceData ? { deviceData } : {}),
    }),
  });

  if (!response.ok) {
    throw new Error('Payment request failed');
  }
});
```

The example intentionally does not define `buildExistingPaymentRequest(form)`. Replace that call with the existing function your checkout uses to produce its complete, validated payment request. The only property this integration adds is optional `deviceData`.

If your form uses different selectors, change only the selectors in `observedFields`. Keep each field name passed to `observeField()` in the supported wire format shown in the [API reference](/docs/highwire-js/api-reference#accepted-field-names).

## When collection is unavailable [#when-collection-is-unavailable]

Handled provider or required-context failures resolve to `null`; missing or broken platform primitives can reject. The helper converts either unavailable outcome to `null`, so the checkout can continue without `deviceData`. Do not send `null`, an empty object, or a hand-built substitute.

The example throws when the merchant endpoint returns a non-success response. Route that condition through the checkout's existing error handling. For collection and script-loading failures, follow [Troubleshooting](/docs/highwire-js/troubleshooting).


# React and Next.js (/docs/highwire-js/guides/react-nextjs)



These guides apply to the standalone **highwire.js v0.2.3** release delivered by HighWire. Use the matching release files; the API names and installation steps below are specific to that version.

## Client-side requirement [#client-side-requirement]

`collect()` must run in the cardholder's browser. In the Next.js App Router, call it from a client component with the `'use client'` directive; in other React stacks, call it from a browser event handler.

Importing the ESM module and constructing `HighWire()` do not access DOM APIs, collect context, or make network requests, but do not call `collect()` from a server component, Node.js API route, or `getServerSideProps`. Modern Node.js can produce valid-looking server context instead of an obvious failure. See the API reference for the complete [browser-only collection](/docs/highwire-js/api-reference#browser-only-collection) boundary.

## Vendor and import the ESM build [#vendor-and-import-the-esm-build]

Place the delivered ESM build somewhere your frontend bundler can import, such as `src/lib/highwire/highwire-v<version>.es.js`. If the project uses TypeScript, keep the matching TypeScript declaration delivered with the release beside the ESM file.

Import the factory and instance type from the vendored module:

```ts
import {
  HighWire,
  type HighWireInstance,
} from '@/lib/highwire/highwire-v<version>.es.js';
```

Bundled imports do not use the separately delivered SRI hash. For the differences between the ESM and UMD handoffs, see [Choose a build](/docs/highwire-js/getting-started#choose-a-build).

## Create and retain an instance [#create-and-retain-an-instance]

Create the instance after the component mounts and retain it in a ref. The submit handler can then use the same instance that observed the form fields:

```tsx
const hwRef = useRef<HighWireInstance | null>(null);

useEffect(() => {
  const hw = HighWire();
  hwRef.current = hw;

  return () => {
    if (hwRef.current === hw) hwRef.current = null;
  };
}, []);
```

No publishable key or other required configuration is needed.

## Observe fields after mount [#observe-fields-after-mount]

Call `observeField()` only after the checkout inputs exist in the DOM. Observe each supported field that your form contains, and keep the cleanup functions returned by those calls:

```tsx
const cardEl = document.querySelector<HTMLInputElement>('#card-number');
const cvcEl = document.querySelector<HTMLInputElement>('#card-cvc');

const unobserves = [
  cardEl && hw.observeField(cardEl, 'number'),
  cvcEl && hw.observeField(cvcEl, 'cvc'),
].filter(Boolean) as Array<() => void>;
```

Observation records paste events only; it never reads an input value. Review the [accepted field names](/docs/highwire-js/api-reference#accepted-field-names) before wiring the rest of the form.

## Collect on submission [#collect-on-submission]

Keep the merchant's existing request builder as the source of the complete, already-valid payment request. Call this focused helper from the existing submit handler after the merchant has built and validated `existingPaymentRequest`; the helper adds only optional `deviceData`:

```tsx
async function collectDeviceDataOrNull(hw: HighWireInstance) {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

async function submitCheckout(
  existingPaymentRequest: Record<string, unknown>,
) {
  const hw = hwRef.current;
  const deviceData = hw ? await collectDeviceDataOrNull(hw) : null;

  await fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...existingPaymentRequest,
      ...(deviceData ? { deviceData } : {}),
    }),
  });
}
```

Do not serialize the form or enumerate gateway payment fields in the `highwire.js` wiring. The builder remains merchant-owned and must return the request the checkout already validates and sends. If collection returns `null`, omit the field and continue the existing payment flow as described in [When collection returns null](/docs/highwire-js/device-data#when-collection-returns-null).

If `collect()` rejects unexpectedly, the helper returns `null` and the checkout continues without `deviceData`.

## Clean up observers [#clean-up-observers]

Return a cleanup function from the effect that registered the observers. Call every retained function when the form unmounts, then clear the retained instance:

```tsx
return () => {
  unobserves.forEach((unobserve) => unobserve());
  if (hwRef.current === hw) hwRef.current = null;
};
```

When React mounts a new checkout form, create an instance and observe the new DOM elements again. The [API reference](/docs/highwire-js/api-reference#hwobservefieldelement-fieldname) defines the observer's return value and cleanup behavior.

## Complete example [#complete-example]

This component accepts the merchant's request builder and form children. The merchant keeps ownership of the payment DTO, validation, inputs, and submit control; the wrapper owns only the `highwire.js` lifecycle and optional `deviceData` addition.

```tsx
'use client';

import {
  useEffect,
  useRef,
  type FormEvent,
  type ReactNode,
} from 'react';
import {
  HighWire,
  type HighWireInstance,
} from '@/lib/highwire/highwire-v<version>.es.js';

async function collectDeviceDataOrNull(hw: HighWireInstance) {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

interface CheckoutFormProps {
  buildExistingPaymentRequest: () => Record<string, unknown>;
  children: ReactNode;
}

export function CheckoutForm({
  buildExistingPaymentRequest,
  children,
}: CheckoutFormProps) {
  const formRef = useRef<HTMLFormElement | null>(null);
  const hwRef = useRef<HighWireInstance | null>(null);

  useEffect(() => {
    const form = formRef.current;
    if (!form) return;

    const hw = HighWire();
    hwRef.current = hw;

    const cardEl = form.querySelector<HTMLInputElement>('#card-number');
    const cvcEl = form.querySelector<HTMLInputElement>('#card-cvc');
    const expEl = form.querySelector<HTMLInputElement>('#card-exp');
    const zipEl = form.querySelector<HTMLInputElement>('#billing-zip');
    const unobserves = [
      cardEl && hw.observeField(cardEl, 'number'),
      cvcEl && hw.observeField(cvcEl, 'cvc'),
      expEl && hw.observeField(expEl, 'exp'),
      zipEl && hw.observeField(zipEl, 'zip'),
    ].filter(Boolean) as Array<() => void>;

    return () => {
      unobserves.forEach((unobserve) => unobserve());
      if (hwRef.current === hw) hwRef.current = null;
    };
  }, []);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const existingPaymentRequest = buildExistingPaymentRequest();
    const hw = hwRef.current;
    const deviceData = hw ? await collectDeviceDataOrNull(hw) : null;
    const response = await fetch('/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...existingPaymentRequest,
        ...(deviceData ? { deviceData } : {}),
      }),
    });

    if (!response.ok) throw new Error('Payment request failed');
  }

  return (
    <form ref={formRef} onSubmit={onSubmit}>
      {children}
    </form>
  );
}
```

The browser request still goes to your own backend. Follow the [integration flow](/docs/highwire-js/integration#forward-devicedata-to-highwire) to pass the collected object through on the HighWire request.


# Plain HTML (/docs/payment-sdk/guides/plain-html)



## 1. Add the checkout markup [#1-add-the-checkout-markup]

Load the SDK once, before your same-origin integration file. The URL and SRI below are the
matching 0.7.0 pair; preserve the served file unchanged.

```html
<button id="start-payment" type="button">Continue to payment</button>
<section id="checkout-controls" inert aria-label="Payment">
  <form id="billing" aria-label="Contact and billing details">
    <input name="customerEmail" type="email" autocomplete="email" aria-label="Email" required>
    <input name="name" autocomplete="billing name" aria-label="Name" required>
    <input name="country" autocomplete="billing country" aria-label="Country code"
           pattern="[A-Za-z]{2}" required>
    <input name="line1" autocomplete="billing address-line1" aria-label="Address">
    <input name="line2" autocomplete="billing address-line2" aria-label="Apartment, suite">
    <input name="city" autocomplete="billing address-level2" aria-label="City">
    <input name="state" autocomplete="billing address-level1" aria-label="State">
    <input name="postalCode" autocomplete="billing postal-code" aria-label="Postal code" required>
  </form>
  <div id="payment"></div>
  <button id="pay" type="button" hidden disabled>Pay</button>
</section>
<p id="payment-status" role="status"></p>

<script src="https://cdn.highwirepayments.com/sdk/0.7.0/highwire.js"
        integrity="sha384-NZVoZbDdfN3Ix90/7E+1ojIBysBz1Cwhizp9zPhjUxaOVkMdHBk9RSSxMSAO44LU"
        crossorigin="anonymous"></script>
<script src="/checkout.js" defer></script>
```

The outer section starts `inert`, so card, wallet, and drop-in controls cannot be used until
your backend binds the payment to the order. Keep the container laid out; hiding it can prevent
components from measuring themselves. The CDN bundles the wallet loader.

The email and billing fields are read each time the shopper pays, so a refused email or address can
be corrected in place. Every card payment needs the email. A
[drop-in form that uses your Pay button](/docs/payment-sdk/drop-in-and-wallets#drop-in-forms-that-use-your-pay-button)
needs the full address, at least the `name`, a two-letter `country` and the `postalCode`; the card
field sends `line1`, `city`, `state` and `postalCode`. Use your own labels and styling, and prefill the fields from your
earlier checkout steps if you have them; the example prefills the email from the order.

## 2. Provide merchant endpoints [#2-provide-merchant-endpoints]

The following `/api/orders/current` and `/api/orders/{id}/payment` routes are **your merchant
backend**, not SDK endpoints:

1. `GET /api/orders/current` reconciles any existing payment and returns `{ id, amount, currency, customerEmail, canStartPayment }`. Use integer cents and USD currency. Set `canStartPayment: true` only when the server authorizes a new attempt; otherwise send the shopper to order status before mounting.
2. `POST /api/orders/{id}/payment` accepts `{ paymentId }` and binds it atomically. Allow an idempotent repeat of the same pair; reject conflicts.

Follow the [server binding and verification requirements](/docs/payment-sdk/authentication#bind-and-verify-orders-on-your-server)
for authorization, same-origin JSON and cookie-based CSRF protection, merchant ownership,
authoritative amount and currency checks, eligibility, unique constraints, and reconciliation.
Never overwrite a paid or unresolved payment; fulfill only from verified capture.

## 3. Create `/checkout.js` [#3-create-checkoutjs]

Flow: initialize on the shopper action → mount with the server amount → bind atomically → enable
the inert controls → verify and reconcile on the server. This is a complete safe example,
including teardown and retry boundaries; replace the publishable key with your sandbox key.

<details>
  <summary>
    Complete checkout.js example
  </summary>

  ```js
  (() => {
    const startButton = document.getElementById('start-payment');
    const controls = document.getElementById('checkout-controls');
    const billingForm = document.getElementById('billing');
    const payButton = document.getElementById('pay');
    const message = document.getElementById('payment-status');
    const sdk = window.HighWire;
    if (sdk?.SDK_VERSION !== '0.7.0') {
      startButton.disabled = true;
      message.textContent = 'Payment SDK unavailable. Reload to try again.';
      return;
    }
    const HighWire = sdk.default;
    const { HighWireError } = sdk;
    let instancePromise;
    let checkout;
    let formReady = false;
    let busy = false;
    let confirming = false;
    let mounting = false;
    let bound = false;
    let closed = false;
    let lastError;
    let disposed = false;
    let formFailed = false;

    const retryableRequestCodes = new Set([
      'HW_NETWORK',
      'HW_TIMEOUT',
      'HW_SERVICE_UNAVAILABLE',
    ]);

    function updateControls() {
      controls.inert = !bound || closed;
      payButton.hidden = !checkout?.requiresSubmit || closed;
      payButton.disabled = !bound || !formReady || busy || confirming || closed;
    }

    function reportError(error) {
      if (disposed || error === lastError) return;
      lastError = error;
      const code = error instanceof HighWireError ? error.code : null;
      const recovery = error instanceof HighWireError
        ? error.recovery ?? (code === 'HW_INCOMPLETE' ? 'correct' : undefined)
        : undefined;
      if (recovery === 'wait') return;
      if (recovery === 'correct' || recovery === 'retry' || recovery === 'recheck') {
        confirming = recovery === 'recheck';
        if (!confirming) busy = false;
        message.textContent = recovery === 'correct'
          ? 'Check your email, payment details and billing address, then try again.'
          : recovery === 'retry'
            ? 'Payment could not start. Try again.'
            : 'We are confirming your payment. Check your order status before paying again.';
        updateControls();
        return;
      }
      const retryableSubmit = busy && retryableRequestCodes.has(code);
      formFailed = ['HW_IFRAME_UNRECOVERABLE', 'HW_PROTOCOL_MISMATCH'].includes(code)
        || (code === 'HW_TIMEOUT' && !busy && !mounting);
      closed = formFailed || !retryableSubmit;
      if (closed) {
        formReady = false;
        checkout?.unmount();
      }
      busy = false;
      message.textContent = formFailed
        ? 'Payment form unavailable. Check your order status before reloading.'
        : code === 'HW_RATE_LIMITED'
        ? 'Too many attempts. Wait, then check your order status before trying again.'
        : closed
        ? 'Payment has stopped. Check your order before trying again.'
        : 'Payment could not complete. Try again.';
      updateControls();
    }

    startButton.addEventListener('click', async () => {
      if (disposed || startButton.disabled) return;
      startButton.disabled = true;
      message.textContent = 'Preparing payment…';
      try {
        const orderResponse = await fetch('/api/orders/current');
        if (disposed) return;
        if (!orderResponse.ok) throw new Error('Order unavailable');
        const order = await orderResponse.json();
        if (disposed) return;
        if (order.canStartPayment !== true) {
          closed = true;
          startButton.hidden = true;
          throw new Error('Check the existing order payment before starting again');
        }
        instancePromise ??= HighWire.init({
          publishableKey: 'REPLACE_WITH_YOUR_TEST_PUBLISHABLE_KEY',
          apiBase: 'https://api-payments-staging.highwirepayments.com',
        }).catch((error) => {
          instancePromise = undefined;
          throw error;
        });
        const hw = await instancePromise;
        if (disposed) return;
        billingForm.elements.customerEmail.value ||= order.customerEmail ?? '';
        mounting = true;
        const mounted = await hw.mount({
          container: '#payment',
          amount: order.amount,
          currency: order.currency,
          onReady: () => {
            if (!disposed && !closed) message.textContent = 'Enter your payment details.';
          },
          onAvailabilityChange: (available) => {
            if (disposed) return;
            formReady = available;
            updateControls();
          },
          onChange: (card) => {
            if (disposed) return;
            formReady = card.complete;
            updateControls();
          },
          onProcessing: () => {
            if (disposed) return;
            busy = true;
            message.textContent = 'Processing your payment…';
            updateControls();
          },
          onResult: (result) => {
            if (disposed) return;
            busy = false;
            if (result.status === 'declined' && result.retryable) {
              message.textContent = 'Declined. Try another card or method.';
            } else {
              closed = true;
              checkout?.unmount();
              message.textContent = result.status === 'pending'
                ? 'We are confirming your payment. Check your order status.'
                : result.status === 'succeeded'
                  ? 'Payment submitted. Your server will confirm the order.'
                  : 'This checkout has ended. Check your order before starting again.';
            }
            updateControls();
          },
          onError: reportError,
        });
        mounting = false;
        if (disposed) {
          mounted.unmount();
          return;
        }
        checkout = mounted;
        if (closed) checkout.unmount();
        const binding = await fetch(`/api/orders/${encodeURIComponent(order.id)}/payment`, {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ paymentId: checkout.paymentId }),
        });
        if (disposed) return;
        if (!binding.ok) throw new Error('Payment could not be bound to the order');
        bound = true;
        startButton.hidden = true;
        updateControls();
      } catch (error) {
        mounting = false;
        if (disposed) return;
        if (checkout) {
          checkout.unmount();
          closed = true;
        } else {
          const code = error instanceof HighWireError ? error.code : null;
          if (!formFailed && retryableRequestCodes.has(code)) {
            startButton.disabled = false;
            closed = false;
            formReady = false;
            message.textContent = 'Payment unavailable. Try entering payment again.';
            updateControls();
            return;
          }
          closed = true;
          formReady = false;
          message.textContent = formFailed
            ? 'Payment form unavailable. Check your order status before reloading.'
            : code === 'HW_RATE_LIMITED'
              ? 'Too many attempts. Wait, then reload to check your order before trying again.'
              : 'Payment could not start. Check your order status before trying again.';
          updateControls();
          return;
        }
        reportError(error);
      }
    });

    payButton.addEventListener('click', async () => {
      if (disposed || !checkout || payButton.disabled || !billingForm.reportValidity()) return;
      const { customerEmail, ...billingAddress } = Object.fromEntries(new FormData(billingForm));
      busy = true;
      updateControls();
      try {
        await checkout.submit({ customerEmail, billingAddress });
      } catch (error) {
        reportError(error);
      } finally {
        busy = false;
        updateControls();
      }
    });

    window.addEventListener('pagehide', () => {
      disposed = true;
      bound = false;
      closed = true;
      startButton.disabled = true;
      checkout?.unmount();
      updateControls();
    });
    window.addEventListener('pageshow', (event) => {
      if (event.persisted) {
        message.textContent = 'Checkout paused. Reload to check your order before paying.';
      }
    });
  })();
  ```
</details>

If the CDN script is blocked or fails integrity verification, the example keeps payment
inactive and shows a reload message.

`onError` and rejection handling receive the same SDK error; this example displays it once.
Do not process the resolved submit result again: `onResult` already handles it.

The same code covers every layout. A checkout reports either `onChange` (card field) or
`onAvailabilityChange` (a [drop-in form that uses your Pay button](/docs/payment-sdk/drop-in-and-wallets#drop-in-forms-that-use-your-pay-button)),
and both set `formReady`. The card field ignores the billing `name`, `country` and `line2`. Errors from
that drop-in form carry [`recovery`](/docs/payment-sdk/api-reference#checkoutrecovery): `correct`
and `retry` let the shopper fix the email, card or billing fields and pay again, `wait` changes nothing,
and `unavailable` stops the controls. `recheck` disables Pay and sends the shopper to their order
status, as `pending` does; the form stays mounted so a result already on its way still arrives.
To let the shopper check from the page instead, add a button that calls
[`checkout.recheck()`](/docs/payment-sdk/api-reference#checkoutrecheck) once the running
`submit()` has settled.

This example sends the billing fields from your page, so it suits a drop-in form mounted with the
default `billingAddressForm: 'none'`. With `'full'`, call `checkout.submit({ customerEmail })`
without a `billingAddress` and keep only the email field.

## 4. Handle leaving and restarting [#4-handle-leaving-and-restarting]

For in-page navigation, call `checkout.unmount()` when the payment step leaves the DOM and remove
your event listeners. Keep the instance if returning to the same unresolved order. Disable order
editing while payment is being confirmed.

After `HW_NETWORK`, request-level `HW_TIMEOUT`, or `HW_SERVICE_UNAVAILABLE` without a
`recovery`, the example permits an immediate retry of the same operation. A form-loading
timeout, rate limit, configuration or authorization error, non-retryable decline, ended
checkout, reload, or abandoned verification stops the controls. Reconcile on your backend
before offering another payment; see
[the lifecycle rules](/docs/payment-sdk/integration#4-order-changes-and-cleanup).
The disposed flag blocks continuations after `pagehide`, including a checkout that finishes
mounting after teardown. A page restored from the back/forward cache stays inactive; reload to
rerun the backend eligibility check before mounting. A binding request already sent may finish
on the server after leaving, so keep reconciling it there.

Use the [wallet and CSP guide](/docs/payment-sdk/drop-in-and-wallets) to configure enabled methods.


# React / Next.js (/docs/payment-sdk/guides/react-nextjs)



## 1. Install the SDK types [#1-install-the-sdk-types]

```bash
npm install @highwirepayments/payment-sdk@0.7.0
```

The package supplies TypeScript declarations. This example loads the self-contained CDN build
at runtime and uses only type imports from npm; no additional runtime dependency is needed.

In Next.js, put the checkout in a client component. Initialize and mount from browser code after
the payment container exists. This example starts payment from a shopper action, so React's
Strict Mode effect replay does not create payments.

Your backend reconciles existing payments before setting `canStartPayment`, supplies the
authoritative amount, and implements the atomic binding and same-origin JSON contract from the
[Plain HTML guide](/docs/payment-sdk/guides/plain-html#2-provide-merchant-endpoints). Follow the
[server binding and verification requirements](/docs/payment-sdk/authentication#bind-and-verify-orders-on-your-server).

## 2. Add the payment component [#2-add-the-payment-component]

Flow: initialize on the shopper action → mount with the server amount → bind atomically → enable
the inert controls → verify and reconcile on the server. This is a complete safe component,
including async teardown and retry boundaries. Replace the test publishable key.

<details>
  <summary>
    Complete PaymentStep component
  </summary>

  ```tsx
  'use client';

  import { useEffect, useRef, useState } from 'react';
  import type {
    BillingAddress,
    HighWire,
    HighWireError,
    Checkout,
    HighWireInstance,
  } from '@highwirepayments/payment-sdk';

  type SDKNamespace = {
    default: typeof HighWire;
    HighWireError: typeof HighWireError;
    SDK_VERSION: string;
  };

  declare global {
    interface Window {
      HighWire?: SDKNamespace;
    }
  }

  let sdkPromise: Promise<SDKNamespace> | null = null;

  const retryableRequestCodes = new Set([
    'HW_NETWORK',
    'HW_TIMEOUT',
    'HW_SERVICE_UNAVAILABLE',
  ]);

  function loadSDK(): Promise<SDKNamespace> {
    if (window.HighWire?.SDK_VERSION === '0.7.0') {
      return Promise.resolve(window.HighWire);
    }
    sdkPromise ??= new Promise<SDKNamespace>((resolve, reject) => {
      const script = document.createElement('script');
      script.src = 'https://cdn.highwirepayments.com/sdk/0.7.0/highwire.js';
      script.integrity = 'sha384-NZVoZbDdfN3Ix90/7E+1ojIBysBz1Cwhizp9zPhjUxaOVkMdHBk9RSSxMSAO44LU';
      script.crossOrigin = 'anonymous';
      script.onload = () => {
        if (window.HighWire?.SDK_VERSION === '0.7.0') resolve(window.HighWire);
        else {
          script.remove();
          reject(new Error('Payment SDK unavailable'));
        }
      };
      script.onerror = () => {
        script.remove();
        reject(new Error('Payment SDK could not load'));
      };
      document.head.appendChild(script);
    }).catch((error) => {
      sdkPromise = null;
      throw error;
    });
    return sdkPromise;
  }

  function sdkError(error: unknown) {
    const ErrorClass = window.HighWire?.HighWireError;
    return ErrorClass && error instanceof ErrorClass ? error : null;
  }

  type Order = {
    id: string;
    amount: number;
    currency: 'usd';
    customerEmail: string;
    canStartPayment: boolean;
  };

  export function PaymentStep({ order }: { order: Order }) {
    const containerRef = useRef<HTMLDivElement>(null);
    const billingRef = useRef<HTMLFormElement>(null);
    const instanceRef = useRef<Promise<HighWireInstance> | null>(null);
    const checkoutRef = useRef<Checkout | null>(null);
    const aliveRef = useRef(false);
    const startingRef = useRef(false);
    const busyRef = useRef(false);
    const mountingRef = useRef(false);
    const formFailedRef = useRef(false);
    const lastErrorRef = useRef<unknown>(null);
    const [starting, setStarting] = useState(false);
    const [bound, setBound] = useState(false);
    const [formReady, setFormReady] = useState(false);
    const [busy, setBusy] = useState(false);
    const [confirming, setConfirming] = useState(false);
    const [closed, setClosed] = useState(false);
    const [requiresSubmit, setRequiresSubmit] = useState(false);
    const [message, setMessage] = useState('');

    useEffect(() => {
      aliveRef.current = true;
      return () => {
        aliveRef.current = false;
        checkoutRef.current?.unmount();
      };
    }, []);

    function reportError(error: unknown) {
      if (!aliveRef.current || error === lastErrorRef.current) return;
      lastErrorRef.current = error;
      const failure = sdkError(error);
      const code = failure?.code ?? null;
      const recovery = failure?.recovery ?? (code === 'HW_INCOMPLETE' ? 'correct' : undefined);
      if (recovery === 'wait') return;
      if (recovery === 'correct' || recovery === 'retry' || recovery === 'recheck') {
        setConfirming(recovery === 'recheck');
        if (recovery !== 'recheck') {
          busyRef.current = false;
          setBusy(false);
        }
        setMessage(recovery === 'correct'
          ? 'Check your email, payment details and billing address, then try again.'
          : recovery === 'retry'
            ? 'Payment could not start. Try again.'
            : 'We are confirming your payment. Check your order status before paying again.');
        return;
      }
      const retryableSubmit = busyRef.current && code !== null
        && retryableRequestCodes.has(code);
      const formFailed = code === 'HW_IFRAME_UNRECOVERABLE'
        || code === 'HW_PROTOCOL_MISMATCH'
        || (code === 'HW_TIMEOUT' && !busyRef.current && !mountingRef.current);
      if (formFailed) {
        formFailedRef.current = true;
        setFormReady(false);
        setClosed(true);
        checkoutRef.current?.unmount();
      } else if (!retryableSubmit) {
        setFormReady(false);
        setClosed(true);
        checkoutRef.current?.unmount();
      }
      busyRef.current = false;
      setBusy(false);
      setMessage(formFailed
        ? 'Payment form unavailable. Check your order status before reloading.'
        : code === 'HW_RATE_LIMITED'
        ? 'Too many attempts. Wait, then check your order status before trying again.'
        : !retryableSubmit
        ? 'Payment has stopped. Check your order before trying again.'
        : 'Payment could not complete. Try again.');
    }

    async function startPayment() {
      if (startingRef.current || !containerRef.current) return;
      startingRef.current = true;
      setStarting(true);
      if (!order.canStartPayment) {
        setClosed(true);
        setMessage('Check the existing order payment before starting again.');
        return;
      }
      setMessage('Preparing payment…');
      try {
        const sdk = await loadSDK();
        if (!aliveRef.current || !containerRef.current) return;
        instanceRef.current ??= sdk.default.init({
          publishableKey: 'REPLACE_WITH_YOUR_TEST_PUBLISHABLE_KEY',
          apiBase: 'https://api-payments-staging.highwirepayments.com',
        }).catch((error) => {
          instanceRef.current = null;
          throw error;
        });
        const hw = await instanceRef.current;
        if (!aliveRef.current || !containerRef.current) return;
        mountingRef.current = true;
        const checkout = await hw.mount({
          container: containerRef.current,
          amount: order.amount,
          currency: order.currency,
          onReady: () => {
            if (aliveRef.current && !formFailedRef.current) {
              setMessage('Enter your payment details.');
            }
          },
          onAvailabilityChange: (available) => {
            if (aliveRef.current) setFormReady(available);
          },
          onChange: (card) => {
            if (aliveRef.current) setFormReady(card.complete);
          },
          onProcessing: () => {
            busyRef.current = true;
            if (!aliveRef.current) return;
            setBusy(true);
            setMessage('Processing your payment…');
          },
          onResult: (result) => {
            if (!aliveRef.current) return;
            busyRef.current = false;
            setBusy(false);
            if (result.status === 'declined' && result.retryable) {
              setMessage('Declined. Try another card or method.');
              return;
            }
            setClosed(true);
            checkoutRef.current?.unmount();
            setMessage(result.status === 'pending'
              ? 'We are confirming your payment. Check your order status.'
              : result.status === 'succeeded'
                ? 'Payment submitted. Your server will confirm the order.'
                : 'This checkout has ended. Check your order before starting again.');
          },
          onError: reportError,
        });
        mountingRef.current = false;
        checkoutRef.current = checkout;
        if (!aliveRef.current) {
          checkout.unmount();
          return;
        }
        if (formFailedRef.current) checkout.unmount();
        const response = await fetch(`/api/orders/${encodeURIComponent(order.id)}/payment`, {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ paymentId: checkout.paymentId }),
        });
        if (!response.ok) throw new Error('Payment could not be bound to the order');
        if (!aliveRef.current) return;
        setRequiresSubmit(checkout.requiresSubmit);
        setBound(true);
      } catch (error) {
        mountingRef.current = false;
        if (checkoutRef.current) {
          checkoutRef.current.unmount();
          if (aliveRef.current) {
            setClosed(true);
            setMessage('Payment unavailable. Check your order before trying again.');
          }
        } else if (aliveRef.current) {
          const code = sdkError(error)?.code ?? null;
          if (!formFailedRef.current && code !== null
              && retryableRequestCodes.has(code)) {
            startingRef.current = false;
            setStarting(false);
            formFailedRef.current = false;
            setClosed(false);
            setFormReady(false);
            setMessage('Payment unavailable. Try entering payment again.');
          } else {
            setFormReady(false);
            setClosed(true);
            setMessage(formFailedRef.current
              ? 'Payment form unavailable. Check your order status before reloading.'
              : code === 'HW_RATE_LIMITED'
                ? 'Too many attempts. Wait, then reload to check your order before trying again.'
                : 'Payment could not start. Check your order status before trying again.');
          }
        }
      }
    }

    async function pay() {
      const checkout = checkoutRef.current;
      const billing = billingRef.current;
      if (!checkout || !billing || !bound || !formReady || busyRef.current || confirming
          || closed || !billing.reportValidity()) return;
      const { customerEmail, ...billingAddress } = Object.fromEntries(new FormData(billing)) as
        BillingAddress & { customerEmail?: string };
      busyRef.current = true;
      setBusy(true);
      try {
        await checkout.submit({ customerEmail, billingAddress });
      } catch (error) {
        reportError(error);
      } finally {
        busyRef.current = false;
        if (aliveRef.current) setBusy(false);
      }
    }

    return (
      <section aria-label="Payment">
        {!bound && !closed && (
          <button type="button" disabled={starting} onClick={startPayment}>
            Continue to payment
          </button>
        )}
        <div inert={!bound || closed}>
          <form ref={billingRef} aria-label="Contact and billing details">
            <input name="customerEmail" type="email" autoComplete="email" aria-label="Email"
              defaultValue={order.customerEmail} required />
            <input name="name" autoComplete="billing name" aria-label="Name" required />
            <input name="country" autoComplete="billing country" aria-label="Country code"
              pattern="[A-Za-z]{2}" required />
            <input name="line1" autoComplete="billing address-line1" aria-label="Address" />
            <input name="line2" autoComplete="billing address-line2" aria-label="Apartment, suite" />
            <input name="city" autoComplete="billing address-level2" aria-label="City" />
            <input name="state" autoComplete="billing address-level1" aria-label="State" />
            <input name="postalCode" autoComplete="billing postal-code" aria-label="Postal code"
              required />
          </form>
          <div ref={containerRef} />
          {requiresSubmit && !closed && (
            <button type="button" disabled={!formReady || busy || confirming} onClick={pay}>
              Pay
            </button>
          )}
        </div>
        <p role="status">{message}</p>
      </section>
    );
  }
  ```
</details>

The loader fetches the integrity-checked CDN file once; npm imports remain type-only. The
`inert` wrapper blocks payment until binding without hiding the SDK container. `requiresSubmit`
controls the Pay button, and callbacks handle completeness, processing, and duplicate errors.

The component covers every layout. `onChange` (card field) and `onAvailabilityChange` (a
[drop-in form that uses your Pay button](/docs/payment-sdk/drop-in-and-wallets#drop-in-forms-that-use-your-pay-button))
both set `formReady`; a checkout reports only one of them. The email and billing fields are read
each time the shopper pays, so a refused email or address can be corrected in place. Every card
payment needs the email; the drop-in form needs the full address, at least the `name`, a two-letter
`country` and the `postalCode`, and the card field sends `line1`, `city`, `state` and `postalCode`. The component prefills
the email from the order; prefill the address from your earlier checkout steps if you have it.

Errors from that drop-in form carry
[`recovery`](/docs/payment-sdk/api-reference#checkoutrecovery): `correct` and `retry` let the
shopper fix the email, card or billing fields and pay again, `wait` changes nothing, and `unavailable`
stops the controls. `recheck` disables Pay and sends the shopper to their order status, as
`pending` does; the form stays mounted so a result already on its way still arrives. To let the
shopper check from the page instead, add a button that calls
[`checkout.recheck()`](/docs/payment-sdk/api-reference#checkoutrecheck) once the running
`submit()` has settled. The component suits the default `billingAddressForm: 'none'`; with
`'full'`, call `checkout.submit({ customerEmail })` without a `billingAddress` and keep only the
email field.

## 3. Keep order and payment lifecycle aligned [#3-keep-order-and-payment-lifecycle-aligned]

Keep the component mounted while the shopper pays and disable order editing during confirmation.
If the order or totals change, reconcile first, then render a fresh component with a new React
`key`. Cleanup unmounts late async work, but cannot cancel a charge; reconcile every bound payment.

After `HW_NETWORK`, request-level `HW_TIMEOUT`, or `HW_SERVICE_UNAVAILABLE` without a
`recovery`, the component permits an immediate retry of the same operation. A form-loading
timeout, rate limit, configuration or authorization error, non-retryable decline, pending
result, abandoned verification, ended checkout, or reload stops the controls. Show order status
and let the backend decide whether a new payment is allowed; see
[order changes and cleanup](/docs/payment-sdk/integration#4-order-changes-and-cleanup).

Configure [wallet domains and CSP](/docs/payment-sdk/drop-in-and-wallets) before production.


# Authorize and capture a payment (/docs/gateway-payments/payments/authAndCapture_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Authorize a payment (/docs/gateway-payments/payments/auth_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Capture an authorized payment (/docs/gateway-payments/payments/capture_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Charge a saved payment method (/docs/gateway-payments/payments/chargeSavedMethod_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Create a hosted checkout link (/docs/gateway-payments/payments/createCheckoutSession_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Retrieve a payment (/docs/gateway-payments/payments/getPayment_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Refund a captured payment (/docs/gateway-payments/payments/refund_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Reissue a hosted checkout link (/docs/gateway-payments/payments/reissueCheckoutLink_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}



# Void an authorized payment (/docs/gateway-payments/payments/void_v1)



{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

