Error Codes

Error responses use the same envelope as success responses: { "meta": {}, "links": {}, "data": { "error", "code", "tracking_id" } }. The data object contains the human-readable message (error), machine-readable code (code), and per-request correlation id (tracking_id). This document lists all error codes returned by the API.

Response shape

All error responses use the envelope with error payload in data:

{
  "meta": {},
  "links": {},
  "data": {
    "error": "Human-readable message",
    "code": "ERROR_CODE",
    "tracking_id": "uuid-or-client-provided-id"
  }
}

The HTTP status code is in the response status. The same id is also returned in the x-correlation-id response header so clients can log it without parsing the body. The id is Fastify's request.id (generated UUID, or the request's x-correlation-id if provided and valid—alphanumeric, underscore, and hyphen).


Error code reference

HTTPCodeError messageWhen it occurs
400BAD_REQUESTvariesGeneric bad request for malformed or invalid input that does not fit a more specific code.
400CHECKOUT_PRICE_CHECKSUM_MISMATCHThe price changed since this quote. Confirm the new estimate.POST /ingestions/:id/checkout-session carried a price_checksum that no longer matches what the selection costs. Neither figure is charged. details carries expected, received and estimate — the full fresh estimate, so the caller re-renders the price and asks the customer to confirm again. Not retryable as sent: the same body fails identically. Detects drift, not tampering — the amount is always recomputed server-side.
400MISSING_FILEMissing file upload in `file` fieldA multipart request to an ingestion endpoint did not contain a file part, or the part had a different field name.
400MISSING_LOCATION_TYPEMissing required field "location_type"; allowed values: […]The location_type multipart field was not provided. Must be facility, plot, or shed.
400INVALID_LOCATION_TYPEInvalid location_type "…"; allowed values: […]The location_type value is not in the allowed set.
400INVALID_COMMODITY_TYPEInvalid commodity_type "…"; allowed values: […]The commodity_type value is not in the allowed set (cattle, cocoa, coffee, palm, rubber, soy, timber, shrimp), or the field was sent empty (omit it entirely for auto-detection). Common aliases (e.g. palm oil / oil palm → palm) are normalised to a canonical value rather than rejected.
400INVALID_COMMODITY_COLUMNInvalid commodity_column; expected one non-empty column name of at most 255 charactersThe multipart commodity_column field was sent empty, sent more than once, or is longer than 255 characters. Omit it to let the commodity column be detected.
400MULTIPART_REQUIREDFile uploads must use multipart/form-data with a `file` fieldThe request body is not multipart/form-data on an upload endpoint.
400INVALID_MULTIPARTFailed to parse multipart requestThe multipart request body could not be parsed (malformed boundaries, truncated stream, etc.).
400MISSING_CONTENT_LENGTHContent-Length header is required for file uploadsThe Content-Length header was not provided on a file upload request.
400VALIDATION_ERRORRequest validation failedFastify/Zod validation failed for request params, query, body, or headers. Returns a summary such as Request validation failed: params/id: Invalid UUID. Details in meta.validation and meta.validationContext.
400WEBHOOK_URL_INVALIDWebhook URL must use HTTPS / cannot target localhostThe webhook subscription URL failed validation: must use HTTPS (HTTP allowed in dev only) and must not target localhost, 127.0.0.1, or 0.0.0.0.
400WEBHOOK_VERIFICATION_FAILEDVerification failedThe webhook URL verification handshake failed (target did not respond correctly to the challenge).
400UNSUPPORTED_MEDIA_TYPEUnsupported file type; expected CSV, Parquet, JSON, GeoJSON, GeoPackage, Shapefile, or ExcelThe file's Content-Type and/or extension are not in the allowed set during ingestion.
400AUTHORIZATION_TEXT_VERSION_MISMATCHAuthorisation text_version "…" is not the current versionA DDS authorisation was submitted referencing a retired legal-text version. The current version is returned in meta.expected.
401MISSING_API_KEYMissing API keyThe x-api-key header is absent or empty. Sent by the global API key auth preHandler before any route runs.
401INVALID_API_KEYInvalid keyThe x-api-key header was present but the key could not be validated: wrong prefix, key not found, revoked/expired, or lookup failure.
401UNAUTHORIZEDUnauthorizedThe route requires a member-level context but the client used an org-level key, or auth context is missing entirely.
403FORBIDDENAccess deniedThe authenticated caller does not have access to the requested resource (organization mismatch).
403FORBIDDENInsufficient scopeThe API key does not have the required scope. Only applies to member keys (sk-epoch-) with explicit scopes set.
403SERVICE_NOT_ENABLEDCadastral service is not enabled for this organisationThe caller invoked an endpoint for a service (e.g. cadastral parcel search) that the organisation is not entitled to use.
404NOT_FOUNDvaries (e.g. "Facility not found", "Plot not found", "Supply shed not found")The requested resource does not exist. Returned by individual route handlers when a lookup by ID yields no result.
404SUBSCRIPTION_NOT_FOUNDThis organisation has no live subscription.DELETE /organizations/:id/subscription or POST /organizations/:id/subscription/resume found no live subscription. A subscription past its period end is canceled and is not live.
406NOT_ACCEPTABLEAccept header must include one of application/json, application/x-ndjson, …The Accept header is missing or does not include a supported content type.
409CONFLICTTask status is '…'; ingestion requires 'completed' or 'completed_with_errors'The task is not in a valid state for the requested operation (e.g. parcel ingestion on a non-completed task, or requesting a summary before completion).
409TAG_NAME_CONFLICTA tag named '…' already existsA tag rename was attempted but the target name is already in use within the organization.
409CHECKOUT_SESSION_PENDINGA checkout session for this selection is being created.POST /ingestions/:id/checkout-session lost the race for the same task and selection, and the winner has not yet recorded its Stripe session id. Retryable — the same request succeeds once the winner finishes. On POST /organizations/:id/checkout-sessions it means another credit pack is being created, or the previous one is completing payment. On POST /organizations/:id/subscription/checkout-session it means a plan purchase or upgrade for the organisation is already waiting for payment.
409CHECKOUT_SESSION_CONFLICTThis checkout session could not be recorded. Try again.POST /ingestions/:id/checkout-session created a Stripe session but could not record it: another row already holds that session id, or the claimed row had moved on. The claim is released, so a retry is clean.
409CHECKOUT_SUBSCRIPTION_EXISTSThis organisation already has a subscription.POST /ingestions/:id/checkout-session carried a plan, or POST /organizations/:id/subscription/checkout-session was called, while the organisation already holds a live subscription (pending, incomplete, trialing, active, past_due or unpaid) — including a same-tier request, a live row that is not active or trialing, and a negotiated plan key. details carries plan_key and status for the subscription that already holds the slot. Enforced by a partial unique index before any Stripe call, so two concurrent purchase attempts cannot both win.
409CHECKOUT_PLAN_DOWNGRADEA self-serve plan can only move to a higher tier.POST /organizations/:id/subscription/checkout-session or POST /ingestions/:id/checkout-session asked for a tier below the live one (starter < growth < pro). details carries plan_key (the live tier) and requested. Nothing is claimed and Stripe is never called.
409CHECKOUT_UPGRADE_IN_PROGRESSAn upgrade for this organisation is already waiting for payment.POST /organizations/:id/subscription/checkout-session or POST /ingestions/:id/checkout-session tried to claim a second upgrade row while one is already pending. details carries plan_key and requested. Complete or abandon the waiting upgrade; one per organisation is enforced by a unique index.
409SUBSCRIPTION_NOT_CANCELLABLEvaries — names the status, a missing Stripe subscription, or a change Stripe appliedDELETE /organizations/:id/subscription or POST /organizations/:id/subscription/resume found a pending or incomplete subscription, a live row with no Stripe subscription yet, a negotiated plan, or Stripe applied the change but reports a status the route does not show (for example paused). details.status names the status.
409INGESTION_RUN_IN_PROGRESSPart of this ingestion is still being read. Quote it once every task in the run has finished.POST /ingestions/:id/cost-estimate was called on a plot upload that also carried address-only rows, while one of its two tasks is still pending, processing or validating. The address rows are geocoded as facilities on a sibling task; poll run.overall_status on either task and quote once it leaves processing.
413PAYLOAD_TOO_LARGEUploaded file is too large (max 100MB)The uploaded file exceeds the configured size limit. Detected from Content-Length or mid-stream.
415UNSUPPORTED_MEDIA_TYPEUnsupported Media TypeFastify rejected an unregistered content type at the framework level (distinct from the ingestion-level 400 check).
422COST_ESTIMATE_TASK_NOT_ELIGIBLEA cost estimate applies only to a screening run, requested through the self-serve journey.POST /ingestions/:id/cost-estimate was called on a task whose run kind is not screening (a full run, or a task with an unknown run kind); on a task a direct org key created, or one created before its creator was recorded; or the caller's org API key was not issued for the self-serve journey (key_type is not self_serve). A member key carries no key_type and is unaffected as a caller, and a task a member created is eligible.
422CHECKOUT_TASK_NOT_ELIGIBLEA checkout session applies only to a screening run, bought through the self-serve journey.The same three causes as the row above, on POST /ingestions/:id/checkout-session. Named for the checkout route so a consumer branching on code is not handed a cost-estimate-named failure.
422CREDIT_CHECKOUT_NOT_ELIGIBLEA credit purchase is available only to the self-serve journey.POST /organizations/:id/checkout-sessions was called with an org API key whose key_type is not self_serve, or with a member key for an organisation whose credits are not enforced (its runs never spend credits, so a pack would sit unused).
422CHECKOUT_NOTHING_CHARGEABLEVaries — names the cause: the balance, exemption, or nothing to price.POST /ingestions/:id/checkout-session found nothing to charge for. FOUR causes share this code and the message names which one. Covered (the one credit balance) and exempt both mean there is no payment to make, so proceed without a checkout. An empty or unpriceable ingestion means there is no priceable work — fix the ingestion instead.
422CHECKOUT_SELECTION_TOO_LARGEA selection may cover at most 10060 units.The selection's total entitlement exceeds the per-session cap. Split it into several checkouts.
422OPERATOR_INCOMPLETEvariesA DDS record PATCH referenced an operator that is incomplete (missing required operator fields) for the requested update.
422PRE_SUBMISSION_BLOCKEDDDS submission blocked by pre-flight checksA DDS submission failed pre-flight validation. The blocking reasons are listed in data.details.blockers.
422GEOCODE_NO_RESULTcould not geocode supplierThe geocoder returned no usable result for the supplier — zero features, or the top feature has no valid WGS84 Point geometry. Returned by GET /discovery/validate/{taskId}/results, and as the failed task's error_code on …/status.
409TRACES_REFERENCE_PENDINGTRACES has not yet issued a reference/verification number for this statementThe TRACES read-through statement routes need the statement's reference and verification number, which TRACES issues asynchronously after a submit. Retry once the reconciliation sweep has recorded them.
429RATE_LIMIT_EXCEEDEDvariesThe client has exceeded the rate limit. Retry after the duration indicated in the Retry-After response header.
502CHECKOUT_GATEWAY_REJECTEDStripe refused the checkout session. Retrying will not help.POST /ingestions/:id/checkout-session, DELETE /organizations/:id/subscription or POST /organizations/:id/subscription/resume reached Stripe and Stripe refused the request — an invalid parameter, a bad key, a missing price. Not transient: the same request fails identically until the cause is fixed.
502TRACES_REQUEST_REJECTEDvariesA TRACES read-through or lookup call reached TRACES and TRACES answered with an authoritative fault. Not transient: the same request fails identically until the cause is fixed.
503TRACES_UNAVAILABLEvariesA TRACES read-through or lookup call could not reach TRACES — network failure, timeout, or client configuration. Transient: safe to retry.
503CHECKOUT_UPSTREAM_ERRORCould not reach Stripe to create the session. Try again.POST /ingestions/:id/checkout-session, DELETE /organizations/:id/subscription or POST /organizations/:id/subscription/resume could not reach Stripe (a connection or API fault), or Stripe rate-limited the call. The subscription routes change nothing on this error. The claim it took is released before responding, so a retry succeeds once Stripe recovers rather than 409ing forever.
503CHECKOUT_NOT_CONFIGUREDCheckout is not enabled on this deployment.The deployment has no Stripe gateway configured. Also returned by the subscription cancel and resume routes. Distinct from SERVICE_NOT_ENABLED, which is a 403 about the cadastral service: this is a deployment gap, not an entitlement one.
500INTERNAL_SERVER_ERRORInternal Server ErrorAn uncaught exception occurred. The client always receives this generic message; the real error is logged server-side.
500STORAGE_ERRORFailed to persist uploaded fileThe storage layer failed to persist an uploaded file (disk error, permissions, driver failure, etc.).
502TASK_DISPATCH_FAILEDFailed to dispatch taskThe API accepted the request but could not hand the long-running job off to the task runner (dispatch/transport failure).
502GEOCODE_UPSTREAM_ERRORgeocoding service is unavailableA transient upstream/transport failure while geocoding (sco2api 5xx, network error, or request timeout) — lets agent callers tell "retry me" apart from an unresolvable supplier. Returned by POST /discovery/validate (the task could not be started) and GET /discovery/validate/{taskId}/results, and as the failed task's error_code on …/status.
422COMMODITY_COLUMN_NOT_FOUNDcommodity_column '…' is not a column of this file.POST /ingestions/profile named a commodity_column the file does not have. details carries commodity_column and columns, the file's column names. An upload with the same column fails its task with this message.
502PROFILE_UPSTREAM_ERRORCould not profile the file: the upstream profiler did not respond successfully.The file profiler was unreachable or failed. Distinct from a 400/415 on the file itself, so a caller can tell "retry me" from "your file is wrong". Returned by POST /ingestions/profile.
503CREDIT_LEDGER_UNAVAILABLECredit ledger is temporarily unavailable; try again shortlyThe credit ledger is not reachable, so no balance can be read. A fault on our side, not an entitlement refusal — never read it as a zero balance. Carries retry-after. Returned by GET /organizations/:id/credits.

