Skip to content
JuicePay

JuicePay documentation

Create account

Documentation

Errors

Error shapes, retry guidance, and the full code table.

JuicePay returns conventional HTTP status codes and a consistent JSON error body. Error bodies are safe to log and safe to surface to your own support tooling.

Shape#

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "recipient_invalid_address",
    "message": "The recipient address is not valid on chain 'solana'.",
    "param": "legs[3].recipient",
    "doc_url": "https://juicepay.test/docs/api/errors#recipient_invalid_address",
    "request_id": "req_8kL3mQ2xNp"
  }
}

Always log request_id. It is the only identifier that lets support correlate your failure with our traces.

Types#

TypeStatusMeaning
invalid_request_error400The request is malformed or fails validation.
authentication_error401The key is missing, malformed, or revoked.
permission_denied403The key is valid but lacks a required scope.
not_found404The resource does not exist, or belongs to another account.
idempotency_key_reuse409Same key, different body.
precondition_failed412The resource state does not permit this operation.
rate_limit_error429Too many requests.
provider_error502An upstream venue or chain node failed.
api_error500Something on our side broke.

Retry guidance#

The distinction that matters is whether retrying can ever help.

  • Retry with backoff — 429, 500, 502, and any connection error. 429 respects Retry-After.
  • Retry after fixing the request — 400, 412. The body or the resource state must change first.
  • Never retry unchanged — 401, 403, 404. These indicate a configuration problem that will reproduce forever.
ts
async function withRetry<T>(operation: () => Promise<T>, attempts = 5): Promise<T> {
  for (let attempt = 0; attempt < attempts; attempt += 1) {
    try {
      return await operation();
    } catch (error) {
      if (!isJuicePayError(error)) throw error;

      const retryable = ["rate_limit_error", "api_error", "provider_error"];
      if (!retryable.includes(error.type)) throw error;
      if (attempt === attempts - 1) throw error;

      const ceiling = Math.min(2 ** attempt * 250, 8000);
      const jitter = Math.random() * ceiling * 0.25;
      await sleep(ceiling + jitter);
    }
  }
  throw new Error("unreachable");
}

Rate limits#

Limits are per key and per endpoint class:

ClassLimitBurst
Reads1,000/min2,000
Writes300/min600
Batch submissions20/min40
text
HTTP/1.1 200 OK
JuicePay-RateLimit-Limit: 300
JuicePay-RateLimit-Remaining: 271
JuicePay-RateLimit-Reset: 1755594900

Common codes#

CodeFix
recipient_invalid_addressValidate the destination against the target chain.
recipient_screening_blockedStop. Escalate to compliance; do not retry.
insufficient_balanceFund the account, then replay the leg.
amount_below_minimumCheck per-asset and per-chain minimums.
asset_precision_exceededSend whole units for low-decimal tokens.
quote_expiredRequest a fresh quote before executing.
chain_haltedTransient. Retries resume with the network.
routing_unavailableRelax prefer_chains or enable bridging.
idempotency_key_reuseA genuine bug — the same key was sent with a different body.
settlement_confidence_invalidincluded is not available for this rail.

Validation errors#

Batch endpoints report every failing leg in one response rather than stopping at the first, so a dry run tells you about all of your problems at once.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "batch_validation_failed",
    "message": "3 of 10000 legs failed validation.",
    "legs": [
      { "row": 3, "code": "recipient_invalid_address", "param": "legs[3].recipient" },
      { "row": 881, "code": "amount_below_minimum", "param": "legs[881].amount" },
      { "row": 4417, "code": "asset_precision_exceeded", "param": "legs[4417].amount" }
    ]
  }
}