Errors

Every error response uses the RFC 7807 / RFC 9457 application/problem+json shape, so error handling is uniform across every operation — there is no per-operation error format to special-case.

http
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json

{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "missing scope 'orders:write' for 'orders.archive.place'"
}

The problem envelope

The error body is Geopera’s Problem object — an RFC 7807 / RFC 9457 problem detail. The Content-Type is always application/problem+json (the 422 validation case is the one structured exception, described below).

FieldTypeMeaning
statusintegerThe HTTP status code (mirrors the response status line)
titlestringA short, stable, human-readable summary of the error class
detailstringA specific, contextual message about this occurrence
typestring (URI)A URI identifying the problem type; defaults to about:blank when the status code alone is sufficient
instancestring (URI)Optional. A URI reference identifying the specific occurrence (RFC 7807 member)

title, status, and detail are always present. type defaults to about:blank. Geopera may also publish typed problem URIs (for example https://api.geopera.com/problems/not-found) so clients can branch on type rather than parsing prose.

Extension members

RFC 7807/9457 allows a problem object to carry additional top-level members beyond the four standard fields. Geopera uses these to attach machine-actionable context to specific error classes — for example a client_secret on a 3-D Secure 402, or retry hints alongside a 429.

Tip — Treat unknown extension members as optional. Branch on type/status first, then read the members you recognise.

Status codes

CodeTitleTypical cause
400 Bad RequestBad RequestA structurally invalid request (e.g. body is not valid JSON)
401 UnauthorizedUnauthorizedMissing, malformed, or expired Bearer token
402 Payment RequiredPayment RequiredInsufficient credits, no/declined card, or a past-due account
403 ForbiddenForbiddenThe principal lacks the operation’s scope, or isn’t a member of the target’s project/org
404 Not FoundNot FoundA bad id, an unknown operation, or a resource outside your organization (returned as 404, not 403, to avoid leaking existence)
409 ConflictConflictAn invalid state transition, or a concurrent change that lost the race
410 GoneGoneAn expired approval, or a retention-expired asset
422 Unprocessable EntityValidation ErrorThe body parsed but failed the operation’s typed schema or a domain rule
429 Too Many RequestsToo Many RequestsA per-key rate limit, or an egress/bandwidth limit
500 Internal Server ErrorInternal Server ErrorAn unexpected backend fault

All of these — except the structured 422 below — are returned as application/problem+json with the envelope above.

Validation errors (422)

Because every operation has a typed input model, malformed inputs fail fast with 422 Unprocessable Entity before any side-effect runs — the wrong input never reaches the handler, and a 422 is therefore always safe (nothing was charged or mutated).

The 422 body is Geopera’s HTTPValidationError: a top-level detail array of field-level ValidationError objects, so you get per-field diagnostics rather than a single prose string.

json
{
	"detail": [
		{
			"loc": ["body", "geometry", "coordinates"],
			"msg": "value is not a valid list",
			"type": "type_error.list"
		}
	]
}
FieldTypeMeaning
locarray of string/integerThe path to the offending field, from the request root (e.g. ["body", "geometry", "coordinates"])
msgstringA human-readable description of why this field failed
typestringA stable machine code for the failure class (e.g. type_error.list, value_error.missing)

Each element’s loc, msg, and type are required. To surface a validation failure in a form, walk the detail array and map each loc to its input field.

Payment & spend errors (402)

Spend operations (placing an order, topping up, creating a processing job) surface billing problems as 402 Payment Required:

  • Insufficient credits — top up, or enable auto top-up.
  • No / declined card — attach a payment method.
  • 3-D Secure required — the response carries a client_secret extension member so the client can complete the bank challenge, then retry.

A 402 means the request was well-formed and authorized but could not be settled. Resolve the billing condition and retry (using the same idempotency key for spend operations).

State conflicts (409)

A 409 Conflict means the request was valid but the resource was not in a state that permits it — an invalid state transition, or a concurrent change that won the race. It also surfaces idempotency-key reuse conflicts: replaying an Idempotency-Key with a different request body is rejected as a conflict rather than silently overwriting the first request. See Idempotency for how to retry spend operations safely.

Expired & retired (410)

A 410 Gone is returned when a resource that once existed has expired or been retired — for example an approval whose window has lapsed, or an asset past its retention horizon. Unlike 404, a 410 asserts the resource existed and is now permanently unavailable; re-issuing the underlying request (a fresh approval, a new order) is the correct recovery.

Rate limits (429)

A 429 Too Many Requests is returned when you exceed a per-key request rate or an egress/bandwidth limit. The response carries retry guidance — back off and retry the request. See Rate limits for the headers, limits, and the recommended backoff strategy.

403 vs 404

To avoid leaking the existence of resources across organizations, the platform returns 404 Not Found (not 403 Forbidden) when you reference a resource that exists but belongs to another organization. A 403 means you authenticated, but your scope or project membership doesn’t permit this action on a resource you can see. See Scopes for how operation scopes map to principals.

Handling errors

Branch on status (and, where published, type); read detail for logging and 422’s detail[] for field-level UI. The SDKs raise typed exceptions that wrap this same envelope, so the fields above are available on the raised error object.

python
import httpx

resp = httpx.post(
    "https://api.geopera.com/v1/op/orders.archive.place",
    headers={"Authorization": "Bearer gpra_..."},
    json={"items": []},
)

if resp.is_error:
    problem = resp.json()
    if resp.status_code == 422:
        for err in problem["detail"]:
            field = ".".join(str(p) for p in err["loc"])
            print(f"{field}: {err['msg']} ({err['type']})")
    else:
        print(f"{problem['status']} {problem['title']}: {problem['detail']}")
else:
    print(resp.json())
typescript
const resp = await fetch('https://api.geopera.com/v1/op/orders.archive.place', {
	method: 'POST',
	headers: {
		Authorization: 'Bearer gpra_...',
		'Content-Type': 'application/json'
	},
	body: JSON.stringify({ items: [] })
});

if (!resp.ok) {
	const problem = await resp.json();
	if (resp.status === 422) {
		for (const err of problem.detail) {
			const field = err.loc.join('.');
			console.error(`${field}: ${err.msg} (${err.type})`);
		}
	} else {
		console.error(`${problem.status} ${problem.title}: ${problem.detail}`);
	}
}

Related