HTTP API (any language)
You do not need Python to read a drawing. The HTTP API takes one multipart/form-data POST and delivers the results to a URL you own, as a series of JSON POSTs. Anything that can send a form upload and receive a webhook works: curl, C#, Java, TypeScript, Go, PHP, or a low-code HTTP module.
This page takes you from an API token to a finished read. You need:
- a Werk24 API key: see Get an API key,
- an HTTPS URL that accepts POST requests. For a first try, a request bin (any service that shows you the requests it receives) is enough.
Sample drawing
Use the sample drawing the Python client tests with: DRAWING_SUCCESS.png. curl -L -o drawing.png https://github.com/W24-Service-GmbH/werk24-python/raw/main/werk24/assets/DRAWING_SUCCESS.png
1. Submit the drawing
POST https://api.w24.co/techread/read-with-callback, authenticated with Authorization: Token <your-token>.
=== "curl"
1 2 3 4 5 6 7 8 9 | |
=== "C#"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
=== "Java"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
=== "Node.js"
1 | |
The form fields
Every field except drawing is a JSON value in its own form part. A string is sent with its quotes ("https://..."), a number without (5).
| Field | Required | JSON type | What it is |
|---|---|---|---|
drawing | yes | file part | The drawing: PDF, PNG, JPEG or TIFF. Give the part a filename. |
asks | yes | array | What to extract, for example [{"ask_version": "v2", "ask_type": "META_DATA"}]. See Asks. |
callback_url | yes | string | Where the results are POSTed. Use HTTPS. Redirects are not followed. |
max_pages | yes | integer, at least 1 | How many pages of the document to read. |
callback_headers | no | object | Headers sent with every callback POST, for example a shared secret. Names must start with X- or be Authorization, at most 128 characters; values at most 4096. |
priority | no | string | PRIO1, PRIO2 or PRIO3, at most your account's tier. See Priority. |
client_version | no | string | Leave it out. The Python client uses it to report its own version. |
public_key | no | string | A PEM public key to encrypt binary results with. Used only for accounts with end-to-end encryption; otherwise it is ignored. |
The whole request is limited to about 4.6 MB, so larger drawings cannot go this route. See Drawing File Size Limit.
To tie the callbacks to a job of your own, put your id in the callback URL's query (?job=4711) or in an X- header in callback_headers. Both come back on every callback POST.
The response
A 200 means the read is queued and charged. The body carries the id every callback will carry:
A refusal carries a JSON body with the status as code, a message, and sometimes details and a request_id (quote it to support). A 400, 401, 403, 415 or 429 is not charged and receives no callbacks.
| Status | When | Example body | Retry? |
|---|---|---|---|
400 | A field is missing or invalid, or the body is not multipart. | {"code": "400", "message": "Missing required field", "details": {"missing_field": "'callback_url'"}} | No, fix the request. |
401 | No or unknown token. (Authorization without the Token prefix and a space is a 400.) | {"code": "401", "message": "Invalid or expired authentication token"} | No. |
403 | priority above your account's tier. | {"code": "403", "message": "Requested priority PRIO1 exceeds account tier PRIO2", "details": {"error": "PRIORITY_TOO_HIGH", "account_tier": "PRIO2", "requested_priority": "PRIO1"}, "request_id": "..."} | No. |
415 | The drawing is not a supported file format. | {"code": "415", "message": "Unsupported file format. Please provide PDF, PNG, JPEG, or TIFF files.", "details": {"supported_formats": [...], "provided_format": "..."}, "request_id": "..."} | No. |
429 | The account's request allowance is used up. | {"code": "429", "message": "...", "details": {"error": "QUOTA_EXHAUSTED", "message": "...", "limit": 100}} | No. It does not reset with time; contact your Werk24 account team. |
500 | A fault on our side. | {"code": "500", "message": "An internal error occurred. Please try again later.", "details": {"message": "The error has been logged and will be investigated."}, "request_id": "..."} | Yes, after a pause, unless callbacks arrive for the request_id in the body: rarely the read was queued anyway, and then it runs and is charged. A read that was not queued is not charged. |
A request over the size limit is refused before it reaches Werk24, with no JSON body of ours and no request_id. See HTTP Error Codes for more.
2. What arrives at your callback URL
One read arrives as several POSTs, each a JSON TechreadMessage:
PROGRESS/STARTEDonce, when the read begins.ASK/<ask type>for every ask you sent. Some asks answer once per page (page_numbertells which). Up to 8 of these can be in flight at once, so they arrive in any order and can overlap.PROGRESS/COMPLETEDonce, last, after every earlier delivery has finished.
If a read fails on our side, an ERROR / INTERNAL message comes before COMPLETED, and the asks still waiting get an ASK message with an ERROR in exceptions.
The read is done when COMPLETED arrives, not before. A receiver that acts on the first POST acts on STARTED, which carries no data. Collect the ASK messages under their request_id and act on the set when COMPLETED comes.
The examples below are illustrative bodies of a read with one META_DATA ask; the values of a real read differ. Every POST carries the first three headers below, plus the callback_headers you sent (here X-Token):
=== "STARTED"
1 2 3 4 5 6 7 8 9 10 11 12 | |
=== "ASK"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
=== "COMPLETED"
1 2 3 4 5 6 7 8 9 10 11 12 | |
=== "COMPLETED, read incomplete"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
=== "ASK that failed"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
The message fields
| Field | What it is |
|---|---|
request_id | The id from the 200 response. The same on every message of one read. |
message_type | PROGRESS, ASK or ERROR. |
message_subtype | STARTED or COMPLETED for PROGRESS; the ask type (META_DATA, FEATURES, ...) for ASK. |
page_number | The page an ASK result is for, from 0. -1 on messages about the whole document. |
payload_dict | The result of the ask, shaped by its response model (see each ask page). null when there is none. |
payload_url | A download link for a binary result such as a sheet image or a redacted PDF. See below. |
payload_bytes | Always null on the wire. The Python client fills it after downloading payload_url. |
exceptions | What went wrong, if anything. Only exception_level: "ERROR" means the result is missing. |
Decimal values (lengths, tolerances, weights) are JSON strings, such as "1.25" or "Infinity" for a plane radius, so no precision is lost. Parse them as decimals, not floats. New fields and new enum values are added over time: ignore the ones you do not know. See the compatibility policy and the API Changelog.
3. Delivery rules
Your endpoint must answer every POST with a 2xx within 10 seconds. Do the work after answering, not before.
Delivery is at least once. Each POST is tried up to 3 times within about 20 seconds, when:
- the connection could not be opened, or was closed before an answer (a TLS error is not retried),
- your endpoint did not answer within 10 seconds,
- your endpoint answered
408,425,429,500,502,503or504. ARetry-Afterof up to 5 seconds is honoured; a longer one is not retried.
The budget is shared: after a 10-second timeout there is room for one retry, not two.
Every attempt carries X-Werk24-Delivery-Attempt: 1 for the first, 2 and 3 for retries. A retry after a timeout can reach a receiver that did process the first attempt, so the same message can arrive twice, with an identical body. Deduplicate on the body (a hash of it is enough), not on request_id and message_subtype alone: some asks, such as view images and redaction, send several messages with the same page_number.
What stops the read. Any other answer (a 3xx redirect, 400, 401, 403, 404 and the like), a TLS error, or a message whose attempts all failed ends the read: no further ASK messages are sent, the owner of the token gets an email, and the read stays charged. A final COMPLETED is still attempted, so a COMPLETED after a delivery you refused does not mean you hold every result. A receiver that answers 401 during development, because a secret did not match, ends every read it receives. Test your endpoint with a request bin or a stub that always answers 200 first.
The callback URL has to resolve to a public address. Plain http is still accepted for now but is to be refused: use HTTPS.
4. Binary results (payload_url)
Asks that return a file (sheet and view images, a redacted drawing) send it as a payload_url: a presigned HTTPS link, fetched with a plain GET and no Authorization header.
Download it within 10 minutes of receiving the message. The link is signed for an hour, but it can stop working after about ten minutes. If you queue downloads behind your webhook, keep that queue short.
If your account has end-to-end encryption and you sent a public_key, the downloaded bytes are encrypted to it.
A binary result's payload_dict is either a response model (a redaction) or a small object that says which sheet or view the file shows, such as {"sheet_id": "...", "sectional_id": "..."} for a view image.
5. Receive the callbacks
A receiver needs to do three things: check your secret, answer 200 at once, and act when COMPLETED arrives. The Webhooks page has a Node.js receiver; the same logic in any web framework works.
Next steps
- Asks: what else you can extract.
- Webhooks: the same route, with the Python client.
- OpenAPI and JSON Schemas: the machine-readable contract, for generating models.