Skip to main content
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"
}
FieldAlways presentMeaning
statusCodeYesThe HTTP status code — mirrors the response status line.
codeYesA stable machine-readable code. Branch on this, never on message.
messageYesHuman-readable explanation. Wording may change between releases — do not parse it.
retryableYestrue only when it is safe to resend the identical request (see Retry Semantics).
detailsWhen applicableMachine-readable context for the error: amounts, symbol, region, and so on. Money values are decimal strings.
errorIdYesUnique 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[].

Response headers

HeaderWhen presentHow to use it
X-Request-IdEvery responseCorrelation ID — log it. If you send the header yourself it is echoed back; otherwise it is generated.
Retry-After429 and retryable 5xx responsesSeconds to wait before your next attempt. Honor it before applying your own backoff.

HTTP status codes

StatusWhen it appearsWhat to do
200 OKSuccessful responseRead data.
400 Bad RequestMalformed, missing, or invalid inputFix the request. Read code and details.
401 UnauthorizedMissing or invalid API keyCheck the Authorization: Bearer murmo_... header.
403 ForbiddenNot a member, not the creator, geo-restricted, or an in-app-only actionDo not retry as-is.
404 Not FoundUnknown resource ID (proposal, position, market, group)Verify the ID.
409 ConflictResource state forbids the action (market closed, proposal closed, already claimed)Re-read state; check retryable.
413 Payload Too LargeRequest body exceeds size limitReduce payload size.
422 UnprocessableInput is valid but breaks a trading rule (insufficient funds, min size, slippage)Adjust the order parameters.
429 Too Many RequestsRate limit exceeded (1,200 / 60s)Back off using Retry-After.
5xxUpstream dependency rejected, unavailable, or unexpected server errorRetry 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:
codeStatusretryableMeaning
INSUFFICIENT_FUNDS422falseNot enough balance. details includes required and available.
INVALID_AMOUNT400falseAmount field is malformed (not a decimal string, or negative).
VALIDATION_ERROR400falseInvalid input. details.fields lists the offending fields.
UNAUTHORIZED401falseMissing or invalid API key.
API_KEY_EXPIRED401falseYour key has expired — rotate it in the app.
NOT_GROUP_MEMBER403falseYou are not a member of this group.
NOT_GROUP_LEADER403falseAction requires leader status in the group.
NOT_PROPOSAL_CREATOR403falseOnly the proposal creator can perform this action.
NOT_POSITION_OWNER403falseThis position does not belong to your account.
GEO_RESTRICTED403falseProduct unavailable in your region. See Geo Restrictions.
ACTION_IN_APP_ONLY403falseThis action (e.g. withdrawal) can only be performed in the Murmo app.
PROOF_REQUIRED403falseKYC or identity verification is required — complete it in the app.
GROUP_NOT_FOUND404falseUnknown group ID.
PROPOSAL_NOT_FOUND404falseUnknown proposal ID.
POSITION_NOT_FOUND404falseUnknown position ID.
MARKET_NOT_FOUND404falseUnknown market or event ID.
MARKET_CLOSED409falseMarket is not open for this action. details.nextOpenAt when known.
MARKET_MAINTENANCE409truePrediction market in a maintenance window. Retry later.
PROPOSAL_CLOSED409falseProposal is closed; no new positions accepted.
POSITION_CLOSED409falsePosition is already closed.
POSITION_ALREADY_CLAIMED409falseWinnings have already been claimed for this position.
REQUEST_IN_PROGRESS409trueA concurrent request is in flight. Retry after a brief pause.
TRANSACTION_EXPIRED409trueBlockchain transaction expired before submission. Retry.
ORDER_BELOW_MIN_SIZE422falseOrder rounds below the market minimum. Increase the size.
COLLATERAL_BELOW_MIN422falseCollateral is below the minimum required for this perp.
LEVERAGE_EXCEEDS_MAX422falseRequested leverage is above the market’s maximum.
LEVERAGE_BELOW_MIN422falseRequested leverage is below the market’s minimum.
SLIPPAGE_EXCEEDED422trueFill would exceed slippage tolerance. Retry or widen tolerance.
ROUTE_NOT_FOUND422trueNo swap route or quote found. Often transient — retry or adjust size.
NO_MARKET_PRICE422trueMarket price is temporarily unavailable. Retry.
FAIR_PRICE_REJECTED422trueFair-price check failed. Retry.
NOTHING_TO_SELL422falseNo position to sell or close for this proposal.
NOT_CLAIMABLE422falsePosition is not claimable — it did not win or there is nothing to claim.
REDUCE_ROUNDS_TO_ZERO422falseReduce amount rounds to zero at this precision. Increase size.
AMOUNT_TOO_SMALL_AFTER_FEE422falseNet amount after fee is zero or negative. Increase size.
RATE_LIMITED429trueRate limit exceeded. Honor Retry-After.
UPSTREAM_UNAVAILABLE503trueA dependency is down. Retry with backoff.
UPSTREAM_TIMEOUT504trueA dependency timed out. Retry with backoff.
UPSTREAM_REJECTED502falseUpstream rejected the request. Do not retry as-is.
INTERNAL_ERROR500falseUnexpected 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.