Skip to content

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
```bash
curl https://api.w24.co/techread/read-with-callback \
  -H "Authorization: Token $WERK24_TOKEN" \
  -F "drawing=@drawing.png" \
  -F 'asks=[{"ask_version": "v2", "ask_type": "META_DATA"}]' \
  -F 'callback_url="https://example.com/werk24-callback?job=4711"' \
  -F 'callback_headers={"X-Token": "my-shared-secret"}' \
  -F 'max_pages=5'
```

=== "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
```csharp
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Token", Environment.GetEnvironmentVariable("WERK24_TOKEN"));

using var form = new MultipartFormDataContent();
var drawing = new ByteArrayContent(File.ReadAllBytes("drawing.png"));
drawing.Headers.ContentType = new MediaTypeHeaderValue("image/png");
form.Add(drawing, "drawing", "drawing.png");

// Every field except the drawing is a JSON value.
form.Add(new StringContent(JsonSerializer.Serialize(new[] {
    new { ask_version = "v2", ask_type = "META_DATA" } })), "asks");
form.Add(new StringContent(JsonSerializer.Serialize("https://example.com/werk24-callback?job=4711")), "callback_url");
form.Add(new StringContent(JsonSerializer.Serialize(new Dictionary<string, string> {
    ["X-Token"] = "my-shared-secret" })), "callback_headers");
form.Add(new StringContent("5"), "max_pages");

var response = await http.PostAsync("https://api.w24.co/techread/read-with-callback", form);
var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
    throw new Exception($"Submission refused: {(int)response.StatusCode} {body}");
var requestId = JsonDocument.Parse(body).RootElement.GetProperty("request_id").GetString();
Console.WriteLine($"Registered request {requestId}");
```

=== "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
```java
// Java 11+, no dependencies. java.net.http has no multipart builder,
// so the body is assembled by hand.
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.io.ByteArrayOutputStream;
import java.util.UUID;

public class Submit {
    public static void main(String[] args) throws Exception {
        String boundary = "werk24-" + UUID.randomUUID();
        ByteArrayOutputStream body = new ByteArrayOutputStream();

        field(body, boundary, "asks", "[{\"ask_version\": \"v2\", \"ask_type\": \"META_DATA\"}]");
        field(body, boundary, "callback_url", "\"https://example.com/werk24-callback?job=4711\"");
        field(body, boundary, "callback_headers", "{\"X-Token\": \"my-shared-secret\"}");
        field(body, boundary, "max_pages", "5");

        body.write(("--" + boundary + "\r\n"
            + "Content-Disposition: form-data; name=\"drawing\"; filename=\"drawing.png\"\r\n"
            + "Content-Type: image/png\r\n\r\n").getBytes(StandardCharsets.UTF_8));
        body.write(Files.readAllBytes(Path.of("drawing.png")));
        body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.w24.co/techread/read-with-callback"))
            .header("Authorization", "Token " + System.getenv("WERK24_TOKEN"))
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }

    static void field(ByteArrayOutputStream out, String boundary, String name, String json) throws Exception {
        out.write(("--" + boundary + "\r\n"
            + "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"
            + json + "\r\n").getBytes(StandardCharsets.UTF_8));
    }
}
```

=== "Node.js"

1
See the [Webhooks](webhooks.md#nodejs-equivalent) page.

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:

{"request_id": "0a4b3c2d-1e2f-4a5b-8c7d-9e0f1a2b3c4d"}

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:

  1. PROGRESS / STARTED once, when the read begins.
  2. ASK / <ask type> for every ask you sent. Some asks answer once per page (page_number tells which). Up to 8 of these can be in flight at once, so they arrive in any order and can overlap.
  3. PROGRESS / COMPLETED once, 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):

1
2
3
4
5
POST /werk24-callback?job=4711 HTTP/1.1
Content-Type: application/json
User-Agent: Werk24-Callback-Worker
X-Werk24-Delivery-Attempt: 1
X-Token: my-shared-secret

=== "STARTED"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
```json
{
  "exceptions": [],
  "request_id": "0a4b3c2d-1e2f-4a5b-8c7d-9e0f1a2b3c4d",
  "message_type": "PROGRESS",
  "message_subtype": "STARTED",
  "page_number": -1,
  "payload_dict": null,
  "payload_url": null,
  "payload_bytes": null
}
```

=== "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
```json
{
  "exceptions": [],
  "request_id": "0a4b3c2d-1e2f-4a5b-8c7d-9e0f1a2b3c4d",
  "message_type": "ASK",
  "message_subtype": "META_DATA",
  "page_number": 0,
  "payload_dict": {
    "ask_version": "v2",
    "ask_type": "META_DATA",
    "page_type": "COMPONENT_DRAWING",
    "bill_of_material": null,
    "certifications": [],
    "designation": [
      {"reference_id": 12345, "language": "ENG", "value": "SHAFT"}
    ],
    "identifiers": [],
    "general_roughness": null,
    "general_tolerances": null,
    "languages": ["ENG"],
    "material_options": [],
    "notes": [],
    "projection_method": null,
    "unit_systems": [],
    "weight": {"reference_id": 12346, "value": "1.25", "unit": "kg"}
  },
  "payload_url": null,
  "payload_bytes": null
}
```

=== "COMPLETED"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
```json
{
  "exceptions": [],
  "request_id": "0a4b3c2d-1e2f-4a5b-8c7d-9e0f1a2b3c4d",
  "message_type": "PROGRESS",
  "message_subtype": "COMPLETED",
  "page_number": -1,
  "payload_dict": null,
  "payload_url": null,
  "payload_bytes": null
}
```

=== "COMPLETED, read incomplete"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
```json
{
  "exceptions": [
    {"exception_level": "WARNING", "exception_type": "READ_INCOMPLETE"}
  ],
  "request_id": "0a4b3c2d-1e2f-4a5b-8c7d-9e0f1a2b3c4d",
  "message_type": "PROGRESS",
  "message_subtype": "COMPLETED",
  "page_number": -1,
  "payload_dict": null,
  "payload_url": null,
  "payload_bytes": null
}
```

A `WARNING` does not make the read fail: everything you received stands. See [Incomplete reads](../error-handling/index.md#incomplete-reads).

Requests that leave `client_version` out, as direct HTTP callers do, are to receive this warning too, but not yet: today it goes only to requests reporting werk24 2.8.0 or newer. The [API Changelog](changelog.md) will say when that changes. Accept it anyway, so your receiver keeps working when it starts arriving.

=== "ASK that failed"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
```json
{
  "exceptions": [
    {"exception_level": "ERROR", "exception_type": "DRAWING_CONTENT_NOT_UNDERSTOOD"}
  ],
  "request_id": "0a4b3c2d-1e2f-4a5b-8c7d-9e0f1a2b3c4d",
  "message_type": "ASK",
  "message_subtype": "META_DATA",
  "page_number": 0,
  "payload_dict": null,
  "payload_url": null,
  "payload_bytes": null
}
```

An ask that could not be answered still gets its `ASK` message, with an `ERROR` in `exceptions`. See [Error Handling](../error-handling/index.md).

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, 503 or 504. A Retry-After of 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