Idempotency

Idempotency ensures that retrying a request produces the same result as the original request, preventing duplicate resource creation. This is essential for payment operations where network failures or timeouts may require request retries.


Overview

An idempotency key is a unique identifier included in the request header. When Martis receives a request with an idempotency key:

ScenarioBehavior
First requestProcessed normally; response cached against the key
Retry with same key and payloadReturns the cached response without reprocessing
Same key with different payloadReturns a 409 Conflict error

Idempotency keys are valid for 24 hours after the initial request.


Implementation

Include the Idempotency-Key header in POST requests to supported endpoints:

Request with idempotency key

curl https://api-staging.martis.id/api/v1/payments/charges \
  --request POST \
  --header 'Authorization: Bearer {API_KEY}' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: order_12345_payment_v1' \
  --data '{
    "payment_method": "qris",
    "channel": "gudang_voucher",
    "amount": 50000,
    "currency": "idr",
    "customer_details": {
      "email": "customer@example.com"
    }
  }'

Supported Endpoints

Idempotency is supported on all POST endpoints that create resources:

EndpointResource
POST /v1/payments/chargesPayment charges
POST /v1/payoutsPayouts
POST /v1/transfersTransfers

Key Generation Strategies

Generate idempotency keys that are unique per logical operation and stable across retries.

Recommended Approaches

StrategyExampleUse Case
UUID550e8400-e29b-41d4-a716-446655440000General purpose
Business identifierorder_12345_paymentTied to business objects
Composite keyuser_123_invoice_456_attempt_1Multi-entity operations
Hash-basedSHA256 of operation detailsDeterministic generation

Key Format

  • Name
    Idempotency-Key
    Type
    string
    Description

    A unique string identifying the logical operation. Maximum length: 255 characters.


Best Practices

Key Generation

  • Tie to business operations — Use order IDs, invoice numbers, or other business identifiers
  • Avoid randomization on retry — The same operation must use the same key across retries
  • Include version or attempt — Append a version if the same operation may be intentionally repeated

Storage and Tracking

  • Store the mapping — Maintain a record linking business identifiers to idempotency keys
  • Handle key expiration — Idempotency keys expire after 24 hours; generate new keys for operations after expiration
  • Log for debugging — Record idempotency keys in logs for troubleshooting

Retry Logic

  • Retry on network errors — Safe to retry with the same key when the request fails due to network issues
  • Retry on 5xx responses — Server errors indicate the request may not have been processed
  • Do not retry on 4xx responses — Client errors require fixing the request, not retrying

Error Handling

Idempotency Conflict (409)

Returned when the same idempotency key is used with a different request payload:

Response 409

{
  "status": "fail",
  "data": {
    "message": "Idempotency key already used with different parameters"
  }
}

Resolution: Generate a new idempotency key if the operation parameters have changed.

Successful Retry

When a request is successfully retried, the original response is returned:

Cached response returned

{
  "status": "success",
  "data": {
    "id": "01HZCHARGE123456ABCDEF",
    "payment_method": "qris",
    "status": "pending"
  }
}

The response is identical to the original, including the same resource ID.


Examples

Payment Charge with Idempotency

Create payment charge

curl https://api-staging.martis.id/api/v1/payments/charges \
  --request POST \
  --header 'Authorization: Bearer {API_KEY}' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: checkout_789_charge' \
  --data '{
    "payment_method": "virtual_account",
    "channel": "mandiri",
    "amount": 150000,
    "currency": "idr",
    "customer_details": {
      "email": "customer@example.com",
      "name": "Customer Name"
    },
    "client_reference_id": "checkout_789"
  }'

Payout with Idempotency

Create payout

curl https://api-staging.martis.id/api/v1/payouts \
  --request POST \
  --header 'Authorization: Bearer {API_KEY}' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: vendor_payment_PO2024001' \
  --data '{
    "amount": 500000,
    "currency": "idr",
    "destination": {
      "type": "bank_account",
      "bank_account": {
        "bank_code": "bca",
        "account_number": "1234567890",
        "account_holder_name": "Vendor Name"
      }
    },
    "client_reference_id": "PO-2024-001"
  }'

Transfer with Idempotency

Create transfer

curl https://api-staging.martis.id/api/v1/transfers \
  --request POST \
  --header 'Authorization: Bearer {API_KEY}' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: revenue_share_invoice_456' \
  --data '{
    "destination_account_id": "01HZDEF456789012ABCDEF",
    "amount": 100000,
    "currency": "idr",
    "client_reference_id": "invoice_456_share"
  }'

Retry Implementation

JavaScript Example

Retry with idempotency

async function createPaymentWithRetry(paymentData, maxRetries = 3) {
  const idempotencyKey = `payment_${paymentData.orderId}_${Date.now()}`;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch('https://api-staging.martis.id/api/v1/payments/charges', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey
        },
        body: JSON.stringify(paymentData)
      });
      
      if (response.status < 500) {
        return await response.json();
      }
      
      // Retry on 5xx errors
      if (attempt < maxRetries - 1) {
        await sleep(Math.pow(2, attempt) * 1000);
      }
    } catch (error) {
      // Retry on network errors
      if (attempt < maxRetries - 1) {
        await sleep(Math.pow(2, attempt) * 1000);
      } else {
        throw error;
      }
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Troubleshooting

IssueCauseSolution
409 Conflict on first requestKey previously used with different payloadGenerate a new unique key
Duplicate resources createdIdempotency key not includedAdd Idempotency-Key header
Different response on retryKey expired (24 hours)Generate a new key for expired operations
Key rejected as invalidKey exceeds 255 charactersShorten the key format

Was this page helpful?