Skip to main content

Webhooks

A webhook is a signed HTTPS request Mathpix sends to the callback_url on your submission when that document or batch reaches its final state. A webhook replaces polling for status with being told.

The notification carries the same state you would read by polling. Polling keeps working exactly as today, and polling remains the authoritative record: if a notification is ever missed, nothing is lost, and the status endpoints still answer.

info

This is a separate mechanism from the callback object on v3/text, v3/latex, and v3/batch. Those endpoints keep working exactly as documented today.

EndpointDescription
GET /files/v1/webhook-configYour signing secret
POST /files/v1/webhook-config/testSend a test notification to a URL you name

Two sections describe the requests Mathpix makes to you:

SectionDescription
Callback parameters at submissioncallback_url, callback_headers, callback_events on the four submission endpoints
The notificationThe request body, the signature, and the delivery guarantees

GET /files/v1/webhook-config

GET api.mathpix.com/files/v1/webhook-config

Returns your signing secret, creating it on the first call. The secret belongs to the application the calling app_key authenticates, so every key on that application returns the same secret, and creating another key does not produce a new one. The secret is your only stored webhook setting: where a notification goes, what headers it carries, and which events fire are all parameters on the submission.

signing_secret is sensitive information, stored like a password. It is the key notifications are signed with, and the same key your handler verifies them with.

Example

curl https://api.mathpix.com/files/v1/webhook-config \
-H 'app_key: APP_KEY'
Example response
{
"signing_secret": "whsec_REDACTED"
}

Response body

signing_secret string

The key you verify notification signatures with. Created on your first call to this endpoint.

Errors

CodeHTTPWhen it fires
unauthorized401The request carries no app_key header (Missing app_key), or an app_key that does not resolve to an account (Invalid app_key).

POST /files/v1/webhook-config/test

POST api.mathpix.com/files/v1/webhook-config/test

Sends one signed sample notification to the callback_url in the request body, and answers synchronously with the outcome. You see delivered, or exactly what your endpoint answered instead, in one round trip, before any real document is involved.

Because the target is named per call, you can verify an endpoint before any submission points at it, and verify a second endpoint without disturbing the first.

The test is a single attempt with no retries: you are present to read the answer, fix your handler, and call again. Up to 10 test notifications per minute can be sent per app_key; beyond that, the call answers a rate limit error until the next minute begins.

The sample notification is a file.completed body carrying the reserved diagnostic identifier 00000000-0000-0000-0000-000000000000. That identifier is never a real document, so your handler can recognize test notifications structurally and keep them out of business logic.

Request parameters

callback_url string

The endpoint to send the test notification to. The same rules as callback_url at submission: HTTPS only, and URLs that resolve to private or internal networks are rejected.

callback_headers object (optional)

Headers to send with the test, so you can verify your own authentication too. The same rules as callback_headers at submission.

Example

{
"callback_url": "https://your-app.example.com/mathpix/webhook",
"callback_headers": { "Authorization": "Bearer <a token you issued>" }
}
Example response
{
"status": "delivered",
"response_code": 200,
"detail": null
}

Response body

status string

delivered when your endpoint accepted the notification, failed otherwise. A test is one attempt, so delivering never appears here.

response_code integer

The HTTP status your endpoint answered, or null when no answer arrived.

detail string

A plain sentence saying what went wrong, or null when the notification was delivered.

An endpoint that refuses the test is not an error in this call. The request succeeded, and the answer describes what your endpoint did, so an endpoint that never answered reads like this:

Example response, no answer from the endpoint
{
"status": "failed",
"response_code": null,
"detail": "no response was received: Connection refused"
}

Errors

CodeHTTPWhen it fires
unauthorized401The request carries no app_key header (Missing app_key), or an app_key that does not resolve to an account (Invalid app_key).
bad_request400The request has no body (Missing request body), names no callback_url (callback_url is required: the endpoint to send the test notification to), or gives one that is not a string (callback_url must be a string).
bad_request400callback_headers is not an object of string values (callback_headers must be an object of string values), carries more than ten headers (callback_headers: at most 10 headers are allowed), exceeds the total size cap (callback_headers: headers exceed the total size cap of 4096 bytes), names a header Mathpix sets (callback_headers: header name is reserved: Mathpix-Signature), or names one that is not a valid HTTP token (callback_headers: header name is not a valid HTTP token: bad header).

