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 in UUID v4 format:

123e4567-e89b-12d3-a456-426614174000
ResourceUsed as
Merchantmerchant_id in account creation
Accountaccount_id, source_account_id, destination_account_id
Payout RequestPath param in GET /v2/payout-requests/{id}
PayoutPath param in GET /v2/payouts/{id}
Payment IntentPath param in GET /v2/payment-intents/{id}
Funding MethodPath param in GET /v2/funding-methods/{id}

IDs are returned in every create and get response. Store them in your database — they are the stable reference for polling and webhook reconciliation.

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;
}
}
}