Webhooks
Tento obsah zatiaľ nie je dostupný vo vašom jazyku.
When a case changes, TrustInk POSTs an event to the callback.url you gave in POST /cases. Webhooks are
the recommended way to learn that a case is complete; see real-time events for
live updates in a browser.
Events
Section titled “Events”type |
When |
|---|---|
participant-signed |
a participant signed; participantId names them |
case-completed |
everyone signed and the signed PDF is ready; signedDocument.url links it |
case-failed |
everyone signed, but the document could not be signed; the case is FAILED |
case-expired |
validUntil passed before everyone signed |
case-cancelled |
you cancelled the case |
Request
Section titled “Request”POST <callback.url>Content-Type: application/jsonTrustink-Event: case-completedTrustink-Delivery: 2f1c9a7e-5b0d-4c61-9d1e-7a3b8c2e4f10Trustink-Signature: t=1790175600,v1=5c3f…{ "id": "7b1e4c2a-…", "type": "case-completed", "occurredAt": "2026-09-23T15:40:00Z", "tenantId": "d47b9463-…", "caseId": "49d4ae62-…", "status": "DONE", "data": {"orderId": 4711}, "signedDocument": {"url": "https://…"}, "evidenceDocument": {"url": "https://…"}}idis the same on every attempt to deliver one event, so you can drop duplicates.Trustink-Deliveryis new on every attempt.statusis the case status when the event is delivered.datais thecallback.dataof the case, unchanged: put your own reference (an order number, a record id) there.participantIdcomes withparticipant-signed.signedDocument.urlcomes once the case isDONE; it works for an hour and downloads as<title>-signed.pdf.evidenceDocument.url, the sealed evidence summary of the signing, comes with it and downloads as<title>-evidence.pdf.
Setting it up
Section titled “Setting it up”The callback object of POST /cases:
{ "callback": { "url": "https://example.com/trustink/webhook", "authType": "NONE", "signingSecret": "replace-with-32-to-128-random-characters", "data": {"orderId": 4711} }}Authentication
Section titled “Authentication”callback.authType decides what the request carries, in addition to the signature:
authType |
Request |
|---|---|
NONE |
nothing |
BASIC |
Authorization: Basic base64(clientId:clientSecret) |
API_KEY |
<apiKeyHeader>: <apiKeyValue>, e.g. X-Api-Key: … |
OAUTH_CLIENT_CREDENTIALS |
Authorization: Bearer <access_token>, a token from tokenUrl |
For OAUTH_CLIENT_CREDENTIALS TrustInk POSTs grant_type=client_credentials (plus scope and audience
when the callback sets them) as application/x-www-form-urlencoded to tokenUrl, an https URL. The client
authenticates with HTTP Basic (client_secret_basic, the default), or with client_id and client_secret
in the form when tokenAuthMethod is post. The answer must carry access_token, and token_type Bearer
if any; expires_in defaults to an hour.
The token is cached until 60 seconds before it expires. When your endpoint answers 401, TrustInk fetches a new token and sends once more right away. A token endpoint that is down (5xx, 408, 429, no answer) counts as a delivery to retry; one that refuses the credentials fails the delivery.
Verifying the signature
Section titled “Verifying the signature”With callback.signingSecret set (32 to 128 characters), every request carries
Trustink-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(signingSecret, t + "." + raw body)>Verify it over the raw body, before parsing it, and reject old timestamps against replays. Compare in constant time.
const {createHmac, timingSafeEqual} = require('node:crypto');
function verify(secret, rawBody, header, toleranceSeconds = 300) { const parts = Object.fromEntries(header.split(',').map(part => part.split('=', 2))); const t = Number(parts.t); if (!parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); const given = Buffer.from(parts.v1, 'hex'); return given.length === expected.length && timingSafeEqual(given, expected);}With Express, keep the raw body for the check:
app.post('/trustink/webhook', express.raw({type: 'application/json'}), (req, res) => { const rawBody = req.body.toString('utf8'); if (!verify(process.env.TRUSTINK_WEBHOOK_SECRET, rawBody, req.get('Trustink-Signature') ?? '')) { return res.sendStatus(400); } const event = JSON.parse(rawBody); // … handle event.type, dropping ids you have seen res.sendStatus(204);});import java.nio.charset.StandardCharsets;import java.security.GeneralSecurityException;import java.security.MessageDigest;import java.time.Instant;import java.util.HexFormat;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;
public final class TrustInkWebhook {
public static boolean verify(String secret, String rawBody, String header, long toleranceSeconds) throws GeneralSecurityException { String t = null; String v1 = null; for (String part : header.split(",")) { String[] pair = part.split("=", 2); if (pair.length != 2) continue; if (pair[0].equals("t")) t = pair[1]; if (pair[0].equals("v1")) v1 = pair[1]; } if (t == null || v1 == null) return false; long timestamp; try { timestamp = Long.parseLong(t); } catch (NumberFormatException e) { return false; } if (Math.abs(Instant.now().getEpochSecond() - timestamp) > toleranceSeconds) return false;
Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] expected = mac.doFinal((t + "." + rawBody).getBytes(StandardCharsets.UTF_8)); byte[] given; try { given = HexFormat.of().parseHex(v1); } catch (IllegalArgumentException e) { return false; } return MessageDigest.isEqual(given, expected); }}Call it with the body as your framework received it, before any JSON binding, and a tolerance of 300
seconds. Java 17 or newer (HexFormat).
using System;using System.Security.Cryptography;using System.Text;
public static class TrustInkWebhook{ public static bool Verify(string secret, string rawBody, string header, long toleranceSeconds = 300) { string? t = null, v1 = null; foreach (var part in header.Split(',')) { var pair = part.Split('=', 2); if (pair.Length != 2) continue; if (pair[0] == "t") t = pair[1]; if (pair[0] == "v1") v1 = pair[1]; } if (t is null || v1 is null || !long.TryParse(t, out var timestamp)) return false; if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > toleranceSeconds) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{rawBody}")); byte[] given; try { given = Convert.FromHexString(v1); } catch (FormatException) { return false; } return CryptographicOperations.FixedTimeEquals(given, expected); }}In ASP.NET Core read the body with new StreamReader(Request.Body).ReadToEndAsync() before binding it.
.NET 5 or newer (Convert.FromHexString).
Test vector
Section titled “Test vector”Use it to check your implementation. With the clock set within 300 seconds of t, it verifies; change one
character of the body or the secret and it must not.
| Signing secret | whsec-trustink-docs-test-vector-2026 |
t |
1790178000 |
| Header | Trustink-Signature: t=1790178000,v1=388855130144b108ad958f6a1d73a518e24d1a449f0e602ef96b3b2715f848a7 |
{"id":"7b1e4c2a-5d3f-4e8a-9c21-5d6f7a8b9c0d","type":"case-completed","occurredAt":"2026-09-23T15:40:00Z","tenantId":"d47b9463-2c1e-4f7a-8b5d-6e9f0a1b2c3d","caseId":"49d4ae62-8e3b-4d1f-a6c7-0b2e4f6a8c1e","status":"DONE","data":{"orderId":4711},"signedDocument":{"url":"https://example.com/signed.pdf"},"evidenceDocument":{"url":"https://example.com/evidence.pdf"}}The signed message is t, a dot and the raw body; its HMAC-SHA256 with the secret, in lowercase hex, is
388855130144b108ad958f6a1d73a518e24d1a449f0e602ef96b3b2715f848a7. The samples on this page are run against this vector in the site’s
test suite.
Answers and retries
Section titled “Answers and retries”Your endpoint has 10 seconds to answer; redirects are not followed.
| Answer | Result |
|---|---|
| 2xx | delivered |
| 408, 429, 5xx, no answer, connection error | retried every 90 seconds, at most 8 attempts |
| any other status (e.g. 400, 401, 404, 3xx) | failed, not retried |
Events that fail all 8 attempts are recorded as dead. Every attempt is kept for 90 days with its status code, duration and error.