Guides

Idempotency

Retry deposits and withdrawals safely without moving money twice.

A network timeout does not tell you whether a deposit or withdrawal was created. Idempotency keys let you retry without risk: the same key always means the same transaction.

Where keys are required

Endpoint Key
POST /deposits Required
POST /withdrawals Required
POST /deposits/{id}/otp Not used. It acts on an existing transaction.
POST /withdrawals/{id}/confirm Not used. Fetch the transaction before retrying.
POST /withdrawals/{id}/resend Not used.

A required key that is missing fails with 400 IDEMPOTENCY_KEY_REQUIRED.

Sending the key

Send it in the Idempotency-Key header:

http
POST /api/v2/deposits
Idempotency-Key: dep_user_1842_order_0001

If you cannot set headers, send idempotency_key in the JSON body instead. When both are present, the header wins.

Format: 8 to 120 characters of letters, digits, ., _, : and -. Anything else fails with 400 INVALID_REQUEST.

Choosing keys

Use one key per logical attempt and save it before you send the request. Build it from your own ids so a retry naturally reuses it:

text
dep_<your customer id>_<your order id>
wd_<your customer id>_<your withdrawal request id>

A random UUID also works, as long as you store it and reuse it on retries. Keys are scoped to your Deripay account.

What happens on a repeat

You send Result
Same key, same request 200, the same transaction (with its latest state), and the header Idempotent-Replayed: true. Nothing new is created.
Same key, different request 409 IDEMPOTENCY_KEY_REUSED.
New key A new transaction.

"Same request" means the same type (deposit or withdrawal), connection, country, amount, phone number and operator. Phone numbers are compared after they are normalised, so 0712345678 and 254712345678 count as the same number.

The first successful request returns 201. A replay returns 200. Check Idempotent-Replayed if you need to tell them apart.

Timeouts and 5xx errors

If a create call times out, or returns 502 UPSTREAM_UNAVAILABLE or another 5xx, the request may still have landed. Retry with the same key and the same body, with backoff:

ts
async function createDepositWithRetry(body: object, idempotencyKey: string) {
  for (let attempt = 0; attempt < 5; attempt++) {
    try {
      const res = await fetch("https://deriapi.site/api/v2/deposits", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.DERIPAY_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(60_000),
      });
      const json = await res.json();
      if (json.success) return json.data;
      const retryable = res.status === 429 || res.status >= 500;
      if (!retryable) throw new Error(json.error.code);
      const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
      await new Promise((r) => setTimeout(r, wait * 1000));
    } catch (err) {
      if (err instanceof Error && !/abort|timeout|fetch failed/i.test(err.message)) throw err;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
    }
  }
  throw new Error("Deposit not confirmed yet; retry later with the same key");
}

While Deripay waits for the network to confirm, the transaction can show status: "SUBMISSION_PENDING" (state: "pending"). The retry picks it up.

Never switch to a new key because a request timed out. That can charge the customer twice.

Refused requests

When the payment network refuses a request outright with a 4xx, for example WITHDRAWAL_ALREADY_ACTIVE or WITHDRAWAL_COOLDOWN, nothing was created and the key is released. You can send the same logical request again later with the same key. It is treated as a new attempt.

Validation errors from the gateway (INVALID_REQUEST, AMOUNT_OUT_OF_RANGE, INVALID_PHONE_NUMBER, …) are checked before anything is recorded, so they also do not use up the key.