Callback parameters at submission

All four submission endpoints accept the same optional fields in their existing request body:

  • POST /v3/pdf
  • POST /files/v1
  • POST /files/v1/uri
  • POST /files/v1/jobs
callback_url string (optional)

HTTPS URL Mathpix sends this submission's notifications to. URLs that resolve to private or internal networks are rejected with a bad request response at submission time. A submission without one is not notified at all.

callback_headers object (optional)

Sent on this submission's notifications, so you can authenticate the request on your side. Belongs with callback_url: set both together, because a submission that sets callback_url and no callback_headers is notified with no headers. At most 10 headers, 4 KB in total. Names must be valid HTTP header tokens (custom names like X-My-Auth are fine), but these are reserved and rejected with a 400: Host, Content-Type, Content-Length, Transfer-Encoding, Connection, User-Agent, and any Mathpix- name (such as Mathpix-Signature).

callback_events string[] (optional)

The events to receive for this submission. An empty array turns notifications off for this one submission, keeping its callback_url in place.

Errors

A callback parameter the submission endpoint refuses fails the whole submission, and no document is created, so the identifier the error body echoes has nothing behind it.

CodeHTTPWhen it fires
bad_request400callback_headers or callback_events without callback_url (callback_headers requires callback_url: without it this submission sends no notifications).
bad_request400callback_url is empty, is not a string, or is not https (callback_url must be a non-empty https URL), or resolves to a private or internal address (callback_url rejected: <reason>).
bad_request400callback_headers is not an object of string values (callback_headers must be a JSON object of string values), or one of its names is reserved or malformed (callback_headers rejected: header name is reserved: Mathpix-Signature).
bad_request400callback_events names an event that does not exist (callback_events allows only: file.error, file.completed, job.completed).

Events

EventFiresDefault
file.completedPer document, once it and all requested conversions finishOn for single-file submissions; opt-in for batches
file.errorPer document that failsOn for single-file submissions; opt-in for batches
job.completedOnce, after you finalize a batch and every document (with its conversions) has finishedOn for /files/v1/jobs batches

Which events a submission receives is resolved in two steps:

  1. The submission's own callback_events, when present, decides alone, on every submission endpoint, including the empty array as the off-switch.
  2. Otherwise the submission's shape decides: a batch receives job.completed only, and a single-file submission receives both per-document events.

Set callback_events on the submission whenever you want anything other than the default for its shape.

file.completed fires only once the document's OCR result and every conversion format the submission requested have settled, so a requested docx or md is downloadable the moment the notification arrives. A conversion that fails does not turn the event into file.error: the OCR result is there, so file.completed still fires, and the failed format's status is visible in formats. file.error is reserved for the document itself failing.

job.completed requires finalizing the job

A job is an open container: job_id is yours to choose, so any later request naming the same job appends to it. Mathpix therefore cannot tell a batch that has finished from one whose next request has not arrived, and will not guess. It sends job.completed only once you have said the batch is complete, by calling POST /files/v1/jobs/{job_id}/finalize.

A job you never finalize never sends job.completed. Its documents' file.completed and file.error notifications are unaffected, and GET /files/v1/jobs/{job_id} reports its state as always.

Finalize whenever you are done submitting, before or after the documents finish. Finalizing a batch whose documents are all already done sends the notification in one to two minutes; finalizing one still processing sends it one to two minutes after the last document finishes. A job has to be quiet for a minute before the notification is sent, and a check runs every minute, so the wait is one minute plus up to one more.

One notification per job

A batch's job.completed fires once for the job. Finalizing is one-way, so there is no second one to send; a job that needs more files is a new job.

A batch's job.completed and its documents' file.completed notifications have no guaranteed order. In practice the job notification arrives last: each document's own notification is sent as that document finishes, and the batch's one to two minutes after the final one.

The three single-file endpoints (POST /v3/pdf, POST /files/v1, and POST /files/v1/uri) have exactly one document and therefore one event, file.completed or file.error, delivered at least once, so your handler can still receive it twice. You can disable notifications for that document by adding a request-level callback_events: [].

