GunSpec

Error Handling

Every failure is a JSON envelope with a stable code, a more specific reason, and a request_id. Branch on code and reason; never on message, whose wording may change. This page is generated from the API's OpenAPI specification and its reason registry.

Every non-2xx response has this shape, and success is always false.

Error response
json
{  "success": false,  "error": {    "code": "FORBIDDEN",    "reason": "PLAN_REQUIRED",    "message": "This endpoint requires 'studio' tier or above. Your tier: 'builder'",    "details": {      "requiredTier": "studio",      "currentTier": "builder"    }  }}
  • codestringrequired

    Machine-readable error code, e.g. NOT_FOUND, RATE_LIMITED, SUBSCRIPTION_REQUIRED. Branch on this, never on message.

  • reasonenumrequired

    Which situation, within code. Stable and machine-readable: branch on this, never on message. Each error response lists the reasons it can carry, and the error reference says what to do about each.

  • messagestringrequired

    Human-readable account of what went wrong. The wording may change; treat it as text for a log or a person, not as a value to match on.

  • detailsobjectoptional

    Structured context for the reason: the invalid fields for INVALID_PARAMETER, requiredTier for PLAN_REQUIRED.

  • limitintegeroptional

    On DAILY_CAP_EXCEEDED, MCP_DAILY_CAP_EXCEEDED and MONTHLY_CAP_EXCEEDED only: the limit that was reached. The daily limits are per key; the monthly one is the plan's allowance for the whole account.

  • resetsAtstringoptional

    On DAILY_CAP_EXCEEDED, MCP_DAILY_CAP_EXCEEDED and MONTHLY_CAP_EXCEEDED only: when the limit returns to zero, ISO 8601. The next midnight UTC for the daily limits (the same instant as X-Daily-Reset), midnight UTC on the first of next month for the monthly one (X-Monthly-Reset). Nothing sent before then will be served.

  • retryAfterintegeroptional

    On DAILY_CAP_EXCEEDED, MCP_DAILY_CAP_EXCEEDED and MONTHLY_CAP_EXCEEDED only: whole seconds until resetsAt, the same figure as the Retry-After header.

  • request_idstringoptional

    Quote this to support: it finds the request in our logs.

The HTTP statuses an error can carry, the situations each one covers, and how many endpoints declare it.

DiagramWhat to do with each status
100%
What to do with each statusA 2xx is your answer and a 304 means the copy you already hold is current. A 400 is a request to fix, and error.reason names the parameter. A 401 means present a valid key. A 403 means the key is fine and something else is not, and the reason says whether it is your plan, a permission or the page depth. A 404 after a resolve means resolve again rather than retry. Only two statuses are worth retrying: a 429 after waiting the seconds in Retry-After, and a 5xx with backoff.
StatusDescriptionReasonsEndpoints
400Validation error159
401Missing or invalid API key134
403The credential is valid and the caller is not permitted. Three causes, none of which reissuing a key changes: the plan is below the endpoint's tier, the account is suspended, or the key is not attached to the thing being acted on, meaning a user account for /v1/me or a shop for /v1/vendor. Distinct from 401, which means the credential is missing, malformed or expired and a new one can work.134
404Resource not found71
413The request body is larger than this endpoint accepts, which is 1 MB for a JSON body. Refused from Content-Length before the body is read, so nothing was stored and the request can be split and resent. error.maxBytes carries the limit.8
429A limit was reached. RATE_LIMITED is the per-minute limit: wait Retry-After seconds (a minute) and retry. DAILY_CAP_EXCEEDED is the plan's daily limit, per key: nothing will be served until the counters return to zero at midnight UTC. MONTHLY_CAP_EXCEEDED is the plan's monthly allowance, for the whole account: nothing will be served until midnight UTC on the first of next month, or until the plan changes. For both, Retry-After is the seconds until then and error.resetsAt says when as an instant. Calls made after a refusal are refused too and do not spend the month, and a free key that keeps calling is paused, so stop at the first one and resume at the reset.160
500Internal server error160

code is the family and never changes. When a request fails without a more specific situation, the reason is the family's default, listed here.