By source

Global error handler

HTTPCodeDescription
415UNSUPPORTED_MEDIA_TYPEFastify's content-type parser rejects an unregistered media type (FST_ERR_CTP_INVALID_MEDIA_TYPE).
400VALIDATION_ERRORFastify/Zod validation fails. Includes raw validation array and context under meta.validation and meta.validationContext.
429RATE_LIMIT_EXCEEDEDRate limiter rejects the request.
500INTERNAL_SERVER_ERRORAny uncaught exception; response message is always "Internal Server Error".

Accept header plugin

HTTPCodeDescription
406NOT_ACCEPTABLEAccept header is missing or does not include a supported content type (application/json, application/x-ndjson, application/geo+json, application/x-ndgeo+json).

API key auth plugin

HTTPCodeDescription
400BAD_REQUESTDuplicate x-api-key headers detected.
401MISSING_API_KEYx-api-key header is absent or empty.
401INVALID_API_KEYKey has wrong prefix, is not found, or is revoked/expired.

Auth context

HTTPCodeDescription
401UNAUTHORIZEDAuth context is missing or the route requires a member-level key but an org key was used.
401INVALID_API_KEYAuth context lookup failed.

Asset task access

HTTPCodeDescription
404NOT_FOUNDThe asset task ID does not exist.
403FORBIDDENThe API key's organization does not own the asset task.

