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.
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:
- A merchant-side external identifier (so you can correlate with your own order/refund/subscription records without storing our internal IDs).
- The deduplication key that makes retries safe.
The table below maps each mutating endpoint to the field(s) MidasPay considers for dedup.
| Operation | Endpoint | Dedup field(s) |
|---|---|---|
| Create order | POST /v1/payment/orders | reference_id (top-level) + per-unit purchase_units[].reference_id |
| Create authorization | POST /v1/payment/orders/authorization | reference_id |
| Refund payment | POST /v1/payment/orders/refund | reference_id (of the refund) |
| Create subscription | POST /v1/subscription/create | reference_id |
| Create checkout session (MOR) | POST /v1/mor/session/create | reference_id |
| Bind card / token create | POST /v1/card/* | reference_id |
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
- MidasPay looks up existing resources keyed by
(merchant_id, resource_type, reference_id). - If none exists, the request proceeds normally.
- If one exists and the request body matches (same business intent), the stored resource is returned — your retry is safe.
- 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_idreserved for the lifetime of the resource (indefinitely for orders, refunds, subscriptions, authorizations). - A failed create (validation error, decline) may free the
reference_idor may not, depending on the channel. Always generate a newreference_idafter a permanent, server-acknowledged failure (4xx / declined payment) rather than reusing the old one.
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):
| Code | HTTP / gRPC | When |
|---|---|---|
DUPLICATE_REFERENCE_ID | 409 AlreadyExists | You reused a top-level reference_id for a different operation on the same resource type. |
DUPLICATE_ORDER_ID | 409 AlreadyExists | An internal order-id collision occurred (rare — retry with a fresh reference_id if you see this). |
DUPLICATE_SUB_REFERENCE_ID | 400 InvalidArgument | Within 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.
Recommended client pattern
// 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
| Mistake | Consequence | Fix |
|---|---|---|
Generating a new reference_id on every retry | Server has no way to dedupe → duplicate order | Persist reference_id on first attempt and reuse it verbatim |
Reusing the same reference_id with a different amount / currency / items | 409 DUPLICATE_REFERENCE_ID | Generate a new reference_id for the new operation |
Using the same reference_id inside purchase_units[] twice | 400 DUPLICATE_SUB_REFERENCE_ID | Make each unit's reference_id unique within the request |
Reusing a failed order's reference_id after a hard decline | Unpredictable — may conflict or may succeed | After a 4xx / hard decline, generate a fresh reference_id |
Treating reference_id as a correlation ID only | You lose retry safety | Always 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
- Webhooks → Deduplication — dedupe inbound notifications on
id, notreference_id. - Error codes — full catalogue including
DUPLICATE_*conflicts. - Signature generation — explains
nonce_strand why it's not an idempotency key.