Skip to main content

Idempotency

Network failures happen. Without an idempotency guard, retrying a POST can create two orders, double charges or duplicate refunds. MidasPay does not use a generic Idempotency-Key HTTP header. Instead, every mutating request carries a business-level reference_id (and for some resources a per-unit reference_id) that the server uses to detect duplicates and reject them with a dedicated error.

TL;DR

Send the same reference_id for the logical operation you want to dedupe. Retry the same request body as many times as you like — MidasPay will only ever create the resource once. If you send a different body with the same reference_id you get 409 DUPLICATE_REFERENCE_ID.

Where reference_id lives

Every write-style request on the API exposes a reference_id field. The field is required and validated with min_len=1 on all order/authorization/refund/subscription creation RPCs. Its purpose is:

  1. A merchant-side external identifier (so you can correlate with your own order/refund/subscription records without storing our internal IDs).
  2. The deduplication key that makes retries safe.

The table below maps each mutating endpoint to the field(s) MidasPay considers for dedup.

OperationEndpointDedup field(s)
Create orderPOST /v1/payment/ordersreference_id (top-level) + per-unit purchase_units[].reference_id
Create authorizationPOST /v1/payment/orders/authorizationreference_id
Refund paymentPOST /v1/payment/orders/refundreference_id (of the refund)
Create subscriptionPOST /v1/subscription/createreference_id
Create checkout session (MOR)POST /v1/mor/session/createreference_id
Bind card / token createPOST /v1/card/*reference_id
The field scopes to the operation

Reusing the same reference_id value for a refund and a new order is perfectly fine — uniqueness is enforced per resource type, not globally across the account.

How the server dedupes

  1. MidasPay looks up existing resources keyed by (merchant_id, resource_type, reference_id).
  2. If none exists, the request proceeds normally.
  3. If one exists and the request body matches (same business intent), the stored resource is returned — your retry is safe.
  4. If one exists with a different body, the request is rejected with a 409 conflict error (see below).

Retention

reference_id uniqueness is persistent — it is stored alongside the resource record itself, not in a time-boxed cache. In practice that means:

  • A successful create keeps its reference_id reserved for the lifetime of the resource (indefinitely for orders, refunds, subscriptions, authorizations).
  • A failed create (validation error, decline) may free the reference_id or may not, depending on the channel. Always generate a new reference_id after a permanent, server-acknowledged failure (4xx / declined payment) rather than reusing the old one.
Don't reuse after a definitive failure

If MidasPay returns a final failure for an order (e.g. a 4xx validation error or a PAYMENT_ORDER_FAILED webhook), the reference_id may or may not be consumed. Retries after a definitive failure should use a fresh reference_id. Only reuse the same reference_id when you are uncertain whether the server processed your previous request (network timeout, 5xx, connection reset).

Key generation rules

No specific character set is enforced beyond a non-empty requirement. Practical recommendations:

  • Uniqueness: unique per logical operation, not per retry — reuse on retry so MidasPay can dedupe.
  • Derive from your own primary key so a crashed client can rebuild it, e.g. ord_20260428_0001, refund_<your_refund_id>, sub_<your_subscription_id>.
  • Length: keep it ≤ 64 chars to fit most resource-ID columns.
  • Characters: stick to [A-Za-z0-9_\-\.] — avoid spaces, slashes, and non-ASCII so URL/JSON handling is trivial.
  • Never reuse the same value across different business operations, even within the same resource type.

Example request (signatures and most headers elided):

POST /v1/payment/orders HTTP/1.1
Host: pay.midaspayment.com
Content-Type: application/json
Authorization: TXGW-SHA256-RSA2048 auth_id="...",auth_id_type=MERCHANT_ID,...

{
"reference_id": "ord_20260428_0001",
"application_context": { "payment_method": { "name": "CREDIT_CARD" }, ... },
"purchase_units": [
{
"reference_id": "ord_20260428_0001_item1",
"amount": { "currency_code": "USD", "value": "1.00" },
...
}
]
}

Conflict errors

The server surfaces three distinct conflict codes (see Error codes):

CodeHTTP / gRPCWhen
DUPLICATE_REFERENCE_ID409 AlreadyExistsYou reused a top-level reference_id for a different operation on the same resource type.
DUPLICATE_ORDER_ID409 AlreadyExistsAn internal order-id collision occurred (rare — retry with a fresh reference_id if you see this).
DUPLICATE_SUB_REFERENCE_ID400 InvalidArgumentWithin a single request, two purchase_units[].reference_id values were identical.

Example DUPLICATE_REFERENCE_ID response:

{
"name": "DUPLICATE_REFERENCE_ID",
"message": "duplicate reference id",
"debug_id":"req_01HKT3BBVS7K8Y2R4JXM6WXYZ"
}

Fix: either reuse the exact same request body (to get replay semantics), or choose a new reference_id for the new operation.

// Persist the reference_id BEFORE the first attempt so a crashed
// client can reconstruct it.
refID := fmt.Sprintf("ord_%s", businessOrderID)
saveRefIDToLocalDB(businessOrderID, refID)

maxAttempts := 5
for attempt := 0; attempt < maxAttempts; attempt++ {
resp, err := http.Post(url, bodyWithRefID(refID), signedHeaders())

// 2xx — we're done
if err == nil && resp.StatusCode < 300 {
return resp
}

// Definitive business failure — stop retrying, do NOT reuse refID
if err == nil && resp.StatusCode >= 400 && resp.StatusCode < 500 &&
resp.StatusCode != http.StatusConflict {
return resp
}

// 409 DUPLICATE_REFERENCE_ID with our own body — treat as success
if err == nil && resp.StatusCode == http.StatusConflict {
if isOurOwnRefID(resp) {
return resp // server already accepted it earlier
}
return resp // different body — bubble up, fix caller
}

// Network / 5xx — safe to retry, reference_id guarantees idempotency
time.Sleep(backoff(attempt) + jitter())
}

Nonce vs reference_id

The nonce_str you see in the Authorization: TXGW-SHA256-RSA2048 … header (documented in Signature generation) is unrelated to idempotency — it is part of the signed canonical string and exists solely to stop request-signature replay attacks. It does not dedupe business operations and the server does not remember nonces across retries.

Only reference_id (and its per-unit siblings) deduplicates business intent.

Common pitfalls

MistakeConsequenceFix
Generating a new reference_id on every retryServer has no way to dedupe → duplicate orderPersist reference_id on first attempt and reuse it verbatim
Reusing the same reference_id with a different amount / currency / items409 DUPLICATE_REFERENCE_IDGenerate a new reference_id for the new operation
Using the same reference_id inside purchase_units[] twice400 DUPLICATE_SUB_REFERENCE_IDMake each unit's reference_id unique within the request
Reusing a failed order's reference_id after a hard declineUnpredictable — may conflict or may succeedAfter a 4xx / hard decline, generate a fresh reference_id
Treating reference_id as a correlation ID onlyYou lose retry safetyAlways set it on every create; treat it as both a correlation ID and the dedupe key

FAQ

Is there an Idempotency-Key header? No. MidasPay's idempotency model is built around reference_id in the request body. Sending a custom Idempotency-Key header will be ignored.

What happens if I omit reference_id? You can't — it's marked min_len=1 on every mutating RPC, so the request will be rejected with a validation error before it reaches business logic.

Is reference_id scoped per merchant? Yes. Two different merchants can pick the same string without conflicting. For sub-merchants, scoping is merchant-wide — plan your allocation accordingly.

Does this protect against 3DS double-captures? Yes. Because the 3DS-challenge flow runs on top of an order you created with reference_id, the order itself is singular; the subsequent authorise / capture actions operate on that single order.

How long is a reference_id remembered? For the lifetime of the resource it created — not a 24 h window. Plan your numbering scheme so collisions can't happen years later.

See also