Scope authorization

HTTPCodeDescription
403FORBIDDENThe API key does not have the required scope for the endpoint.

Ingestion handler

HTTPCodeDescription
400MULTIPART_REQUIREDRequest body is not multipart/form-data.
400INVALID_MULTIPARTMultipart request could not be parsed.
400MISSING_FILENo file part in the multipart request.
400MISSING_CONTENT_LENGTHContent-Length header not provided for file upload.
400MISSING_LOCATION_TYPElocation_type field not provided.
400INVALID_LOCATION_TYPElocation_type value is not in the allowed set.
400INVALID_COMMODITY_TYPEcommodity_type value is not in the allowed set.
400INVALID_COMMODITY_COLUMNcommodity_column was empty, duplicated, or longer than 255 characters.
400UNSUPPORTED_MEDIA_TYPEFile type/extension is not in the allowed set.
413PAYLOAD_TOO_LARGEFile exceeds the configured size limit.
422COST_ESTIMATE_TASK_NOT_ELIGIBLEPOST /ingestions/:id/cost-estimate called on a task that is not a screening run, on one a direct org key created (or with no recorded creator), or by an ORG key whose key_type is not self_serve. Member keys are unaffected.
409INGESTION_RUN_IN_PROGRESSPOST /ingestions/:id/cost-estimate called while a task of a split plot upload is still running.
500STORAGE_ERRORStorage driver failed to persist the file.