Subscribing to file.completed on a large batch means one notification per document, delivered at our processing pace. Before doing that, read receiving at volume.

The notification

A notification is an HTTPS POST request to your webhook URL:

  • The request carries Content-Type: application/json, the Mathpix-Signature header, User-Agent: Mathpix-Webhook/1, and any callback_headers you configured. The user agent is stable, so you can allowlist or filter on it; its version rises only if what we send changes.
  • Any answer with an HTTP status from 200 through 299 counts as delivered.
  • Your endpoint has 10 seconds to answer; a slower answer counts as no answer and the attempt is retried.
  • Redirects are never followed: an answer that redirects counts as a rejected notification, the same as a client error.

The body carries the same state polling would return:

  • status is completed or error, with the same meaning as the status endpoints.
  • Error details use the same error and error_info shape as every other error in this API.
  • A field that does not apply is omitted, never sent as null: read fields by name and treat an absent one as not applicable.

A notification identifies the document and reports its outcome. It does not carry the document's content or a link to it: fetch the result the same way you do when polling.

file.completed, result held by Mathpix
{
"event": "file.completed",
"file_id": "b1c9c3a8-55e4-4a09-b7d0-218ba5de4c4d",
"custom_id": "invoice-001",
"job_id": "contracts-2026-05",
"status": "completed",
"num_pages": 30,
"num_pages_completed": 30,
"formats": {
"docx": "completed",
"md": "completed"
}
}

The formats map appears because that submission requested docx and md. A submission that requested no conversion format has no such map in its notification.

When you submitted the document with a destination_uri, the notification echoes where the result was written, including the basename after any server-side defaulting, which you cannot derive:

file.completed, result written to your storage
{
"event": "file.completed",
"file_id": "b1c9c3a8-55e4-4a09-b7d0-218ba5de4c4d",
"status": "completed",
"destination_uri": "s3://my-bucket/out/",
"destination_basename": "invoice-001",
"num_pages": 30,
"num_pages_completed": 30
}

file.error carries the same identifiers with status set to error, plus the error and error_info pair used everywhere else in this API:

file.error
{
"event": "file.error",
"file_id": "b1c9c3a8-55e4-4a09-b7d0-218ba5de4c4d",
"status": "error",
"error": "content_too_large",
"error_info": { "id": "content_too_large", "message": "Document exceeds the page limit for this endpoint." },
"num_pages": 30,
"num_pages_completed": 12
}

A batch's job.completed reports the job rather than one document, with the same counts GET /files/v1/jobs/{job_id} returns:

job.completed
{
"event": "job.completed",
"job_id": "contracts-2026-05",
"status": "completed",
"file_count": 500,
"files_completed": 498,
"files_errored": 2,
"num_pages_sent": 14500,
"num_pages_completed": 14440
}

status is completed for a finished batch even when some of its documents failed: files_errored is where that shows. There is no file_id: to see which documents failed, list the job's files with status=error.

Notification fields

event string

Which event this notification carries: file.completed, file.error, or job.completed. Always present; read this first.

file_id string

The document the notification is about. Present on file.completed and file.error; a job.completed notification has no single document and carries job_id instead.

custom_id string (optional)

Your identifier for the document, echoed when the submission carried one. File events only.

job_id string (optional)

The batch the document belongs to. On file events, present when the document was submitted as part of a batch; on job.completed, always present.

status string

completed or error on file events, with the same meaning as the status endpoints. On job.completed, always completed: the batch finished, and the two count fields carry the outcome.

destination_uri string (optional)

Where the result was written, echoed on file.completed when the submission carried a destination_uri.

destination_basename string (optional)

The output basename at the destination, after any server-side defaulting, which you cannot derive. Present together with destination_uri.

error string (optional)

The error code, on file.error only. The same code the status endpoints return.

error_info ErrorInfo (optional)

The error's id and message pair, on file.error only. The same shape as every other error in this API.

num_pages integer

The document's page count, on file events. 0 when the document failed before we could read it, so a file.error with num_pages: 0 never started processing.

num_pages_completed integer

