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

# Accept a Payment

> Build a production-safe Payment Element checkout from card input through backend creation, 3DS, and final order state handling.

Use this page when you are moving from a demo integration to a production-safe checkout.

## Production-safe flow

1. Disable duplicate submits in the browser.
2. Call `elements.submit(...)` once per checkout attempt.
3. Let your backend create the transaction.
4. Let `elements.submit(...)` complete card authentication when the payment requires it.
5. Resume the transaction if the buyer is interrupted mid-payment.
6. Fulfill only from webhook or reconciliation state.

## Frontend submit pattern

```js theme={null}
let isSubmitting = false;
let lastTokenData = null;

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  if (isSubmitting) return;

  isSubmitting = true;

  const result = await elements.submit({
    createTransaction: async (tokenData) => {
      lastTokenData = tokenData;

      const response = await fetch("/api/pay", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          token: tokenData.token,
          amount: 2490,
          orderId: "order_2001",
        }),
      });

      const payload = await response.json();
      return payload.data ?? payload;
    },
  });

  isSubmitting = false;

  if (result.status === "error") {
    messageEl.textContent = result.error ?? "Payment failed.";
    return;
  }

  if (lastTokenData) {
    cardSummaryEl.textContent = `${lastTokenData.brand} ending in ${lastTokenData.last4}`;
  }
});
```

`elements.submit(...)` drives the whole attempt. It creates an Element session if needed, asks the
hosted card field to tokenize, calls your `createTransaction` callback with the token payload, then
completes any card authentication the payment requires before resolving.

Authentication needs no extra input from your checkout form. It uses the `buyer` and `products` your
backend already sends when it creates the transaction. See
[3D Secure](/frontend/payment-element/three-d-secure).

## Resume an interrupted payment

If the buyer reloads or navigates away mid-payment, resume the same transaction instead of starting
a new attempt:

```js theme={null}
const result = await elements.resume({
  transactionId: savedTransactionId,
});
```

Persist the transaction id as soon as your backend returns it.

<Warning>
  `resume()` cannot recover a payment that was waiting on card authentication. It returns `failed` —
  start a new payment attempt. See [3D Secure](/frontend/payment-element/three-d-secure).
</Warning>

## Token payload

Your `createTransaction` callback receives non-sensitive card metadata with the token:

```json theme={null}
{
  "token": "pgct_token_from_browser",
  "brand": "visa",
  "last4": "4242",
  "exp_month": "12",
  "exp_year": "2030"
}
```

Send `tokenData.token` to your backend to create the transaction. Use `brand`, `last4`, `exp_month`, and `exp_year` only for provisional checkout UI or your own backend bookkeeping; final payment state still comes from the transaction response, webhook, or reconciliation.

## Backend request example

```json theme={null}
{
  "external_ref": "order_2001",
  "amount": 2490,
  "currency": "BRL",
  "method": "credit_card",
  "token": "pgct_token_from_browser",
  "installments": 1
}
```

## Backend response example

```json theme={null}
{
  "success": true,
  "requestId": "0190a2b4-18a7-7de0-9a43-69b7cf261201",
  "data": {
    "id": "018f1f2e-7b43-7c9a-8d3e-1a2b3c4d5e70",
    "status": "three_ds_required",
    "next_action": {
      "type": "three_ds_challenge"
    }
  }
}
```

Return this payload to the browser unchanged. `next_action` is opaque — `elements.submit(...)` reads
it and completes card authentication for you.

## Common error

```json theme={null}
{
  "type": "https://api.pagou.ai/problems/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "The request contains invalid data.",
  "errors": [
    {
      "field": "token",
      "message": "Token is required for credit card payments",
      "code": "invalid_type"
    }
  ]
}
```

Fix: do not create the transaction until the browser has a token from Payment Element. If a challenge flow was interrupted, reconcile the transaction before letting the customer retry.

## Invalid state handling

Keep the submit button disabled until the card field reports a valid state:

```js theme={null}
let cardIsValid = false;

card.on("change", ({ valid, brand, errors }) => {
  cardIsValid = valid;
  submitButton.disabled = !cardIsValid || isSubmitting;
  brandEl.textContent = brand ?? "";
  errorEl.textContent = Object.values(errors ?? {})[0] ?? "";
});

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  if (!cardIsValid || isSubmitting) return;

  // Call elements.submit(...) here.
});
```

If `elements.submit(...)` returns `{ "status": "error" }`, do not call your backend again with a missing or stale token. Show the returned error, let the buyer correct the card details, then run a new checkout attempt.

## Final-state rule

A browser success message is not enough to fulfill an order. Final fulfillment belongs to webhook-confirmed or reconciled payment state.
