Errors, limits, and configuration reference

This is the reference page you'll keep open in the other tab — error shapes, status codes, validation rules, and the deployment-level limits that shape what your integration can do.

For per-endpoint failure modes, see the topic docs (User Onboarding (users, KYC, terms), Market Orders (single-contract trades), Parlays (2–12 leg trades), Wallets, Webhooks (push events)).


1. Error response shapes

ErrorResponse — single business error

Returned for most 4xx and 5xx errors.

{ "error": "user already exists", "code": "user_already_exists" }

ValidationErrorResponse — per-field validation

Returned for 422 on body validation.

{
  "errors": [
    { "field": "firstName",   "message": "required" },
    { "field": "dateOfBirth", "message": "must be age 19-125" }
  ]
}

StaleTermsResponse — only on POST /users/USERID/terms 409

{ "error": "terms version stale", "code": "stale_terms", "currentTotalVersion": 8 }

Use currentTotalVersion to re-prompt the user with the right version. See User Onboarding (users, KYC, terms).


2. HTTP status codes

HTTPMeaningCommon triggers
200OKRead, or idempotent write that already happened
201CreatedPOST /users, POST /parlays, POST /push/register
202Accepted (async)POST /market-orders — poll refId
204No ContentDELETE, POST /users/USERID/terms first-time acceptance
400Bad RequestMalformed JSON, bad query params
401UnauthorizedJWT missing/invalid, subUSERID
402Payment RequiredInsufficient balance on POST /parlays/{id}/confirm. Not used by POST /market-orders — that endpoint accepts the order with 200 status=pending and lets the matching engine cancel it. See Market Orders.
403ForbiddenIP geo-gate denial (GEO_BLOCKED) or a required permission not granted for the user (x-required-permission on the endpoint). Surface as ForbiddenError.
404Not FoundBad UUID, INDIVIDUAL ISV reading /wallets without sub
409ConflictDuplicate user, stale terms version, push already registered, non-zero balance on delete
422UnprocessableField validation, KYC pending, insufficient liquidity, immutable field touched
500Internal Server ErrorUnhandled backend failure

3. Common error codes

codeHTTPTriggered by
user_already_exists409POST /users — identity hash (name + DOB + SSN) collides
user_not_found404Any user-scoped path with an unknown UUID
user_pending_kyc409/422GET /users/USERID before the user is verified (409), DELETE /users/USERID on any user who hasn't reached SUCCESS (409 — the code says "pending" but the check is "not verified", so FAILURE / MORTALITY / PEP / OFAC users hit this too), or trying to trade before KYC has resolved (422)
non_zero_balance409DELETE /users/USERID while balance > 0 (INDIVIDUAL ISVs)
stale_terms409POST /users/USERID/terms with an out-of-date totalVersion
kyc_not_retryable409POST /users/USERID/kyc-retry when the user isn't in FAILURE status or when the per-ISV retry limit has been exhausted. Check GET /kyc-status to tell them apart — a FAILURE user that still returns this code means retries are exhausted. See User Onboarding.
address_not_found422POST /validate-address — Smarty could not find a match for the submitted US address. Ask the user to correct and re-validate before you try to create the user.
limit_exceeded4xxSurfaces on the embedded UI's AeroPay or PayNearMe deposit/withdraw calls when the requested amount would exceed a configured per-user cap (daily, pending, or per_tx). Body includes kind (which cap), limit, and remaining. Not called from the ISV backend — the embedded UI reads limits and enforces this itself. See Embedded UI.
INVALID_CONTRACT400/422Unknown or stale contractId on order endpoints
GEO_BLOCKED403The client_ip claim resolves to a non-licensed jurisdiction. Surface to the user as a region notice, not an auth error. See Authentication.
permission_denied403The user lacks a permission required by the endpoint (x-required-permission on the spec). Inspect GET /users/USERID/permissions to see which permissions are granted and any denyReason. The response body may include pending_gates: ["<gate>", …] — the specific gates blocking the permission. A pending_gates: ["not-suspended"] value specifically means the user is currently suspended (see User Onboarding).
internal_server_error500Unhandled backend failure

4. Request validation rules

CreateUserRequest

