Printy3D

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#

urlstringrequired9–2048 characters

An 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 201 answer: 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, with Idempotent-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_failed with the reason in detail. 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.

All your endpoints of the key's mode in data, oldest first, without their secrets. The list is not paged.

Removes 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.

POST /v1/jobs
{
  "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.

eventWhen
job.succeededThe job succeeded; its result and evaluation are ready.
job.failedThe job failed; error_code on the job says why.
job.cancelledThe job was cancelled.
Request body
{"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:

text
Printy3D-Signature: t=1762790400,v1=5f0c…
  • t is when it was signed, in Unix seconds.
  • v1 is 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 2xx to accept the delivery.
  • Each request times out after 10 seconds and follows no redirects: a 3xx counts 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 Host and 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_id plus event as 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.