CodeStatusDefault reason
400INVALID_REQUEST
401AUTH_REQUIRED
403ACTION_NOT_ALLOWED
403PAGINATION_DEPTH_EXCEEDED
403PLAN_REQUIRED
404RESOURCE_NOT_FOUND
409CONFLICT
413PAYLOAD_TOO_LARGE
429DAILY_CAP_EXCEEDED
429MCP_DAILY_CAP_EXCEEDED
429MONTHLY_CAP_EXCEEDED
429RATE_LIMITED
429REPORT_RATE_LIMITED
429RATE_LIMITED
500INTERNAL_ERROR
503DEPENDENCY_UNAVAILABLE

Every reason the API can send, by status: what happened, and what to do next. A reason is part of the published contract. New ones are added; none are renamed.

400Bad request3
  • A path, query or body field failed validation. details names each field and what was wrong with it.

    What to doCorrect the named fields and send the request again.

  • The request body is not valid JSON.

    What to doSend a JSON body with Content-Type: application/json.

  • The request is well-formed but asks for something the endpoint refuses; the message says what.

    What to doChange the request as the message describes.

401Unauthorized: the credential11Read more
  • This endpoint needs a credential and none was sent.

    What to doSend an API key in X-API-Key, or sign in.

  • No API key was sent.

    What to doSend your key in the X-API-Key header.

  • The API key is not one we issued, or it has been deleted.

    What to doCheck the key for a typo, or create a new one in your account.

  • The key was switched off by the account that owns it.

    What to doRe-enable it in your account, or use another key.

  • The key has passed its expiry date.

    What to doCreate a new key in your account.

  • This endpoint needs a signed-in session.

    What to doSign in, then retry.

  • The session is no longer valid.

    What to doSign in again.

  • The credential verified, but the account behind it no longer exists.

    What to doSign in again, or contact support.

  • A proxy or marketplace credential was presented and does not verify.

    What to doThrough a marketplace such as RapidAPI, contact the API provider: the proxy is misconfigured.

  • An inbound webhook delivery is unsigned, or its signature does not match the secret it was sent under.

    What to doSet the same webhook secret on the sender and in the console; a sender's test delivery signed with another secret fails here.

403Forbidden: the permission16Read more
  • The credential is valid and its plan is below what this endpoint needs. details.requiredTier names the plan.

    What to doUpgrade the plan. A new key on the same plan will get the same answer.

  • The account behind this credential has been suspended.

    What to doContact support. Rotating the key will not help.

  • This key is paused: it kept making requests long after its daily or monthly limit refused them. Only this key is paused, never the account.

    What to doStop sending requests and wait for the time in Retry-After, or upgrade your plan, which lifts the pause at once. Signing in and changing plan are unaffected. A new key does not help: the pause follows the key you were given, and creating more is limited by your plan.

  • The key is valid but no user account owns it, so account endpoints (/v1/me) have nothing to act on.

    What to doUse a key created from your account.

  • The key is a platform token issued for the hosted MCP server alone, and this request did not come through it.

    What to doCall the MCP server at the documented URL with this key, or use a key of your own for direct API requests.

  • The key is valid but no shop names it, so seller endpoints (/v1/vendor) have no shop to act for.

    What to doName the key in Profile > Seller on the shop it should act for, then check GET /v1/vendor/shops.

  • The shop exists and has not been approved yet.

    What to doWait for approval. Nothing needs to be sent again.

  • The resource exists and belongs to another account or shop.

    What to doUse an id your account owns.

  • The download belongs to a product this account has not bought.

    What to doBuy the product, then retry.

  • The plan's allowance for this kind of item is used up.

    What to doRemove one, or upgrade for a larger allowance.

  • The plan does not page this deep into a list.

    What to doNarrow the list with filters, or upgrade for deeper paging.

  • Staff-only endpoint.

    What to doSign in with a staff account.

  • This is an impersonation session, which may look but never change anything.

    What to doMake the change from your own account.

  • A browser request carried a GunSpec origin without the signature our own apps send.

    What to doCall the API with an API key.

  • This client, user agent or address is blocked.

    What to doContact support if you think this is a mistake.

  • The action is not permitted on this resource; the message says why.

    What to doDo not retry unchanged.

