PULLFIRST[THE RECORD]

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 as error.request_id. Set on every response, not just errors.
  • X-Error-Code — same value as error.code. Useful for log scrapers, CDN routing, and dashboard aggregation without parsing the body.
  • Retry-After — seconds until the window opens, on 429 and some 503 responses.

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 (lonlng, latitudelat, qquery). 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

HTTPDefault codeNotes
400bad_request or specific validation.* / query.* / filter.*Single-field issues land in validation.*; structural issues land in query.* or body.*.
401auth.missingBearer header absent.
403auth.invalidBearer header present but rejected.
404not_foundResource or route.
405method_not_allowedWrong verb on a known route.
409conflictResource state collision.
415unsupported_media_typeWrong Content-Type.
422validation.semanticCross-field validation failure.
429rate_limit.exceededQuota exceeded. Retry-After set.
500internal.errorUnexpected server error.
502upstream.bad_gatewayUpstream returned an invalid response.
503upstream.unavailableUpstream unreachable.
504upstream.timeoutUpstream 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.