Errors
Response envelope, error codes, and consumer guidance for every 4xx and 5xx response.
Every error response uses one envelope. Switch on error.code for routing logic, surface error.message to humans, click error.doc_url for the spec of any code you receive, and quote error.request_id in support tickets.
{
"error": {
"code": "validation.out_of_range",
"message": "lat: The field lat must be between -90 and 90.",
"doc_url": "https://pullfirst.com/docs/errors#validation.out_of_range",
"request_id": "00-3f9a8c1d4b2e7f6a-9c8e7d6f5a4b3c2d-00",
"details": [
{
"field": "lat",
"issue": "out_of_range",
"received": "200",
"expected": { "min": -90, "max": 90 }
}
]
}
}Envelope fields
code is the stable machine-readable identifier. Switch on this. Codes use dotted namespaces (validation.out_of_range, query.unknown_param) so prefix matching works (error.code.startsWith("validation.")).
message is human-readable. Safe to render to operators and end users; never includes stack traces, internal field names, or secrets.
doc_url links to the section below for the specific code. Click-through is the fastest way to recover from an unfamiliar error.
request_id matches the X-Request-Id response header. Quote this in support requests; it correlates to logs.
details[] is per-field breakdown for validation errors. Each entry has field (parameter name), issue (short kind), and may include received (the value you sent), expected (the constraint), and suggestion (a hint when the field name is recognizable).
Headers
Every error response also carries:
X-Request-Id— same value aserror.request_id. Set on every response, not just errors.X-Error-Code— same value aserror.code. Useful for log scrapers, CDN routing, and dashboard aggregation without parsing the body.Retry-After— seconds until the window opens, on429and some503responses.
Multiple violations in one response
When more than one parameter is invalid, the top-level code is validation.multiple and details[] carries every violation in one response. Fix them all at once instead of round-tripping for each.
{
"error": {
"code": "validation.multiple",
"message": "3 parameters were invalid.",
"doc_url": "https://pullfirst.com/docs/errors#validation.multiple",
"request_id": "00-...",
"details": [
{ "field": "lat", "issue": "required" },
{ "field": "lng", "issue": "out_of_range", "received": 0, "expected": { "min": -180, "max": 180 } },
{ "field": "pageSize", "issue": "out_of_range", "received": 500, "expected": { "min": 1, "max": 100 } }
]
}
}Error codes
Codes are stable across minor releases. New codes may be added; existing ones will not be renamed or removed without a major version bump.
Validation
validation
Fallback code when a more specific validation code does not apply. Status: 400. Action: read details[].issue for the specific failure type.
validation.required
A required parameter was missing or null. Status: 400. Action: include the named field on the next request. Applies to parameters marked [BindRequired], [Required], or non-nullable primitives without a default.
curl -i "https://api.pullfirst.com/v1/parcels/containing?lat=44.948" \
-H "Authorization: Bearer sk_live_..."
# HTTP/1.1 400 Bad Request
# X-Error-Code: validation.required
# {
# "error": {
# "code": "validation.required",
# "message": "lng: A value for the 'lng' parameter or property was not provided.",
# "details": [{ "field": "lng", "issue": "required" }]
# }
# }validation.out_of_range
A numeric parameter was outside its declared [Range]. Status: 400. The expected object carries min and max; received echoes the value you sent. Action: clamp client-side or surface the bounds to the user.
validation.length
A string parameter violated its [StringLength] minimum or maximum. Status: 400. The expected object carries min and/or max character counts. Action: trim or expand to the declared bounds.
validation.format
A parameter could not be parsed into its declared type. Most commonly: a string that is not a valid date, number, or enum value. Status: 400. Action: send the value in the format the docs specify (ISO-8601 for dates, plain decimal for numbers).
validation.multiple
More than one parameter failed validation. Top-level code is validation.multiple; per-field codes appear in details[].issue. Status: 400. Action: walk details[] and fix every entry.
validation.semantic
The request was syntactically valid but semantically incoherent. Cross-field constraints land here: a bounding box where maxLat < minLat, a date range where endDate < startDate, mutually-exclusive fields both supplied. Status: 422 Unprocessable Entity. Action: re-derive the relationship the message describes; details[0].expected summarizes the rule.
Query
query.unknown_param
A query parameter was sent that the endpoint does not declare. Status: 400. The error often includes a suggestion field for typos and well-known aliases (lon → lng, latitude → lat, q → query). Action: rename or remove the offending key. The first unknown key is named in error.message; all unknown keys appear in details[].
curl -i "https://api.pullfirst.com/v1/parcels/near?lat=44.948&lon=-93.090&radiusMeters=300" \
-H "Authorization: Bearer sk_live_..."
# HTTP/1.1 400 Bad Request
# X-Error-Code: query.unknown_param
# {
# "error": {
# "code": "query.unknown_param",
# "message": "Unknown query parameter 'lon'. Did you mean 'lng'?",
# "details": [{ "field": "lon", "issue": "not_recognized", "received": "-93.090", "suggestion": "lng" }]
# }
# }The whitelist of always-allowed parameters is api_key and apikey; everything else must be declared on the action.
query.duplicate_param
Reserved for future use — the same query key appearing more than once with conflicting semantics. Currently the API accepts duplicate keys per ASP.NET binding rules.
Body
body.malformed_json
The request body is not valid JSON, or its top-level shape does not match the documented contract. Status: 400. Action: validate the payload against the OpenAPI schema before retrying.
Filter
filter.parse_error
The Gridify filter expression in ?filter= could not be parsed. Status: 400. The message carries the parser's diagnostic. Action: see Filtering for grammar.
filter.unknown_field
The filter expression references a field that is not exposed for filtering on this endpoint. Status: 400. Action: see the per-endpoint OpenAPI page for the list of filterable fields.
Authentication
auth.missing
The Authorization: Bearer ... header is absent. Status: 401. Action: include a valid API key. See Authentication.
auth.invalid
The supplied key is malformed, expired, deactivated, or does not have access to the requested resource. Status: 403. Action: rotate or upgrade the key on your dashboard.
Resource
not_found
The resource at the requested path does not exist or is not visible to your key. Status: 404. Action: confirm the identifier and your key's plan.
method_not_allowed
The path is valid but does not accept the HTTP method you used. Status: 405. Action: see the OpenAPI page for the supported methods.
conflict
The request would violate a constraint or precondition (duplicate identifier, race against concurrent state, payment not yet completed). Status: 409. Action: refresh state and retry; the message describes the specific conflict.
unsupported_media_type
The request's Content-Type is not one this endpoint accepts. Status: 415. Action: send application/json.
Throttling
rate_limit.exceeded
The per-minute or monthly quota was exceeded. Status: 429. The response carries Retry-After in seconds; respect that value in place of your own backoff. Action: see Rate limits for tier ceilings.
Server
internal.error
An unexpected error occurred on our side. Status: 500. The message is intentionally generic; request_id is the correlation key for support. Action: retry with exponential backoff (one second, doubling, capped at sixty); if it persists, contact support with the request_id.
upstream.bad_gateway
A required upstream service returned an unparseable response. Status: 502. Action: retry with backoff; if it persists, the upstream is degraded — check status.pullfirst.com.
upstream.unavailable
A required upstream service is unreachable or returning errors. Status: 503. Action: retry with backoff; honor Retry-After if present.
upstream.timeout
A required upstream service did not respond within the timeout window. Status: 504. Action: retry with backoff.
Generic fallback
bad_request
A 4xx response that does not fit any more specific code. Status: 400. Action: read error.message for the specific failure.
Status code → category
| HTTP | Default code | Notes |
|---|---|---|
| 400 | bad_request or specific validation.* / query.* / filter.* | Single-field issues land in validation.*; structural issues land in query.* or body.*. |
| 401 | auth.missing | Bearer header absent. |
| 403 | auth.invalid | Bearer header present but rejected. |
| 404 | not_found | Resource or route. |
| 405 | method_not_allowed | Wrong verb on a known route. |
| 409 | conflict | Resource state collision. |
| 415 | unsupported_media_type | Wrong Content-Type. |
| 422 | validation.semantic | Cross-field validation failure. |
| 429 | rate_limit.exceeded | Quota exceeded. Retry-After set. |
| 500 | internal.error | Unexpected server error. |
| 502 | upstream.bad_gateway | Upstream returned an invalid response. |
| 503 | upstream.unavailable | Upstream unreachable. |
| 504 | upstream.timeout | Upstream timed out. |
Retry guidance
4xx responses are deterministic — fix the request before retrying. The exception is 429, which is recovered by waiting out Retry-After.
5xx responses are worth retrying with exponential backoff: start at one second, double each attempt, cap at sixty seconds. Stop after five attempts and surface the request_id to the user; persistent failures usually need investigation rather than more retries.
Always honor Retry-After when present, in place of your own backoff.
Stability promise
Codes are part of the API contract:
- New codes may be added.
- Existing codes will not be renamed.
- Existing codes will not be removed without a major version bump.
- Fields on the envelope may be added; existing fields will not be removed or have their type changed.
Switch on error.code with confidence.