How many of the document's pages finished, on file events; on job.completed, how many pages finished across the whole batch. On file.error this is how far processing got before the failure.

formats object (optional)

Each requested conversion format's own outcome, on file.completed and only when the submission requested conversions. An object keyed by format, each value "completed" or "error", for example { "docx": "completed", "md": "error" }. Identical to the formats object GET /files/v1/{file_id} returns, so one handler reads both. A failed format does not make the event file.error; the document's OCR result is still ready.

file_count integer

How many documents the batch contains. job.completed only, as are the counts below.

files_completed integer

How many documents finished successfully.

files_errored integer

How many documents failed. Above zero, list the failures through the job's file listing with status=error.

num_pages_sent integer

How many pages the batch submitted for processing. Pages are counted once a document is split, so a batch whose documents all failed before that reports 0 here against a non-zero files_errored.

Fetching the result requires your API key, exactly like polling: use the notification's file_id with the status and result endpoints. The notification by itself grants no access to the document.

The signature

Every notification carries a Mathpix-Signature header:

Mathpix-Signature: t=1721760000,v1=REDACTED

To verify the signature: compute a hash-based message authentication code (HMAC) with the SHA-256 hash function over the timestamp, a period, and the raw request body, using your signing secret as the key. Compare your result to the v1 value with a constant-time comparison, and reject any notification whose t is more than five minutes old. Complete, runnable verification code in Python and JavaScript is in the webhooks guide.

Compute the HMAC over the raw request bytes, before any JSON parsing. Re-encoding the body can reorder keys or change whitespace, and the signature will not match.

The age check reads your own clock

The t value is compared against the clock on the machine running your handler, so that machine needs a synchronized clock, from the network time protocol or whatever your platform provides. A clock more than five minutes away from real time rejects every notification, in either direction, and if your handler answers a client error when it rejects one, we treat your endpoint as misconfigured and stop retrying that notification. Nothing is lost, because the result is still there to poll, but no notification arrives until the clock is corrected.

Delivery, retries, and what we do not promise

What we promise:

  • A notification is sent only after the state it reports is true. Completed means completed: the results are there when the notification arrives.
  • At least one delivery attempt, and retries for about an hour. If your endpoint answers with a server error or does not answer in time, we retry with growing delays: 8 attempts in total, approximately 30 seconds after the first failure, then 1, 2, 4, 8, 16, and 32 minutes.
  • Your endpoint can pace retries. An answer of HTTP status 429 (too many requests) is retried like a server error, and a numeric Retry-After header on that answer sets the wait before the next attempt: never less than 1 second, and never more than the 32-minute longest delay of the retry schedule. A Retry-After value in date form is ignored.
  • Every notification is verifiable. The signature proves the notification came from Mathpix.

What we deliberately do not promise, and what to do about each:

  • Exactly one delivery. You may receive the same notification twice. Deduplicate on event plus file_id (event plus job_id for job.completed): if you have already processed that pair, acknowledge and skip. Each delivery reflects the document's state at the moment that delivery was sent, exactly as polling at that moment would; keeping whichever copy you process first is safe.
  • Delivery forever. If your endpoint stays unreachable past the retry window, that notification is never sent again. The result still exists: poll the status endpoint and fetch the result as usual. A missed notification never loses a result.
  • Order. Two documents from one batch may notify in any order.
  • Retries after your endpoint rejects a notification. If your endpoint answers with a client error, for example 404 not found or 401 unauthorized, we treat your URL as misconfigured for that notification and stop retrying; HTTP status 429 is the one client error that is retried. Fix the endpoint, then rely on polling for anything missed in the meantime.

Answer with a success status as soon as you have durably accepted the notification, and do your processing afterward. A slow answer counts as no answer and turns a success into a retry, which you will see as a duplicate.

Receiving at volume

Notifications arrive at our processing pace, not yours. A batch of one hundred thousand documents subscribed to file.completed is one hundred thousand requests to your endpoint within the processing window.

For batches at that scale and beyond:

  • Subscribe to job.completed plus file.error.
  • Have results written to your storage with destination_uri.
  • List per-file outcomes through the job's file listing.

For the largest workloads, one notification per document does not fit; your account team can advise on queue-based delivery.

See also