Every response from the Murmo API follows one of two consistent shapes: a success envelope or an error envelope. Understanding both — and knowing when to retry vs. when to fix your request — is the foundation of a robust integration.
Success envelope
Every successful response wraps its payload in a data key:
{ "data": { "...": "..." } }
The shape inside data varies by endpoint (array, keyed object, single resource, or null). See Groups & Proposals → Response Envelopes for the full breakdown.
Error envelope
Every error returns a standard HTTP status code paired with a consistent JSON body:
{
"statusCode": 422,
"code": "INSUFFICIENT_FUNDS",
"message": "Your wallet has 12.50 USDC but this order needs 25.00 USDC.",
"retryable": false,
"details": { "required": "25.00", "available": "12.50", "currency": "USDC" },
"errorId": "8f3c2b1e-7a90-4c2d-9b1e-2f6a4c8d0e11"
}
| Field | Always present | Meaning |
|---|
statusCode | Yes | The HTTP status code — mirrors the response status line. |
code | Yes | A stable machine-readable code. Branch on this, never on message. |
message | Yes | Human-readable explanation. Wording may change between releases — do not parse it. |
retryable | Yes | true only when it is safe to resend the identical request (see Retry Semantics). |
details | When applicable | Machine-readable context for the error: amounts, symbol, region, and so on. Money values are decimal strings. |
errorId | Yes | Unique per error occurrence. Include this when filing a support request. |
The message field may occasionally be an array of validation strings rather than a single string.
Write your error handler to accept both string and string[].
| Header | When present | How to use it |
|---|
X-Request-Id | Every response | Correlation ID — log it. If you send the header yourself it is echoed back; otherwise it is generated. |
Retry-After | 429 and retryable 5xx responses | Seconds to wait before your next attempt. Honor it before applying your own backoff. |
HTTP status codes
| Status | When it appears | What to do |
|---|
200 OK | Successful response | Read data. |
400 Bad Request | Malformed, missing, or invalid input | Fix the request. Read code and details. |
401 Unauthorized | Missing or invalid API key | Check the Authorization: Bearer murmo_... header. |
403 Forbidden | Not a member, not the creator, geo-restricted, or an in-app-only action | Do not retry as-is. |
404 Not Found | Unknown resource ID (proposal, position, market, group) | Verify the ID. |
409 Conflict | Resource state forbids the action (market closed, proposal closed, already claimed) | Re-read state; check retryable. |
413 Payload Too Large | Request body exceeds size limit | Reduce payload size. |
422 Unprocessable | Input is valid but breaks a trading rule (insufficient funds, min size, slippage) | Adjust the order parameters. |
429 Too Many Requests | Rate limit exceeded (1,200 / 60s) | Back off using Retry-After. |
5xx | Upstream dependency rejected, unavailable, or unexpected server error | Retry with backoff if retryable: true; report if persistent. |
Retry semantics
retryable: true means “nothing moved — it is safe to resend the identical request.” It is not a synonym for “transient error.”
When retryable is true: A pre-flight failure occurred before any funds were touched or any order was submitted. Apply exponential backoff with jitter, honoring Retry-After when the header is present. Examples: RATE_LIMITED, ROUTE_NOT_FOUND, NO_MARKET_PRICE, MARKET_MAINTENANCE, SLIPPAGE_EXCEEDED, UPSTREAM_TIMEOUT, UPSTREAM_UNAVAILABLE, REQUEST_IN_PROGRESS, TRANSACTION_EXPIRED, FAIR_PRICE_REJECTED.
When retryable is false: The error is terminal — fix the request or the account state, then try again. For write operations that may have already been submitted, do not retry blindly. Instead, reconcile by reading /positions or /trades to determine whether the action completed. Blind retries can double-submit orders (idempotency keys are coming in a future release).
Key error codes
The full machine-readable catalog of every code, its status, and retryable flag is published at error-catalog.json and is generated directly from the API source, so it never drifts. The codes you are most likely to encounter:
code | Status | retryable | Meaning |
|---|
INSUFFICIENT_FUNDS | 422 | false | Not enough balance. details includes required and available. |
INVALID_AMOUNT | 400 | false | Amount field is malformed (not a decimal string, or negative). |
VALIDATION_ERROR | 400 | false | Invalid input. details.fields lists the offending fields. |
UNAUTHORIZED | 401 | false | Missing or invalid API key. |
API_KEY_EXPIRED | 401 | false | Your key has expired — rotate it in the app. |
NOT_GROUP_MEMBER | 403 | false | You are not a member of this group. |
NOT_GROUP_LEADER | 403 | false | Action requires leader status in the group. |
NOT_PROPOSAL_CREATOR | 403 | false | Only the proposal creator can perform this action. |
NOT_POSITION_OWNER | 403 | false | This position does not belong to your account. |
GEO_RESTRICTED | 403 | false | Product unavailable in your region. See Geo Restrictions. |
ACTION_IN_APP_ONLY | 403 | false | This action (e.g. withdrawal) can only be performed in the Murmo app. |
PROOF_REQUIRED | 403 | false | KYC or identity verification is required — complete it in the app. |
GROUP_NOT_FOUND | 404 | false | Unknown group ID. |
PROPOSAL_NOT_FOUND | 404 | false | Unknown proposal ID. |
POSITION_NOT_FOUND | 404 | false | Unknown position ID. |
MARKET_NOT_FOUND | 404 | false | Unknown market or event ID. |
MARKET_CLOSED | 409 | false | Market is not open for this action. details.nextOpenAt when known. |
MARKET_MAINTENANCE | 409 | true | Prediction market in a maintenance window. Retry later. |
PROPOSAL_CLOSED | 409 | false | Proposal is closed; no new positions accepted. |
POSITION_CLOSED | 409 | false | Position is already closed. |
POSITION_ALREADY_CLAIMED | 409 | false | Winnings have already been claimed for this position. |
REQUEST_IN_PROGRESS | 409 | true | A concurrent request is in flight. Retry after a brief pause. |
TRANSACTION_EXPIRED | 409 | true | Blockchain transaction expired before submission. Retry. |
ORDER_BELOW_MIN_SIZE | 422 | false | Order rounds below the market minimum. Increase the size. |
COLLATERAL_BELOW_MIN | 422 | false | Collateral is below the minimum required for this perp. |
LEVERAGE_EXCEEDS_MAX | 422 | false | Requested leverage is above the market’s maximum. |
LEVERAGE_BELOW_MIN | 422 | false | Requested leverage is below the market’s minimum. |
SLIPPAGE_EXCEEDED | 422 | true | Fill would exceed slippage tolerance. Retry or widen tolerance. |
ROUTE_NOT_FOUND | 422 | true | No swap route or quote found. Often transient — retry or adjust size. |
NO_MARKET_PRICE | 422 | true | Market price is temporarily unavailable. Retry. |
FAIR_PRICE_REJECTED | 422 | true | Fair-price check failed. Retry. |
NOTHING_TO_SELL | 422 | false | No position to sell or close for this proposal. |
NOT_CLAIMABLE | 422 | false | Position is not claimable — it did not win or there is nothing to claim. |
REDUCE_ROUNDS_TO_ZERO | 422 | false | Reduce amount rounds to zero at this precision. Increase size. |
AMOUNT_TOO_SMALL_AFTER_FEE | 422 | false | Net amount after fee is zero or negative. Increase size. |
RATE_LIMITED | 429 | true | Rate limit exceeded. Honor Retry-After. |
UPSTREAM_UNAVAILABLE | 503 | true | A dependency is down. Retry with backoff. |
UPSTREAM_TIMEOUT | 504 | true | A dependency timed out. Retry with backoff. |
UPSTREAM_REJECTED | 502 | false | Upstream rejected the request. Do not retry as-is. |
INTERNAL_ERROR | 500 | false | Unexpected server error. Report with errorId. |
Handling pattern
Build your API wrapper to extract code, retryable, and details from every non-2xx response. Branch on code — never on message.
async function murmoFetch(path, init = {}) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
...init.headers,
},
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(`${body.code ?? res.status}: ${body.message}`);
err.code = body.code;
err.retryable = body.retryable;
err.details = body.details;
err.errorId = body.errorId;
throw err;
}
return body.data;
}
// Usage
try {
const data = await murmoFetch("/api/v1/spot/proposals", { method: "POST", body: JSON.stringify(payload) });
} catch (err) {
if (err.code === "INSUFFICIENT_FUNDS") {
console.error("Top up your wallet. Required:", err.details?.required);
} else if (err.retryable) {
// schedule retry with backoff
} else {
throw err; // terminal — fix the request
}
}
Always branch on code — it is stable across releases. Back off with jitter when retryable is
true and honor the Retry-After header. For writes, reconcile via /positions or /trades
instead of retrying blindly.
Swaps use the standard error path. POST /api/v1/spot/swap returns a 4xx or 5xx error
with a code on failure — it does not return 200 { success: false }. Handle it the same way as
any other endpoint.