Errors
Every failure the API returns is one shape:
{ "error": { "type": "quota_exceeded", "message": "Monthly document quota reached: 200 of 200 documents used this month. The cap resets on 2026-08-01. Email sales@easting.ai to raise it.", "documents_used": 200, "monthly_cap": 200, "resets_on": "2026-08-01" }}Switch on error.type; it is stable. error.message is written to be shown
to a person verbatim, which is what the QGIS plugin does. Extra fields ride
inside the error object where a type has numbers worth acting on: a 402
carries documents_used and resets_on, a 429 carries its limit. Anything
protocol-shaped stays in the protocol: back-off time is a Retry-After
header, never a JSON field.
400: the request was understood and refused
Section titled “400: the request was understood and refused”All five are on POST /v1/signup, the free-tier door.
| type | when it fires | what to do |
|---|---|---|
invalid_email |
The address does not parse as an email. | Fix the address. |
terms_not_accepted |
accepted_terms was not true. |
Accept the Terms of Service and Privacy Policy, then resend. |
disposable_email |
The domain is a known disposable-mail provider. | Use an address you can receive mail at; the key and invite go there. |
undeliverable_domain |
The domain accepts no mail, so the invitation would bounce. | Check the spelling. |
already_granted |
This address already used its free documents. | Sign in at app.easting.ai, or buy a plan or a pack. |
401: fix the credential, retrying will not help
Section titled “401: fix the credential, retrying will not help”| type | when it fires | what to do |
|---|---|---|
invalid_api_key |
The bearer token is missing, malformed, or matches no key. | Paste the current key from your account page. |
revoked_api_key |
The key existed and was revoked, including by a rotation. | Use the successor key, or contact sales@easting.ai. |
invalid_token |
A portal session token (not a key) failed verification. | Sign in again. |
402: payment, quota, or allowance
Section titled “402: payment, quota, or allowance”The family that matters most, because each type calls for different client
behavior. All carry the numbers (documents_used plus the relevant cap) so a
client can display the state rather than guess it.
quota_exceeded: the account’s cap is reached, counting work already in
flight. On a subscription past its overage ceiling the message names the
ceiling and resets_on; the ceiling exists to catch runaway usage, so
reaching it is a conversation (add seats in the billing portal, or email
sales) rather than a wall. Nothing was submitted and nothing was billed. Do
not retry until the reset or the raise.
pack_exhausted: a document pack’s credits are spent. Credits never
expire, and neither did these: they did their job. Buy another pack (a new
key arrives instantly) or subscribe for monthly volume; the response carries
the checkout path.
trial_exhausted: the trial’s 3 documents are used. The subscription
starts automatically on trial_ends_on and unlocks the full quota; manage or
cancel at easting.ai/account. Waiting is a valid strategy here; buying is not
required.
free_tier_exhausted: the free documents are used. This is the free
tier’s only conversion moment, so the response carries both paths: a $10 seat
(50 documents a month) and a $30 pack (10 that never expire), as subscribe
and pack checkout paths.
checkout_incomplete: the claim page was reached with a Stripe Checkout
session that never completed. Finish the checkout; if you were charged
anyway, email support@easting.ai.
403: yours, but not this
Section titled “403: yours, but not this”| type | when it fires | what to do |
|---|---|---|
upload_not_yours |
An upload id that could not be one of this key’s (wrong shape, or a path escape). | Request a fresh slot from POST /v1/uploads. |
not_permitted |
A staff-only endpoint, called without the staff group. | Nothing; the endpoint is for Easting staff. |
Ownership failures elsewhere deliberately answer 404, so an id can never confirm that somebody else’s resource exists.
404: no such resource under this caller
Section titled “404: no such resource under this caller”| type | when it fires | what to do |
|---|---|---|
no_such_job |
GET /v1/extracts/{job} with an id this key never submitted. |
Check the id; an unknown id and someone else’s look identical. |
no_such_batch |
A batch id this key never submitted. | Same. |
no_such_upload |
The staged document is gone. Ids work exactly once and expire unused. | Stage the document again. |
no_such_key |
A key prefix that does not exist on this account, e.g. in GET /v1/usage?prefix=. |
List your keys; prefixes outside the account are invisible by design. |
no_subscription |
A billing-portal route on a key billed by arrangement or by pack. | Email sales@easting.ai; there is no self-serve portal for it. |
409: the request conflicts with current state
Section titled “409: the request conflicts with current state”| type | when it fires | what to do |
|---|---|---|
batch_not_ended |
Results asked for while the batch runs. Carries batch_status. |
Poll GET /v1/batches/{id} until status is ended. |
rotate_failed |
The key was already revoked or replaced when the rotation ran. | List keys and use the current one. |
too_many_keys |
The account’s key ceiling: ten live keys, or one on a free account. | Revoke one first, or (free) rotate instead of minting. |
no_entitlement |
The account has no active plan to extract against or issue a key against. | Subscribe or buy a document pack first. |
410: the result existed and is gone
Section titled “410: the result existed and is gone”| type | when it fires | what to do |
|---|---|---|
result_expired |
An async job’s result is past its retention window. | Nothing to fetch: results live seven days after a fetch, thirty without one. Re-extract if you still need it. |
results_expired |
A batch’s results, same windows. | Same. |
413: over a size limit
Section titled “413: over a size limit”| type | when it fires | what to do |
|---|---|---|
document_too_large |
One document over its ceiling: 4.5 MB direct, 32 MB staged, PDF size after image conversion. | Stage it through POST /v1/uploads, split it, or rescan lower. The message names both sizes. |
batch_too_large |
A multipart batch whose documents total over the 4.5 MB request ceiling. | Stage the documents and submit the ids, which share no ceiling. |
422: the body cannot be worked with
Section titled “422: the body cannot be worked with”The largest family, grouped by which route raises it.
Document intake (/v1/extract, /v1/extracts, /v1/batches):
| type | when it fires | what to do |
|---|---|---|
empty_document |
The body (or the staged object) was empty. | Send PDF, TIFF, PNG, or JPEG bytes. |
unsupported_format |
The magic bytes match none of the four accepted formats. | Convert to one of them; filenames are not consulted. |
conversion_failed |
A TIFF/PNG/JPEG that could not be read as an image, or has no pages. | If it opens in a viewer, re-export it and try again. |
missing_file |
A multipart body with no file/files part, or a batch with no staged ids. |
Name the form field the message asks for. |
not_multipart |
A batch body that is neither multipart nor a JSON uploads list. |
Use one of the two documented batch bodies. |
too_many_documents |
More than 50 documents in one batch. | Split it across several batches. |
invalid_json |
A batch JSON body that did not parse. | Send {"uploads": [...]}. |
missing_upload_id |
An empty X-Upload-Id. |
Pass the id POST /v1/uploads returned. |
Post-back routes (/v1/certificate, /v1/dxf, /v1/geopackage,
/v1/locate, /v1/corrections). See artifacts for the
signature rules, including why re-serializing the JSON breaks them:
| type | when it fires | what to do |
|---|---|---|
attestation_missing |
The posted body carries no signature. | Re-run the extraction to get a signed response, then post that. |
attestation_mismatch |
The signature does not match the bytes, edited or re-serialized. | Post the response exactly as the API returned it. |
attestation_unavailable |
This server holds no signing key and cannot verify anything. | Contact support@easting.ai; this one is ours. |
nothing_to_draw |
No tract carries computed geometry (DXF, GeoPackage). | Nothing; the burden and reference fields are in the JSON. |
nothing_to_certify |
No tracts and no document verdict (certificate). | Nothing; there is nothing to certify, honestly. |
no_extraction |
The body carries no extraction object. |
Post a whole extraction response back. |
nothing_to_do |
A corrections call with no corrections and no tracts to adjust. | Send at least one of either. |
unknown_source |
A locate source the service does not know. | Use plss_corner. |
source_unavailable |
A locate source that is named but not built yet. | Use plss_corner. |
no_such_tract |
A correction or adjustment names a tract index the extraction lacks. | Indexes are zero-based over extraction.tracts. |
no_such_call |
A correction names a course the tract lacks. | Same, over the tract’s calls. |
unknown_field |
A correction targets a field that is not correctable. | The message lists the correctable fields. |
bad_correction |
A correction value that does not parse (a bearing that is not a bearing, a distance that is not a number). | Fix the value. |
stale_correction |
The correction’s before disagrees with what the extraction now holds. |
Re-read the extraction and correct against what it actually says. |
not_adjustable |
An adjustment on a tract with no traversable geometry. | Nothing; there is no misclosure to distribute. |
Checkout parameters (/billing/checkout):
| type | when it fires | what to do |
|---|---|---|
unknown_tier |
A tier slug that does not exist. | The message lists the self-serve tiers. |
unknown_pack |
A pack slug that does not exist. | pack-10 is the only pack. |
invalid_quantity |
Pack quantity outside 1–25. | For more, email sales@easting.ai. |
invalid_seats |
Seats outside 1–100. | Same. |
429: back off, and the header says how long
Section titled “429: back off, and the header says how long”All three carry Retry-After.
| type | when it fires | Retry-After | what to do |
|---|---|---|---|
rate_limited |
Over the per-minute request limit (120, or 10 free). | 60 | Wait between polls rather than spin. |
too_many_in_flight |
Over the concurrent-extraction limit (25, or 2 free), batch items included. | 30 | Collect results, or send volume through POST /v1/batches, which has no concurrency limit. |
free_tier_busy |
The free tier’s daily signup caps (global or per network) are spent. | 3600 | Try again tomorrow, or start with a document pack. |
The two rate limits are documented on the authentication page.
502: an upstream said no
Section titled “502: an upstream said no”Retrying later is reasonable for all four; nothing was billed on a failed extraction.
| type | when it fires |
|---|---|
extraction_failed |
The model call failed. The message is the actual reason. |
batch_submit_failed |
The batch could not be handed upstream. |
checkout_failed |
Stripe did not return a checkout URL. |
portal_failed |
Stripe did not return a portal URL. |
503: not available on this server
Section titled “503: not available on this server”Each names a capability the deployment lacks rather than a transient fault, so retrying the same server changes nothing.
| type | when it fires |
|---|---|
server_misconfigured |
Extraction itself is unconfigured. Contact support@easting.ai. |
staging_unavailable |
No staging bucket; large documents cannot be sent here. |
async_unavailable |
No async worker; use POST /v1/extract or POST /v1/batches. |
billing_unavailable |
Self-serve billing is not configured. Email sales@easting.ai. |
identity_unavailable |
Accounts are not configured. The API-key path is unaffected. |
free_tier_closed |
Free signups are switched off. Carries no reopening date, because there is none to promise. |
Types an integration should never meet
Section titled “Types an integration should never meet”These exist, use the same envelope, and belong to surfaces an API client does
not call: invalid_admin_token, not_staff (the operator’s console),
not_signed_in, unknown_key, account_unavailable (the legacy /account
pages), invalid_webhook, invalid_signature (the Stripe webhook receiver),
and no_job, no_document (worker plumbing). Meeting one means a request
went somewhere it should not have.