DocsErrors & limits

Open the app →

Errors & limits

Every error uses one envelope: a stable type for programmatic handling, a specific code, a human-readable message, and the request_id.

Envelope
{
  "error": {
    "type": "invalid_request_error",
    "code": "unsupported_file_type",
    "message": "Supported types: PDF, JPEG, PNG, HEIC, HTML.",
    "request_id": "3f1c2a9e-7b4d-4c1e-9a2f-0d5e8b6c4a17"
  }
}

Every response, success or error, carries an X-Request-Id header (a UUID), readable from browsers too. Error bodies repeat it as error.request_id. Include it when you contact support; we log it with every error.

Status codes

typeStatusWhen
—200Successful GETs and PATCH; POST with wait=true that finished in time.
—202Create or retry accepted; extraction continues in the background.
invalid_request_error400, 405, 413, 415, 422Fix the request; retrying it unchanged fails again.
authentication_error401, 403Missing, unknown, revoked, or read-only key.
quota_exceeded402Monthly extraction quota used up. Reads keep working.
not_found404Unknown id, missing document, or unknown path.
rate_limit_error429Per-key rate limit. Respect Retry-After.
api_error409, 500, 502Our side or a conflict. Safe to retry with backoff.

Error codes

Branch on code for specific handling and on type as a fallback. New codes may be added; treat unknown ones by their type.

codeStatusWhen
missing_file400No file field, or the file is empty.
invalid_body400Body could not be parsed (multipart or JSON).
invalid_base64400file_base64 is not valid base64.
invalid_parameter400A query or body parameter is out of range, malformed, or a file_url host is not allowed. The message names the parameter.
invalid_idempotency_key400Idempotency-Key header is empty or longer than 255 characters.
invoice_not_complete400PATCH paid or an approval request on an invoice that is not complete.
already_processing400Retry on an invoice whose extraction is running. Poll it instead.
state_changed409The invoice changed state while a retry was being accepted. Fetch it and try again.
reextract_rate_limited429Too many forced re-extractions today. Wait for Retry-After.
already_complete400Retry on a complete invoice without {"force": true}.
approval_pending400An approval request for this invoice is already waiting on the approver.
missing_api_key401No Authorization header, or it is not Bearer ijk_….
invalid_api_key401Unknown or revoked key.
monthly_quota_reached402The account has used its monthly extraction quota. See Plans and quotas.
insufficient_scope403A read-only key called a write endpoint.
invoice_not_found404Unknown id, or an id on another account.
no_document404The invoice has no stored source document (file download, retry).
route_not_found404No such path.
method_not_allowed405Wrong HTTP method for a known path.
idempotency_in_flight409A request with the same Idempotency-Key is still processing. Retry shortly.
file_too_large413Over 15 MB.
unsupported_file_type415Not PDF, JPEG, PNG, HEIC, or HTML.
file_url_unreachable422file_url could not be fetched (error status, timeout, no body, or too many redirects).
rate_limited429Over 60 requests per minute for this key. Wait Retry-After seconds.
internal500Server error. Safe to retry with backoff.
approval_send_failed502The approval email could not be sent. Safe to retry.

Rate limits

60 requests per minute per API key, counted over a sliding 60-second window. Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds when the oldest request in the window expires). 429 responses also include Retry-After in seconds. 401s do not count. Rate limits are separate from the monthly extraction quota.

Plans and quotas

PlanInvoices / monthPrice
Free10$0
Pro200$29/mo or $290/yr
Business1,000$99/mo or $990/yr

The quota counts extractions across every channel (app, email, API), including each invoice produced by batch-scan splitting. It resets at the start of each calendar month (UTC). Check it with GET /v1/me. Upgrade in the app under Settings, Plan; over 1,000 a month, email support@invoicejet.ai.

At the limit, POST /v1/invoices and retries return 402 monthly_quota_reached with the numbers and an upgrade link; reads keep working. Invoices you upload in the app or email in while you are over the limit are stored with status held instead of being extracted, and are processed when you upgrade or when the quota resets on the 1st; find them with GET /v1/invoices?status=held. An account can have up to 200 invoices waiting at once.

402
{
  "error": {
    "type": "quota_exceeded",
    "code": "monthly_quota_reached",
    "message": "Monthly quota reached (200 of 200).",
    "limit": 200,
    "used": 200,
    "upgrade_url": "https://app.invoicejet.ai/app/settings"
  }
}

Usage is a ledger: extractions that fail are refunded, and deleting an invoice does not give its extraction back. Retrying a failed invoice with POST /v1/invoices/:id/extract uses one extraction; the retry checks quota up front and returns 402 when you are over the limit.

Idempotency & retries

Make POST /v1/invoices safe to retry by sending an Idempotency-Key header (any unique string up to 255 characters; a UUID works well). Retrying with the same key within 24 hours replays the original response (marked with an Idempotency-Replayed: true header) instead of ingesting the document again. If the original request is still processing, the retry gets 409 idempotency_in_flight; wait a moment and try again. Without the header, a retried create ingests twice (duplicate detection flags exact vendor + invoice number + amount matches, but sending a key is the safer path). An empty or over-long key returns 400 invalid_idempotency_key. GETs and PATCH retry freely.

import { randomUUID } from "node:crypto";

// Generate the key once per document and reuse it on every retry.
const idempotencyKey = randomUUID();
await client.invoices.extract({
  fileUrl: "https://example.com/invoices/june.pdf",
  idempotencyKey,
});