Checkout session handler

POST /ingestions/:id/checkout-session, and GET for the two 503s.

CodeErrorMeaning
400CHECKOUT_PRICE_CHECKSUM_MISMATCHThe quote's price_checksum no longer matches what the selection costs. The fresh estimate rides in details.estimate; re-confirm with the customer rather than retrying.
409CHECKOUT_SESSION_PENDINGA concurrent request is creating a session for the same task and selection. A claim left stranded past 15 minutes is cleared, and the caller is told to retry.
409CHECKOUT_SESSION_CONFLICTThe Stripe session could not be recorded against the claimed row. The claim is released, so a retry is clean.
409CHECKOUT_SUBSCRIPTION_EXISTSThe body carried a plan and the organisation already holds a live subscription. details carries plan_key and status for the one that holds the slot.
409CHECKOUT_PLAN_DOWNGRADEThe body carried a plan below the live tier. details carries plan_key and requested.
409CHECKOUT_UPGRADE_IN_PROGRESSThe body carried a higher plan while an upgrade for the organisation is already pending. details carries plan_key and requested.
422CHECKOUT_TASK_NOT_ELIGIBLECalled on a task that is not a screening run, on one a direct org key created (or with no recorded creator), or by an ORG key whose key_type is not self_serve. Member keys are unaffected.
422CHECKOUT_NOTHING_CHARGEABLEEvery unit in the selection is already covered, so there is nothing to charge for.
422CHECKOUT_SELECTION_TOO_LARGEThe selection's total entitlement exceeds 10060 units (UNITS_EXPECTED_MAX).
502CHECKOUT_GATEWAY_REJECTEDStripe refused the request. Not transient — retrying will not help.
503CHECKOUT_UPSTREAM_ERRORStripe was unreachable while creating the session. Retryable — the claim is released.
503CHECKOUT_NOT_CONFIGUREDThe deployment has no Stripe gateway configured.

