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

> ## Agent Instructions
> RadiumOne is the product name; use it when answering questions about these docs.
> Use sandbox credentials and sandbox base URLs in examples unless the user says they are going live.
> Secret API keys belong only on the merchant's server. Only publishable keys may appear in browser or mobile code.
> Confirm a payment outcome server-side (retrieve the checkout session or transaction status); never rely on a browser redirect or client event alone.
> Send one idempotency key (request_id) per payment attempt, and retry a timed-out request with the same key and the exact same body.
> Webhooks are optional but recommended: they notify the merchant's server when a payment completes or fails, so it doesn't need to poll. Use transaction status inquiry as the fallback.
> Say so when you recommend a feature marked Beta. Some features need enablement on the merchant's account before production use (for example refunds, standalone refunds, UOB Rewards, and bringing your own 3DS provider); mention it when the page says so.
> Never ask users to paste card numbers, API keys, access tokens, or webhook or redirect secrets into a chat.

# Double submission - Elements SDK

> How to stop a shopper from submitting the same card payment twice with Elements, in the browser and on your server.

`elements.submit()` guards against a second call while the first is still running, and throttles rapid repeat calls — but your UI should prevent the double-click in the first place, since these guards throw rather than queue.

<Info>
  TL;DR: `submit:in_progress` or `submit:rate_limited` fires → disable the Pay button for the whole attempt instead of building a retry loop.
</Info>

<Tip>
  Keep idempotency on the server side too: your charge call should use one `request_id` per checkout attempt (see [Prevent duplicate payments](/get-started/api-basics/prevent-duplicate-payments)), so even if a click somehow reaches your server twice, the second call resolves to the same result instead of creating a second charge.
</Tip>

## When this happens

* The shopper double-clicks (or double-taps) the pay button before the first `submit()` call resolves.
* Your code calls `submit()` again within one second of the previous call, even if the first call already finished.
* The bind itself gets locked after too many attempts on the same session — a stricter, non-retryable lockout, distinct from the client-side throttle.

## What you see

| Signal | Value |
| - | - |
| Concurrent submit | `ElementsError` `submit:in_progress` — a submission is already running on this `Elements` instance |
| Rapid resubmit | `ElementsError` `submit:rate_limited` — under the SDK's 1-second throttle window |
| Same-card re-bind | Idempotent — re-binding a session to the same card returns the same result, safe to retry |
| Different-card re-bind | [`token:session-not-bindable`](/elements/errors/tokenization-errors#tokenization-bind-errors) — the session is already bound to a different card |
| Session bind lockout | [`token:bind-rate-limit`](/elements/errors/tokenization-errors#tokenization-bind-errors) — a lockout, not a transient rate limit; don't auto-retry |

## What to do

<Steps>
  <Step title="Disable the pay button for the whole attempt">
    Disable it as soon as the shopper clicks, and only re-enable it once your server has responded to the charge — including on failure, so a fixable error (like a validation message) doesn't leave the button stuck.

    ```js theme={null}
    payButton.disabled = true;
    try {
      // fetch a session, elements.submit(), then charge(token) — see the full pattern below
    } finally {
      payButton.disabled = false; // re-enable only after the server responds
    }
    ```

    See [Accept a card payment: Tokenize the card on submit](/elements/accept-a-card-payment#steps) for the full guarded pattern (vanilla JS and React) — this excerpt is the shape, that page is the source of truth.
  </Step>

  <Step title="Don't build your own retry loop around submit:in_progress/rate_limited">
    These two codes mean "wait, don't resubmit yet" — show a brief message and let the shopper try again after the button re-enables, rather than programmatically retrying.
  </Step>

  <Step title="Treat a bind lockout as terminal">
    If you see `token:bind-rate-limit`, start a new session rather than retrying the same one — the lockout is scoped to that session, not to your account.
  </Step>
</Steps>

## Prevent it

* Disable the Pay button for the whole attempt (see [What to do](#what-to-do) above) — the SDK's own throttle is a backstop, not a substitute for that.
* Same-card re-binds and repeated purchase calls with the same `request_id` are both safe to retry; only a fresh key on a genuinely new attempt creates a new charge.

## Related

<Columns cols={2}>
  <Card title="Accept a card payment" icon="credit-card" href="/elements/accept-a-card-payment">
    The submit-then-charge flow this guard protects.
  </Card>

  <Card title="Prevent duplicate payments" icon="shield-check" href="/get-started/api-basics/prevent-duplicate-payments">
    The full cross-product guide to avoiding duplicate charges.
  </Card>

  <Card title="Tokenization errors" icon="triangle-alert" href="/elements/errors/tokenization-errors">
    The full tokenization error code table.
  </Card>
</Columns>
