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

# Payment Setup

> Discover available payment methods, express checkout, and process payments

# Payment Setup

The payment module is the core of the Headless SDK. It lets you discover which payment methods are configured for a store, tokenize cards, and process payments with **automatic 3DS, redirect, and polling handling**.

***

## Discover Available Payment Methods

Every store has a **payment setup config** — a map of payment methods (card, Apple Pay, Google Pay, Klarna, etc.) with their enabled/disabled state, processor IDs, and flow IDs.

```typescript theme={null}
const setup = await tagada.payment.getPaymentSetup(checkoutSessionId);

// Example response:
// {
//   "card": { enabled: true, method: "card", paymentFlowId: "pf_xxx" },
//   "apple_pay:stripe": { enabled: true, method: "apple_pay", express: true, processorId: "proc_xxx" },
//   "google_pay:stripe": { enabled: true, method: "google_pay", express: true },
//   "klarna:stripe": { enabled: true, method: "klarna", processorId: "proc_stripe" },
// }
```

Where the config comes from, in order: an explicit `paymentSetupConfig` you pass in, the
config injected on Tagada-hosted pages, and otherwise the payment methods enabled on the
store for that checkout session (resolved from the API). Keys are `method` or
`method:provider` — so on a self-hosted page an Apple Pay integration shows up as
`"apple_pay"`, the card form as `"card"`. An empty object means no payment method is
enabled for the session's store/currency.

### Just the enabled method keys

```typescript theme={null}
const methods = await tagada.payment.getEnabledMethods(checkoutSessionId);
// ["card", "apple_pay:stripe", "google_pay:stripe"]
```

### Express methods with browser availability

```typescript theme={null}
const express = await tagada.payment.getExpressMethods(checkoutSessionId);
// {
//   applePay: { available: true, processorId: "proc_xxx" },
//   googlePay: { available: true },
//   klarna: { available: true, processorId: "proc_stripe" },
// }
```

The SDK automatically checks `ApplePaySession.canMakePayments()` in the browser to determine Apple Pay availability.

***

## Card Payment Flow

### Recommended: `processPayment()`

`processPayment()` is the **high-level orchestrator** that handles the entire payment lifecycle automatically:

1. Submits payment to the processor
2. If 3DS / bank auth is required → redirects the user and resumes on return
3. If the payment is async → polls until a terminal status
4. Returns a typed `ProcessPaymentResult`

```typescript theme={null}
// 1. Tokenize
const { tagadaToken } = await tagada.payment.tokenizeCard({
  cardNumber: '4242424242424242',
  expiryDate: '12/28',
  cvc: '123',
  cardholderName: 'Jane Doe',
});

// 2. Process payment (3DS, redirects, polling all handled)
const result = await tagada.payment.processPayment({
  checkoutSessionId: session.id,
  tagadaToken,
});

// 3. Handle result
switch (result.status) {
  case 'succeeded':
    console.log('Payment confirmed!', result.order);
    break;
  case 'requires_redirect':
    // In React, usePayment() handles this automatically.
    // In vanilla JS, redirect manually:
    window.location.href = result.redirectUrl;
    break;
  case 'failed':
    console.error('Payment failed:', result.error);
    break;
  case 'pending':
    console.log('Still processing...', result.paymentId);
    break;
}
```

<Note>
  `tokenizeCard()` requires `@tagadapay/core-js` as an optional peer dependency. Install it: `npm install @tagadapay/core-js`.
</Note>

### Return type: `ProcessPaymentResult`

The return value is a **discriminated union** — check `result.status` for exhaustive handling:

| Status              | Fields                                             | Meaning                                      |
| ------------------- | -------------------------------------------------- | -------------------------------------------- |
| `succeeded`         | `payment`, `order`                                 | Payment confirmed. Show success screen.      |
| `requires_redirect` | `redirectUrl`, `method?`, `postData?`, `paymentId` | User must visit a URL (3DS, bank auth, APM). |
| `failed`            | `error`, `payment?`                                | Payment declined or errored.                 |
| `pending`           | `paymentId`                                        | Still processing (rare — polling timed out). |

### 3DS Return URL

After a 3DS redirect, the bank sends the user back to your page with query parameters. The SDK auto-detects these and resumes the payment:

* **React (`usePayment`)**: Handled automatically on mount — detects `?paymentAction=...` params, polls for result, fires callbacks.
* **Vanilla JS**: Call `tagada.payment.maybeResumeFromUrl()` on page load. That is the one-liner. `resumeAfterRedirect(paymentId)` is the lower-level equivalent if you already parsed the query yourself.

`returnUrl` passed to `processPayment()` (defaults to `window.location.href`) **must** be the page that runs this resume hook. After 3DS the bank sends the shopper back there.

```typescript theme={null}
// Vanilla JS — first thing on page load
const resumed = await tagada.payment.maybeResumeFromUrl();
if (resumed?.status === 'succeeded') {
  window.location.href = `/thank-you?orderId=${resumed.order?.id}`;
} else if (resumed?.status === 'failed') {
  showError(resumed.error);
}
// null → no 3DS return, render the checkout as usual
```

***

## Express Checkout