Subscription handler

DELETE /organizations/:id/subscription and POST /organizations/:id/subscription/resume.

CodeErrorMeaning
404NOT_FOUNDThe organisation is not in this key's scope.
404SUBSCRIPTION_NOT_FOUNDThe organisation has no live subscription.
409SUBSCRIPTION_NOT_CANCELLABLEThe subscription is pending or incomplete, has no Stripe subscription yet, is a negotiated plan, or Stripe reports a status the route does not show. details.status names it; a negotiated plan also carries details.plan_key.
502CHECKOUT_GATEWAY_REJECTEDStripe refused the request. Not transient.
503CHECKOUT_UPSTREAM_ERRORStripe was unreachable or rate-limited the call. Retryable. Nothing changed.
503CHECKOUT_NOT_CONFIGUREDThe deployment has no Stripe gateway configured.

Subscription checkout handler

POST /organizations/:id/subscription/checkout-session — buys or upgrades a plan with no task behind it.

CodeErrorMeaning
400BAD_REQUESTreturn_url is off the origin allowlist, plan is not a self-serve tier, or the body failed schema validation. Checked before the gateway, so it fires on a deployment without Stripe.
401UNAUTHORIZEDMissing or invalid credentials.
403FORBIDDENThe key lacks billing:write.
404NOT_FOUNDThe organisation is not in this key's scope.
409CHECKOUT_SUBSCRIPTION_EXISTSThe organisation already holds a live subscription on this tier, on a tier that cannot be upgraded, or on a negotiated plan key. details carries plan_key and status.
409CHECKOUT_PLAN_DOWNGRADEplan is a lower tier than the live one. details carries plan_key and requested.
409CHECKOUT_UPGRADE_IN_PROGRESSAn upgrade for the organisation is already waiting for payment. details carries plan_key and requested.
409CHECKOUT_SESSION_PENDINGA plan purchase or a credit pack for this organisation is being created, or is mid-payment. Retryable once it settles.
422CREDIT_CHECKOUT_NOT_ELIGIBLEAn org key whose key_type is not self_serve, or a member key for an organisation whose credits are not enforced.
502CHECKOUT_GATEWAY_REJECTEDStripe refused the request. Not transient.
503CHECKOUT_UPSTREAM_ERRORStripe was unreachable while creating the session. Retryable — the claim is released.
503CHECKOUT_NOT_CONFIGUREDThe deployment has no Stripe gateway configured. Answered before the eligibility ladder, so the 409s above are unreachable without a gateway.

