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
Recommended patterns
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
| Scenario | Result |
|---|---|
| Same key, same parameters | Original response returned (no new resource created) |
| Same key, different parameters | 409 Conflict — key collision detected |
| Different key, same parameters | New 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:
| Resource | ID prefix | Example |
|---|---|---|
| Merchant | mer_ | mer_abc123 |
| Account | acc_ | acc_def456 |
| Payout Request | pr_ | pr_ghi789 |
| Payout | pay_ | pay_jkl012 |
| Payment Intent | pi_ | pi_mno345 |
| Funding Method | fm_ | 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;
}
}
}
Related
- Errors — error codes including
IDEMPOTENCY_CONFLICT - Send a Single Payout — payout guide with idempotency example
- Handle Payout Failures — retry strategies for failed payouts