### Apple Pay

```typescript theme={null}
const result = await tagada.payment.processApplePay({
  checkoutSessionId: session.id,
  applePayToken: applePayEvent.payment.token,
});
```

<Note>
  **Merchant-scoped Apple Pay domains.** When the checkout domain runs
  merchant-scoped Apple Pay — either the merchant's own Apple certificates
  (BYOK) or Tagada's platform-integrator registration — the backend attaches a
  domain-keyed `applePayByok` map to the `apple_pay` method's metadata. The SDK
  resolves the current hostname against that map automatically and sends the
  matching `merchant_registration_id` to Basis Theory during tokenization, so no
  code change is needed. Pass `merchantRegistrationId` explicitly on
  `processApplePay` only to override the automatic resolution, or call
  `tagada.payment.resolveApplePayRegistration()` yourself when minting the
  Apple Pay merchant session (its `merchantRegistrationId` and `displayName`
  must go into the Basis Theory `POST /apple-pay/session` call for these
  domains).
</Note>

### Google Pay

```typescript theme={null}
const result = await tagada.payment.processGooglePay({
  checkoutSessionId: session.id,
  googlePayToken: paymentData.paymentMethodData.tokenizationData.token,
});
```

### Redirect APMs (Klarna, iDEAL, etc.)

For the APM lifecycle rules (redirect → webhook, vouchers, recurring limits) see
[Alternative payment methods](/developer-tools/payments/alternative-payment-methods).

```typescript theme={null}
const result = await tagada.payment.processApm({
  checkoutSessionId: session.id,
  paymentMethod: 'klarna',
  processorId: 'proc_stripe',
});

if (result.payment.requireAction === 'redirect') {
  window.location.href = result.payment.requireActionData.redirectUrl;
}
```

***

## React Hook

```tsx theme={null}
import { usePayment } from '@tagadapay/headless-sdk/react';

function PaymentForm({ sessionId }) {
  const {
    processPayment,
    tokenizeCard,
    isProcessing,
    paymentSetup,
    loadPaymentSetup,
  } = usePayment({
    onPaymentSuccess: (result) => {
      router.push(`/thank-you?orderId=${result.order?.id}`);
    },
    onPaymentFailed: (result) => {
      setError(result.error);
    },
  });

  const handleSubmit = async () => {
    const { tagadaToken } = await tokenizeCard({ cardNumber, expiryDate, cvc });
    await processPayment({ checkoutSessionId: sessionId, tagadaToken });
    // 3DS redirects + return detection + polling all handled automatically
  };

  return (
    <button onClick={handleSubmit} disabled={isProcessing}>
      {isProcessing ? 'Processing...' : 'Pay Now'}
    </button>
  );
}
```

### Full Hook API

| Property / Method              | Description                                                           |
| ------------------------------ | --------------------------------------------------------------------- |
| `processPayment(opts)`         | High-level: tokenize → pay → 3DS → poll → callbacks. **Recommended.** |
| `pay(opts)`                    | Low-level: submit payment, returns raw `PayResult`. No auto-redirect. |
| `tokenizeCard(card)`           | Tokenize card via `@tagadapay/core-js`                                |
| `isProcessing`                 | True during payment processing                                        |
| `paymentSetup`                 | Cached payment method config                                          |
| `loadPaymentSetup(sessionId)`  | Fetch available payment methods                                       |
| `getExpressMethods(sessionId)` | Check Apple Pay / Google Pay availability                             |
| `onPaymentSuccess`             | Callback when payment succeeds (including after 3DS return)           |
| `onPaymentFailed`              | Callback when payment fails or is declined                            |

***

## Advanced API

For advanced use cases, you can create instruments and process payments separately:

```typescript theme={null}
// Create a reusable payment instrument
const { paymentInstrument } = await tagada.payment.createInstrument({
  tagadaToken,
  storeId: 'store_xxx',
  customerData: { email: 'jane@example.com' },
});

// Create 3DS session
const threeds = await tagada.payment.create3dsSession({
  paymentInstrumentId: paymentInstrument.id,
  sessionData: { sessionId: 'bt_session_xyz' },
});

// Process with instrument directly
const result = await tagada.payment.payDirect({
  checkoutSessionId: session.id,
  paymentInstrumentId: paymentInstrument.id,
  threedsSessionId: threeds.id,
});
```

## Test your integration

In sandbox, any 16-digit card works. Use this one:

|        |                       |
| ------ | --------------------- |
| Card   | `4242 4242 4242 4242` |
| Expiry | Any future date       |
| CVC    | Any 3 digits          |

1. Call `processPayment()` and confirm `result.status === 'succeeded'`.
2. Confirm `returnUrl` is the page that runs `maybeResumeFromUrl()` (vanilla) or `usePayment` (React). After a 3DS challenge the bank sends the shopper back there.

Full sandbox setup: [Sandbox testing](/developer-tools/node-sdk/sandbox-testing).

<Note>
  **Need even more control?** For instrument-level management, later charges (MIT — merchant-initiated, customer not present), auth+capture, or mobile apps, see [Accept a payment with your own cart](/developer-tools/payments/core-js-payments).
</Note>