Location handlers

HTTPCodeDescription
404NOT_FOUNDFacility, plot, supply shed, or metric not found by ID.

Discovery handler

HTTPCodeDescription
404NOT_FOUNDConfidence / source-discovery task is unknown, or belongs to a different organization (ownership is enforced upstream and returns 404 — never 403 — so a guessed id cannot be probed). Poll endpoints only.
409CONFLICTThe task is still processing; keep polling …/status. Returned by the …/results poll endpoints.
422GEOCODE_NO_RESULTThe geocoder returned no usable feature or Point geometry.
502GEOCODE_UPSTREAM_ERRORTransient upstream failure (sco2api 5xx, network, or timeout), or a completed confidence task with no locatable feature.

Parcel handlers

HTTPCodeDescription
400VALIDATION_ERRORParcel search request body validation failed.
403SERVICE_NOT_ENABLEDThe cadastral service is not enabled for the caller's organisation.
409CONFLICTTask is not in a valid state for ingestion or summary retrieval.
404NOT_FOUNDParcel task not found.

EUDR handlers

HTTPCodeDescription
403FORBIDDENOrganization does not own the EUDR resource.
404NOT_FOUNDEUDR task or resource not found.
400AUTHORIZATION_TEXT_VERSION_MISMATCHDDS authorisation references a retired legal-text version.
422OPERATOR_INCOMPLETEDDS record PATCH references an operator missing required fields.
422PRE_SUBMISSION_BLOCKEDDDS submission blocked by pre-flight checks (details.blockers).
500INTERNAL_SERVER_ERRORFailed to retrieve or submit EUDR DDS.
403FORBIDDENTRACES read-through of a manual filing, or POST /dds/lookup, without the eudr:verify scope.
409TRACES_REFERENCE_PENDINGTRACES has not issued this statement's reference/verification number yet.
502TRACES_REQUEST_REJECTEDTRACES answered a read-through or lookup with an authoritative fault.
503TRACES_UNAVAILABLETRACES could not be reached for a read-through or lookup.

The blocker types a PRE_SUBMISSION_BLOCKED response carries, including the HS-code and supplementary-unit blockers, are listed in the readiness table of Get DDS Record.

Webhook handlers

HTTPCodeDescription
400WEBHOOK_URL_INVALIDURL is not HTTPS or targets a blocked host.
400WEBHOOK_VERIFICATION_FAILEDURL verification handshake failed.
404NOT_FOUNDWebhook subscription or delivery log not found.

Tag handlers

HTTPCodeDescription
403FORBIDDENOrganization does not own the tag.
404NOT_FOUNDTag not found by ID.
409TAG_NAME_CONFLICTA tag with the target name already exists in the organization.

Artifact / report generation

HTTPCodeDescription
502TASK_DISPATCH_FAILEDThe request was accepted but the long-running job could not be handed off to the task runner.

Notification handlers

HTTPCodeDescription
404NOT_FOUNDNotification not found by ID.

Internal routes

HTTPCodeDescription
401UNAUTHORIZEDInternal callback authentication failed.
400BAD_REQUESTInvalid task worker callback payload.

Validation details in meta

When a request fails validation (e.g. invalid UUID in a path parameter, missing required field in the body), the error handler returns a VALIDATION_ERROR and enriches the meta object with:

  • meta.validation — the raw validation array from Fastify/Zod. Each entry typically includes:
    • instancePath — JSON Pointer-style path to the offending field (e.g. "/id" or "/page_size").
    • message — human-readable explanation (e.g. "Invalid UUID" or "Expected number").
    • Other adapter-specific fields (e.g. keyword, schemaPath, params).
  • meta.validationContext — where the error occurred:
    • "params" — URL path params (e.g. /locations/facilities/:id).
    • "querystring" — query parameters.
    • "body" — request body.
    • "headers" — request headers.

The data.error field contains a concise summary assembled from these entries, for example:

Request validation failed: params/id: Invalid UUID; querystring/page_size: Expected number

Clients can use data.error for user-facing messages and read meta.validation / meta.validationContext for detailed, machine-readable diagnostics (e.g. form-level error mapping).