404Not found2
  • The path is right and no record has that id.

    What to doCheck the id. The list and search endpoints return valid ones.

  • No endpoint answers this method and path.

    What to doCheck the path and method against the API reference.

409Conflict1
  • The request conflicts with existing data, such as a slug already in use.

    What to doChange the conflicting value and retry.

413Payload too large1
  • The request body is larger than this endpoint accepts. maxBytes carries the limit.

    What to doSplit the request into smaller ones.

429Slow down6Read more
  • Too many requests this minute for the plan.

    What to doWait for Retry-After seconds, then retry.

  • The plan's daily request allowance is used up. It resets at midnight UTC.

    What to doWait for the reset, or upgrade for a larger allowance.

  • The plan's monthly request allowance is used up. It resets at midnight UTC on the first of the month. Calls a limit refused do not count towards it.

    What to doWait for the reset in Retry-After, error.resetsAt or X-Monthly-Reset, or choose a larger plan, which raises the allowance at once.

  • The plan's daily allowance for calls through the MCP server is used up. It resets at midnight UTC; direct API requests still count against the plan's own allowance.

    What to doWait for the reset, call the API directly, or upgrade for a larger MCP allowance.

  • Too many sequential pages in a short time.

    What to doSlow the walk down, or use a larger per_page.

  • A data report for this section was already filed in the last 24 hours.

    What to doWait a day, or add to the report already filed.

500Our side1
  • Something failed on our side. It has been logged against request_id.

    What to doRetry with backoff. Quote request_id if it persists.

503Temporarily unavailable2
  • A service this endpoint depends on is temporarily unavailable.

    What to doRetry after a short wait.

  • The API is paused for maintenance.

    What to doWait for Retry-After seconds, then retry.

Every response carries an X-Request-Id header and every error body echoes it as request_id. Quote it to support: it finds the request in our logs.

Retry-After is the seconds to wait before the next allowed request. On the per-minute limit it is 60. On the daily limit it is the seconds left until the counters return to zero at midnight UTC, never more than a day. On the monthly allowance it is the seconds left until midnight UTC on the 1st, never more than a month. Nothing is served before then. A 503 during maintenance carries it too. A daily or monthly refusal also says so in the body: error.limit, error.resetsAt (an ISO 8601 instant, the same as X-Daily-Reset or X-Monthly-Reset) and error.retryAfter. What is left is sent on every response: the day, per key, as X-Daily-Limit, X-Daily-Remaining and X-Daily-Reset (plus X-Daily-MCP-* on MCP traffic), and the month, for the whole account, as X-Monthly-Limit, X-Monthly-Remaining and X-Monthly-Reset on a call made with a key. Each remaining figure is what is left after the call that returned it, so a response that says 0 was the last one served. Only calls that were served spend the month: a refused call and a 304 do not. Pace against the headers and reconcile against GET /v1/me/usage. There is no X-RateLimit-Remaining; the edge limiter reports whether a request was allowed, not how much of the minute is left. Stop at the first refusal: the SDKs wait out a reset only when it is a few seconds away, and a free key that keeps calling is paused with KEY_ON_HOLD. Stay inside the plan shows how a script stops in time.

What the API does with a call that is not in the reference. These are promises, checked against production on every run, not behaviour that happens to hold today.

  • A method a path does not answer returns 404 with error.reason of ROUTE_NOT_FOUND, naming the method and the path. Not 405: a path is defined per method here, so POST /v1/firearms is not a firearms endpoint refusing a verb, it is not an endpoint.
  • HEAD is answered wherever GET is, with the same status, the same headers and no body. That includes the ETag, so a HEAD carrying If-None-Match answers 304 and is the cheapest way to ask whether a record has changed. It is not served from the shared cache, so it costs a request against your allowance. See caching.
  • A query parameter the operation does not document is ignored rather than refused. A misspelled filter therefore returns unfiltered data rather than an error, so check the name against the operation if a result looks too broad.