Skip to main content

IDs & Idempotency

Networks fail. Requests time out. Users click "Submit" twice. NextAPI uses idempotency keys to ensure that retrying a request never creates a duplicate transaction.

What is idempotency?

An idempotent operation produces the same result no matter how many times it is called with the same input. In payment systems this is critical — a network timeout on a payout request should not result in two payouts being sent.

The X-Idempotency-Key header

Include X-Idempotency-Key in any request that creates or mutates a resource:

POST /v2/payout-requests HTTP/1.1
Authorization: Basic YOUR_ENCODED_CREDENTIALS
Content-Type: application/json
X-Idempotency-Key: payout-user-456-2024-01-15-001

{
"account_id": "acc_abc123",
"amount": 10000,
"currency": "PHP",
...
}

If NextAPI has already processed a request with this key, it returns the original response — no new transaction is created.

Key generation strategies

A good idempotency key is:

  • Unique per intended operation — two different payouts must have different keys
  • Reproducible — the same retry attempt uses the same key
  • Collision-resistant — keys must not accidentally match between unrelated operations

Based on your internal reference:

payout-{your_internal_id}
payout-order-789
payment-intent-checkout-123

Based on operation + timestamp (for retries within a session):

payout-{user_id}-{date}-{sequence}
payout-user-456-2024-01-15-001

UUID v4 (generated once, stored for retries):

import { randomUUID } from "crypto";

// Generate once, store it, reuse on retry
const idempotencyKey = randomUUID();
// → "f47ac10b-58cc-4372-a567-0e02b2c3d479"

Behavior on duplicate requests

ScenarioResult
Same key, same parametersOriginal response returned (no new resource created)
Same key, different parameters409 Conflict — key collision detected
Different key, same parametersNew resource created (duplicate is your responsibility to prevent)

Checking if a response is from cache

NextAPI includes a header indicating whether the response came from the idempotency cache:

X-Idempotency-Replayed: true

When this header is true, the response body is identical to the original response.

Expiration policy

Idempotency keys are retained for 24 hours after the original request. After this window:

  • The same key can be reused for a new operation
  • There is no longer protection against duplicate requests using the old key

For long-running retry logic (e.g., a failed payout you want to retry the next day), generate a new key rather than reusing the old one.

Resource IDs

Every resource NextAPI creates has a system-generated id:

ResourceID prefixExample
Merchantmer_mer_abc123
Accountacc_acc_def456
Payout Requestpr_pr_ghi789
Payoutpay_pay_jkl012
Payment Intentpi_pi_mno345
Funding Methodfm_fm_pqr678

External IDs

You can also supply your own external_id when creating resources to link NextAPI entities to your internal records:

{
"external_id": "your-internal-order-id-789",
"amount": 10000,
...
}

Retrieve resources by external ID using dedicated endpoints:

  • GET /v2/payout-requests?external_id={id}
  • GET /v2/funding-methods/by-external-id/{external_id}
  • GET /v2/payment-intents/by-external-id/{external_id}

External IDs must be unique within your account. They are not system-generated — you control them.

Implementing retry logic

async function createPayoutWithRetry(payload, maxRetries = 3) {
// Generate idempotency key ONCE before any attempts
const idempotencyKey = `payout-${payload.externalId}`;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(
"https://api.partners.nextpay.world/v2/payout-requests",
{
method: "POST",
headers: {
Authorization:
"Basic " +
Buffer.from("YOUR_CLIENT_ID:YOUR_CLIENT_SECRET").toString(
"base64"
),
"Content-Type": "application/json",
// Same key on every retry attempt
"X-Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(payload),
}
);

if (response.ok) return response.json();

const { error } = await response.json();

// Don't retry non-transient errors
if (response.status < 500 && response.status !== 429) throw new Error(error.message);

// Exponential backoff
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
} catch (err) {
if (attempt === maxRetries - 1) throw err;
}
}
}