Skip to content
Documentation menu

API

Webhooks

Events of generated and failed documents and published templates, signed with HMAC-SHA256 and retried.

Webhooks tell your systems what happens in a workspace without polling: a document rendered through the API, a render that failed, a template version published. Prynt sends an HTTPS POST with a JSON body to your endpoint, signs it, and retries until your endpoint answers 2xx.

How webhooks work

  • Add an endpoint in Developers → Webhooks (permission webhooks.manage): an https:// URL on a public host, the events to receive, an optional description. Prynt refuses addresses of private networks and localhost.
  • Prynt shows the endpoint's signing secret (whsec_…) once. Store it with your endpoint's configuration; Rotate secret creates a new one when needed (the old one stops signing at once).
  • Every delivery is logged with its attempts, the HTTP status and an excerpt of your answer. Send test event sends a webhook.test event to check your endpoint.

Events

Webhook events
FieldTypeDescription
document.generatedeventA document was rendered through the public API (sync or async).
document.failedeventA render through the public API failed: errorCode and message tell why.
template.publishedeventA template version was published in the workspace.
webhook.testeventSent by Send test event; always delivered, whatever the endpoint's events.

Payload

Every event has the same envelope: id (the event id, a UUID), type, createdAt, workspaceId and the event's data.

document.generated
{  "id": "0199c2f2-0b6e-7f4c-a1d2-6c8e9b0a7f31",  "type": "document.generated",  "createdAt": "2026-10-08T09:12:04.051Z",  "workspaceId": "0199b8a1-0000-7000-8000-00000000abcd",  "data": {    "template": "INVOICE",    "version": 7,    "environment": "production",    "format": "pdf",    "pageCount": 2,    "outputBytes": 48213,    "fileName": "INVOICE_18425.pdf",    "runId": "0199c2f1-5a7e-7b31-9c4d-2f0a8e6b1d23",    "jobId": "0199c2f1-7c10-7d2e-8f61-1b9a4c3e5d77",    "documentUrl": "https://api.prynt.it/api/v1/documents/eyJhbGciOi…",    "documentExpiresAt": "2026-10-09T09:12:04.020Z",    "correlationId": "4bf92f3577b34da6a3ce929d0e0e4736",    "apiKey": { "id": "0199b9c0-…", "prefix": "prynt_live_k3x9a2mq", "name": "ERP production" }  }}

Data of each event

  • document.generated: template, version, environment, format, pageCount, outputBytes, fileName, runId, jobId, documentUrl (a short-lived signed link), documentExpiresAt, correlationId and the apiKey (id, prefix, name) that asked for it.
  • document.failed: template, version, environment, format, jobId, runId, errorCode, message, correlationId and the apiKey.
  • template.published: templateId, template, version, previousVersion, format, note and publishedBy.

Headers

A delivery
POST /prynt/webhooks HTTP/1.1Content-Type: application/jsonUser-Agent: Prynt-Webhooks/1.0Prynt-Event: document.generatedPrynt-Event-Id: 0199c2f2-0b6e-7f4c-a1d2-6c8e9b0a7f31Prynt-Delivery: 0199c2f2-0c11-7a05-b3e4-9d1f2a6c8e40Prynt-Signature: t=1760000000, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • Prynt-Event: the event type; Prynt-Event-Id: the event id (the same on every retry and redelivery); Prynt-Delivery: this delivery.
  • Prynt-Signature: t=<unix seconds>, v1=<signature>.

Verifying signatures

v1 is the hex HMAC-SHA256 of {t}.{raw body}, keyed with the UTF-8 bytes of the whole secret (including the whsec_ prefix). To verify a delivery:

  • Read the raw body exactly as received, before any JSON parsing (parsing and serializing again changes the bytes).
  • Compute the HMAC of t, a dot and the raw body, and compare it with v1 in constant time.
  • Refuse deliveries whose t is more than 5 minutes away from your clock: a captured request cannot be replayed later.
1using System.Security.Cryptography;2using System.Text;3 4static bool VerifyPryntSignature(string header, string rawBody, string secret, TimeSpan tolerance)5{6    // header: "t=1760000000, v1=5257a869…"7    string? t = null, v1 = null;8    foreach (var part in header.Split(',', StringSplitOptions.TrimEntries))9    {10        if (part.StartsWith("t=")) t = part[2..];11        else if (part.StartsWith("v1=")) v1 = part[3..];12    }13    if (t is null || v1 is null || !long.TryParse(t, out var seconds)) return false;14    var age = DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(seconds);15    if (age.Duration() > tolerance) return false;16 17    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); // the whole "whsec_…" string18    var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{rawBody}"));19    byte[] received;20    try { received = Convert.FromHexString(v1); } catch (FormatException) { return false; }21    return CryptographicOperations.FixedTimeEquals(expected, received);22}23 24// ASP.NET Core: read the raw body before any JSON binding.25// var rawBody = await new StreamReader(Request.Body).ReadToEndAsync();26// var ok = VerifyPryntSignature(Request.Headers["Prynt-Signature"]!, rawBody, secret, TimeSpan.FromMinutes(5));

Verify before trusting the content

Answer 400 to a delivery with a missing or wrong signature and do not process it. While you rotate the secret, accept both the old and the new one for a short time.

Retries and redelivery

A delivery succeeds when your endpoint answers 2xx within a few seconds. Otherwise (an error status, a timeout, a connection failure) Prynt tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours: up to 6 attempts. After the last one the delivery is abandoned.

  • The delivery log of each webhook shows the status (pending, succeeded, failed, abandoned), the attempt, the next attempt and your answer.
  • Redeliver sends an event again, as a new delivery with the same event id: useful once your endpoint is fixed.
  • Disabled webhooks receive nothing; redelivery and test events answer 409 WEBHOOK_DISABLED until you enable them.

Best practices

  • Answer 2xx quickly, then do the work asynchronously (a queue, a background job). Slow answers count as failures and are retried.
  • Be idempotent: the same event can arrive more than once (retries, redeliveries). Store the event id you processed and ignore repeats.
  • Events can arrive out of order: rely on createdAt and on the job or run ids, not on the arrival order.
  • Download documentUrl soon after the event: the link is short-lived. Read the job again (GET /v1/jobs/{jobId}) for a fresh link.