FieldRule
firstName, lastNameRequired, non-empty. Immutable.
dateOfBirthRequired. YYYY-MM-DD. Age 19–125. Immutable.
ssnLastDigitsRequired. Exactly 4 numeric digits. Immutable.
addressLine1, cityRequired.
stateRequired. 2-letter USPS code.
zipRequired. 5–10 chars.
countryCodeRequired. ISO 3166 alpha-2.
phoneNumberOptional. If given, 10 numeric digits, unformatted.
emailOptional. If given, valid email format.
emailVerifiedAtRequired. UTC timestamp.
phoneVerifiedAtOptional. UTC timestamp.

UpdateUserRequest

Same field-level rules as above, but all fields are optional, and firstName / lastName / dateOfBirth / ssnLastDigits must not appear. Including any of them returns 422.

EstimateMarketOrderRequest

FieldRule
contractIdRequired.
quantityRequired. >= 0 (use 0 to peek the best price).

SubmitMarketOrderRequest

FieldRule
contractIdRequired.
quantityRequired. > 0 (strict).
expectedAveragePriceRequired. Integer (American price). Echo back from the /estimate response.
priceListRequired. Non-empty list[int]. Echo back from the /estimate response — at least one level required. The backend rejects the submit if the live price has moved off these levels.

CreateParlayRequest

FieldRule
legsRequired. 2–12 items.
legs[].eventId, marketId, outcomeIdRequired. > 0.
legs[].contractIdRequired.
legs[].strikeOptional (spreads/totals).
quantityRequired. > 0.

ConfirmParlayRequest

FieldRule
quantityRequired. > 0. Must not exceed offer.maxQuantity from the quote.

AcceptTermsRequest

FieldRule
totalVersionRequired. > 0.

GET /transactions query

FieldRule
limitOptional. Integer in 1..200. Defaults to 50.
beforeOptional. Integer. Pass nextCursor from a previous response.

5. Auth-level constraints

ConstraintValue
JWT max exp - iat< 300 seconds
JWT nbf/iat clock skew±30 seconds
subUSERID match on user routesRequired
digest claimBase64URL(SHA256(body)), no padding
subsig claimBase64URL(HMAC-SHA256(secret, "::")), no padding
client_ip claimRequired on user-scoped routes when geofencing is enabled. Plain IPv4/IPv6 string of the end user's IP, not your server's. Missing / private / non-licensed → 403 GEO_BLOCKED.
Inbound webhook signingEdDSA (Ed25519). Verify with the ProphetX public key.

See Authentication.


6. Webhook delivery limits

SettingDefaultEffect
Delivery timeout5 sYour receiver must respond 2xx within this window.
Error threshold3 consecutive failuresReceiver gets paused; resumes when GET /health returns 200.
Retry delay10 sTime between retries.
Message max age24 hOlder messages are dropped, not delivered.

See Webhooks (push events).


7. Quick lookup: "what does this status code usually mean?"

A field guide for log triage.

You see...First look at...
401 on every callJWT generation — check alg=EdDSA, kid==iss, aud="prophetx", clock skew, base64 padding
401 on USERID routes onlysub claim ≠ USERID; or missing subsig
403 GEO_BLOCKEDThe client_ip claim isn't licensed for this jurisdiction. Confirm you're forwarding the trader's IP, not your server's.
403 permission_deniedUser lacks an x-required-permission for the endpoint. Call GET /users/USERID/permissions to see which permissions/gates are granted.
402 on parlay confirmUser balance vs. fee + quantity
Market order came back status: "canceled" with filledQuantity: 0 seconds after submitInsufficient balance. Market-orders don't 402 on submit — poll get() and check status.
404 on /wallets with no subYou're INDIVIDUAL, there's no standing fund — call with sub for the user wallet
409 user_already_existsSame person, different account — identity hash collision
409 stale_termsRe-fetch /terms, show the new version, re-prompt
409 non_zero_balanceSettle, refund, or withdraw before deleting (INDIVIDUAL)
422 INVALID_CONTRACTStale contractId — re-fetch markets
422 user_pending_kycWait for GET /kyc-status to read SUCCESS
422 on POST /market-orders after a clean estimateLive price moved off priceList — re-call /estimate and resubmit with the new expectedAveragePrice + priceList
Parlay failReason: "price moved"Re-quote with the same legs

Did this page help you?