Retries and polling
The API answers two kinds of failure. A failure that retrying can fix carries a
Retry-After header. A failure that needs a different request carries a problem document
that names the field or the state.
The rule is short: retry reads, never retry a create, and poll to follow a transfer.
Read Retry-After first
| Status | Meaning | Retry? |
|---|---|---|
429 | Rate limit exceeded | Yes, after the number of seconds in Retry-After. |
503 | An upstream dependency failed | Yes, after the number of seconds in Retry-After. |
500 | Unexpected server failure | Yes, with backoff, when the request is safe to repeat. |
4xx | Your request is wrong | No. Fix the request. A repeat answers the same problem. |
The rate limit is 50 requests per minute per key. Retry-After is in seconds:
curl -i "https://api.mure.app/v2/transfers" -H "api-key: $MURE_API_KEY"HTTP/2 429
retry-after: 12
content-type: application/problem+json
Wait the header value, then send the same read again. When a 5xx carries no
Retry-After, back off exponentially: 1 second, 2, 4, 8, and stop at 30 seconds or five
attempts, whichever comes first.
Never retry a create
POST /transfers has no idempotency key. Two identical requests create two intents, and
each intent can move funds. So a create is the one call you never repeat automatically,
including when the response never arrives.
| Situation | What to do |
|---|---|
The create answered 201 | Keep the id. Nothing to retry. |
The create failed with a 4xx | Fix the request. The same body answers the same error. |
The create failed with 429/503 | No intent was created. Wait Retry-After, then send it once more, by hand or by a gate. |
| The response was lost (timeout) | Do not resend. List your transfers and match the intent you expected. |
After a lost response, GET /transfers returns every transfer for the key, newest first.
Match on the asset, the amount, and the destination you sent, or on the orderId if you
kept the request body. See List Transfers.
A retry gate looks like this:
import { ClientServiceError } from "@mure/sdk";
async function readWithRetry<T>(
call: () => Promise<T>,
attempts = 5,
): Promise<T> {
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
return await call();
} catch (error) {
if (!(error instanceof ClientServiceError)) throw error;
if (error.status !== 429 && error.status !== 503) throw error;
const wait = Math.min(1000 * 2 ** attempt, 30_000);
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
throw new Error("Retries exhausted");
}Use it around getTransfer, listTransfers, listTokens, and listProtocols. Do not
wrap createTransfer.
Poll to follow a transfer
Webhooks are not available yet, so polling is the supported way to follow a transfer to a terminal status.
GET /transfers/:id answers the current status. It is a read, so it is safe to repeat.
Recommended interval: 2 seconds, then back off to 10 seconds. Two seconds is 30 reads
per minute, which stays under the 50 per minute limit on a key that also makes other calls.
A faster poller hits 429 and spends its own budget on Retry-After waits.
Stop when the status is terminal:
| Status | Terminal | Meaning |
|---|---|---|
draft | No | Created, not submitted. |
pending | No | Submitted, not yet confirmed by the indexer. |
confirmed | Yes | Confirmed across every chain the intent touches. |
finalized | Yes | Closed by the finalization sweep. Success, like confirmed. |
failed | Yes | Execution reverted, or the deadline elapsed. |
expired | Yes | The deadline passed before submission. |
const terminal = new Set(["confirmed", "finalized", "failed", "expired"]);
let transfer = await client.getTransfer({ id });
for (
let attempt = 0;
!terminal.has(transfer.status) && attempt < 30;
attempt += 1
) {
const wait = attempt < 10 ? 2000 : 10_000;
await new Promise((resolve) => setTimeout(resolve, wait));
transfer = await client.getTransfer({ id });
}The SDK's own adapter polls the chain after a send with a fixed 1 second interval and a 120 attempt cap. That poll follows the transaction, not the intent, so keep your own status poll for the API record.
What a terminal failure does not mean
failed and expired are final for that intent. Building the same transfer again creates a
new intent with a new id, so it needs a fresh create rather than a retry of the old call.
Confirm the cause before you create the second intent, or you can send the same money twice.
See Error Handling for the problem document shape, and Intent Lifecycle for the state machine.