Working with the API
Webhooks
A webhook tells your server that a job has ended, so you do not have to poll. Printy3D sends webhooks for Exact Check jobs.
Registering an endpoint#
/v1/webhook_endpointswebhooks:manageurlstringrequired9–2048 charactersAn https address on your server, on port 443 (the default) or a port from 1024 to 65535. Every address its name resolves to must be publicly routable, IPv4 and IPv6: private, loopback and link-local addresses are refused, and so is a URL that carries a user name or password.
curl -sS "$API/v1/webhook_endpoints" -H "Authorization: Bearer $PRINTY3D_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"url": "https://example.com/printy3d/webhooks"}'
# -> 201 {"id": "whe_...", "url": "...", "enabled": true, "livemode": false,
# "created_at": "...", "secret": "whsec_..."}- Keep the secret from the
201answer: listing never shows it. If you lose it, delete the endpoint and register a new one, which has a new secret. - Send an
Idempotency-Key. A retry with the same key and body returns the same endpoint and secret, withIdempotent-Replayed: true, instead of registering a second endpoint. Without a key, every request registers a new one. - An endpoint belongs to the mode of the key that registered it (
livemode). Test keys list, use and delete only test endpoints, and live keys only live ones, so a test job is never announced to your live endpoint. - At most 20 endpoints per mode. Your account can hold 20 live and 20 test endpoints. Registering another answers
409 conflict: delete one first. - A refused URL answers
422 validation_failedwith the reason indetail. A name that does not resolve and a name that resolves to a non-public address get the same sentence. The address is checked when you register and again at every send, and a send connects to the address it has just checked, so a name that later points inward is never reached.
/v1/webhook_endpointswebhooks:manageAll your endpoints of the key's mode in data, oldest first, without their secrets. The list is not paged.
/v1/webhook_endpoints/{endpoint_id}webhooks:manageRemoves the endpoint (204); an endpoint of the other mode, or not yours, answers 404. Deliveries still waiting for it are called off; one already being sent at that moment may still arrive.
Asking to be told#
Name the endpoint when you create the job, in notify.webhook_endpoint. The job is then announced to that endpoint when it ends. Ad-hoc URLs are not accepted, and only cad_check jobs take notify.
{
"input_file_id": "file_...",
"recipe": "cad_check",
"notify": { "webhook_endpoint": "whe_..." }
}An id that names no active endpoint of yours in the same mode as the job answers 422 validation_failed.
What is sent#
One delivery per job, when it ends: a POST with a JSON body of exactly three fields and nothing else.
| event | When |
|---|---|
job.succeeded | The job succeeded; its result and evaluation are ready. |
job.failed | The job failed; error_code on the job says why. |
job.cancelled | The job was cancelled. |
{"event":"job.succeeded","job_id":"job_...","status":"succeeded"}The body carries no file names, results or other customer data. Fetch the job (GET /v1/jobs/{job_id}) for everything else, with your API key.
Verifying the signature#
Each request carries a Printy3D-Signature header:
Printy3D-Signature: t=1762790400,v1=5f0c…tis when it was signed, in Unix seconds.v1is the lowercase hex HMAC-SHA256 of<t>.<raw body>, keyed with the endpoint's whole secret,whsec_included.
Compute it over the raw bytes you received, before any JSON parsing, compare in constant time, and reject a request whose t is more than 300 seconds from your clock. Then check that the job_id is one of yours.
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance_s: int = 300) -> bool:
"""True when Printy3D-Signature matches the raw body and is recent enough."""
fields = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
t, v1 = fields.get("t", ""), fields.get("v1", "")
if not t.isdigit() or not v1 or abs(time.time() - int(t)) > tolerance_s:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Delivery and retries#
- Answer with any
2xxto accept the delivery. - Each request times out after 10 seconds and follows no redirects: a
3xxcounts as a failure. Answer first, then do the work. - A failed attempt is retried, up to 5 attempts in all, waiting 1 minute, 5 minutes, 30 minutes and 2 hours between them. After the fifth the delivery is given up; it is never attempted a sixth time. An attempt cut off on our side is tried again after 5 minutes and counts as one of the five.
- When your name resolves to several public addresses and the first cannot be reached, the same attempt tries the next one, with the same
Hostand TLS name. - If the name now resolves to an address that is not public, the delivery fails at once and is not retried. A name that does not resolve at that moment is an ordinary failed attempt.
- A delivery can arrive more than once. Treat
job_idpluseventas the key, and ignore a repeat.
A delivery that never arrives does not change the job: you can always poll it.
Test keys#
A cad_check job made with a test key can name an endpoint registered with a test key, and is announced to it with the same three fields. Register one endpoint with each key if you want both.
Not offered#
The API does not offer: rotating an endpoint's secret, a test ping, listing or retrying deliveries, events other than the three above, or webhooks for recipes other than cad_check.