Idempotency and Retries

Know which Payment Labs operations are safe to repeat, which can create duplicates, and how to recover when a request times out.

A request can time out after Payment Labs has done the work. Before you retry, you need to know whether repeating the request is harmless or creates a second object. This page answers that for every operation that changes data.

Before you start

Read Errors for which status codes to retry and how to back off.

The short version

  • Payment Labs has no idempotency-key header. Sending the same request twice is safe only where this page says so.
  • Creating batches, payments, checkouts, refunds, plans and offers is not idempotent. A retry can create a duplicate. Check first, using your referenceId.
  • Moving money on an existing object (submitting a payment, charging a checkout, approving and paying a payment request) is protected: a repeat never pays twice.
  • Wallet credits and charges are idempotent when you send a referenceId. Wallet load refunds are always idempotent on their required externalId.

Operations that are not idempotent

Each call creates a new object, even with the same body. Payment Labs does not check referenceId for uniqueness on these.

OperationAfter a timeout or 5xx
Create BatchSearch batches by referenceId with Search Batches. Create again only if nothing is found.
Create Payment, Create Virtual PaymentSearch payments by referenceId and batchId with Search Payments.
Create ImportList the batch's imports with Get Batch Imports. An unused import does no harm; you can delete it.
Create CheckoutSearch checkouts by referenceId with Search Checkouts.
Refund Checkout TransactionSearch refunds by checkoutId with Search Refunds. While a refund on the checkout is in progress, another refund request is refused with 409, but once it completes, a repeat refunds again.
Refund Subscription PaymentList the cycle's refunds with List Subscription Payment Refunds. This operation does not refuse a repeat while a refund is in progress, so a blind retry refunds twice.
Create Subscription Plan, Create Plan OfferSearch plans or offers and look for the one you created.

Pattern for a safe create:

async function createPaymentOnce(token: string, batchId: string, body: { referenceId: string } & Record<string, unknown>) {
  const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
  const base = "https://api-sandbox.paymentlabs.io";
  try {
    const created = await fetch(`${base}/batches/${batchId}/payments`, { method: "POST", headers, body: JSON.stringify(body) });
    if (created.ok) return created.json();
    if (created.status < 500) throw new Error(`Create failed with ${created.status}: ${await created.text()}`);
  } catch (error) {
    if (!(error instanceof TypeError)) throw error; // network failure: fall through and check
  }
  await new Promise((resolve) => setTimeout(resolve, 5000));
  const search = await fetch(`${base}/payments/search`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      filter: {
        all: [
          { fact: "batchId", operator: "equals", value: batchId },
          { fact: "referenceId", operator: "equals", value: body.referenceId },
        ],
      },
      pageSize: 10,
    }),
  }).then((r) => r.json() as Promise<{ data: unknown[] }>);
  if (search.data.length > 0) return search.data[0];
  throw new Error("Payment was not created; it is safe to send the create request again");
}

This works only if each referenceId you send is unique in your system. Use your own invoice, order or transfer ID.

Operations that are safe to repeat

OperationWhat a repeat does
Submit PaymentPayment Labs accepts one submission per payment at a time. A repeat while the first is queued or in progress returns 409. A payment is never paid twice by repeated submits.
Submit BatchSubmits the batch's eligible payments again; payments already queued or paid are not paid again.
Cancel PaymentHarmless. Once the payment is CANCELLED, a repeat returns 423 NOT_EDITABLE.
Remove Auto SubmitHarmless.
Patch Batch, Update PaymentSets the same values again.
Get Widget URLFinds the existing payee by email and reference ID and returns a new URL.
Charge CheckoutA checkout is charged at most once at a time. A repeat while a charge is pending or after it succeeded is refused (409 or 423). Read the checkout to see the result.
Approve Payment RequestsApproving an approved request succeeds again with no change.
Approve Payment Requests and PayConverges on the same batch and payments. A repeat does not create a second payment for the same requests.
Credit Wallet, Charge WalletWith a referenceId: the same referenceId and the same terms return the existing credit or charge with 200; different terms return 409 CREDIT_ALREADY_EXISTS or 409 CHARGE_ALREADY_EXISTS. Without a referenceId there is no protection, and a repeat moves money again. Always send one.
Refund Wallet LoadThe same externalId with the same amount, currency, load and reason returns the existing refund with 200 and sends nothing to the card again; different terms return 409 REFUND_ALREADY_EXISTS.
Bulk Invite Wallet HoldersHolders already invited are returned with every created flag false.
Resolve Wallet DisputeNever moves money twice. A repeat returns the same result or 409.
Add WebhookRegistering the same event and destination again does nothing and returns 204.
Delete WebhookReturns 204 even if the webhook is already gone.

Resend Webhook is the exception by design: each call delivers the event to your endpoint again.

Retrying a payout safely

  1. Create the payment with a unique referenceId. On a timeout, search for it before creating again.
  2. Submit it. On a timeout, read the payment with Get Payment:
    • status is still DRAFT and isSubmittable is true: submit again.
    • Any status past DRAFT (SUBMITTED, PROCESSING and so on): the first submit worked. Do nothing.
  3. A FAILED or RETURNED payment can be submitted again after you fix the cause (for example after the payee updates their bank account). That is a new payout attempt for the same payment, not a duplicate payment.

Handling duplicate webhooks

Webhooks are delivered at least once, and a resend delivers the same event again. Make your handler idempotent: key your processing on the object ID and its status, and ignore an event that does not change what you already recorded. See Webhooks.

Next steps


What’s Next

Did this page help you?