Skip to content
LogoLogo

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

StatusMeaningRetry?
429Rate limit exceededYes, after the number of seconds in Retry-After.
503An upstream dependency failedYes, after the number of seconds in Retry-After.
500Unexpected server failureYes, with backoff, when the request is safe to repeat.
4xxYour request is wrongNo. 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.

SituationWhat to do
The create answered 201Keep the id. Nothing to retry.
The create failed with a 4xxFix the request. The same body answers the same error.
The create failed with 429/503No 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:

StatusTerminalMeaning
draftNoCreated, not submitted.
pendingNoSubmitted, not yet confirmed by the indexer.
confirmedYesConfirmed across every chain the intent touches.
finalizedYesClosed by the finalization sweep. Success, like confirmed.
failedYesExecution reverted, or the deadline elapsed.
expiredYesThe 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.