Webhooks & Monitoring
Stay in sync with Trustline through signed HMAC-SHA256 webhooks, request logs, and the portal's transaction views — with polling as the source of truth.
Trustline pushes signed event notifications to your HTTPS endpoint so your integration reacts the moment something needs attention, rather than waiting on a poll loop. Every delivery is HMAC-signed, retried on failure, and recorded attempt by attempt. Webhooks are a hint that something changed; the transaction resource itself remains the authoritative source of truth, and you should always confirm state by reading it.
This applies the same way to both audiences. Institutions running agents use webhooks to surface a paused transaction to an operations queue; agent developers integrating the API use them to wake a handler that answers an Agentic Challenge. In both cases the notification is a prompt to go read the canonical state, not a replacement for it.
Event Types
Trustline delivers two event types today.
| Event type | When Trustline sends it |
|---|---|
underwriting.transaction.requires_information | An Agentic Challenge opened; the transaction is paused waiting for the agent's answer. The payload embeds a public-safe challenge summary. |
webhook.endpoint.test | You triggered a test delivery from the portal to confirm your endpoint is reachable and your signature verification is correct. |
Four further event types are reserved. An endpoint can subscribe to them now, but Trustline does not emit them yet:
underwriting.transaction.completedunderwriting.transaction.failedunderwriting.challenge.expiredunderwriting.challenge.reassessment_completed
Because these are not emitted, do not build your integration to wait on them for completion, failure, or challenge outcomes. Until they go live, poll the transaction to learn whether it reached a decision, failed, or how a challenge resolved. See Async Underwriting for the polling lifecycle and Agentic Challenge for challenge states.
Each endpoint subscribes to a subset of these types. Subscribing to any other value is rejected with invalid_webhook_event_types.
Signatures
Every webhook request carries three headers:
X-Trustline-Webhook-Id: wh_evt_... X-Trustline-Timestamp: 1780611000 X-Trustline-Signature: v1=<hex_hmac_sha256>
X-Trustline-Webhook-Idis the unique event id, identical to the payloadevent_id. Deduplicate deliveries on it.X-Trustline-Timestampis the delivery time in Unix seconds, used to bound the replay window.X-Trustline-Signaturecarries the version prefixv1=followed by a lowercase hex HMAC-SHA256 digest.
The signature base string is the timestamp and the raw request body joined by a dot — <timestamp>.<raw_body> — and the signature is HMAC-SHA256(signing_secret, base_string). Trustline serializes each payload as canonical JSON (sorted keys, compact separators) and signs those exact bytes, so verify against the raw bytes you received. Do not parse and re-serialize the JSON before verifying; any difference in key order or whitespace produces a different digest.
To verify a delivery:
- Read the raw request body before any JSON parsing.
- Reject the request if the
X-Trustline-Timestampis more than 300 seconds old or in the future. - Build the base string
<timestamp>.<raw_body>and compute HMAC-SHA256 with the endpoint's signing secret. - Compare against the value after the
v1=prefix using a constant-time comparison. - Deduplicate on
X-Trustline-Webhook-Idbefore applying any business action.
import hmac import hashlib import time def verify_signature( signing_secret: bytes, timestamp: str, raw_body: bytes, signature_header: str, tolerance_seconds: int = 300, ) -> bool: if abs(int(time.time()) - int(timestamp)) > tolerance_seconds: return False if not signature_header.startswith("v1="): return False payload = timestamp.encode("utf-8") + b"." + raw_body expected = hmac.new(signing_secret, payload, hashlib.sha256).hexdigest() provided = signature_header.removeprefix("v1=") return hmac.compare_digest(expected, provided)
Each endpoint has its own signing secret, scoped to the endpoint's environment — a sandbox endpoint's secret never signs production traffic, and vice versa. Owners and admins can rotate a secret from the portal at any time; rotation is immediate, and every delivery sent afterward, including retries and replays of older events, is signed with the current secret. Keep event-id deduplication independent of the signing secret so a replayed event never double-applies an action.
Delivery and Retries
Delivery is at-least-once. An event moves through pending → delivering → delivered, or into retrying and eventually dead_lettered if your endpoint keeps failing.
| State | Meaning |
|---|---|
pending | Queued, not yet attempted. |
delivering | A delivery attempt is in flight. |
delivered | Your endpoint returned a 2xx response. |
retrying | A delivery failed retryably and another attempt is scheduled. |
dead_lettered | Retries are exhausted or the failure is non-retryable; the event is parked. |
A delivery succeeds on any 2xx response, with a 5-second HTTP timeout. Timeouts, connection failures, 5xx responses, and the retryable statuses 408, 409, 425, and 429 are retried with exponential backoff — up to 3 attempts per event, 60 seconds after the first failure and 120 seconds after the second. Any other 4xx response dead-letters the event immediately, since a retry would fail the same way. Deliveries interrupted mid-flight are requeued automatically after a 300-second lease expires, which is another reason to deduplicate on X-Trustline-Webhook-Id.
The portal exposes recovery operations on the Webhooks page. Replay re-delivers a delivered or dead_lettered event with a fresh retry budget — useful after you fix a receiver bug. Retry immediately re-attempts an event currently in retrying. Dead-letter parks a pending or retrying event so it stops retrying. If you missed events entirely, for example during an outage, poll the affected transactions and challenges rather than relying on recovery.
Request Logs
The Request Logs page records every authenticated API call your organization makes, so you can debug an integration without handling sensitive data. Each entry stores request metadata and privacy-preserving hashes — never raw secrets or payloads.
| Field | Description |
|---|---|
method, path | The HTTP method and route called. |
status_code, error_code | Response status and, on failure, the machine-readable error code. |
latency_ms | Server-side processing time. |
correlation_id | The X-Correlation-Id header value you sent — match it against your own logs. |
trustline_transaction_id | The transaction the call created or touched, when applicable. |
idempotency_key_hash | Hash of the idempotency key you sent. |
request_hash, response_hash | Hashes of the request and response bodies for integrity checks. |
source_ip_hash, user_agent_hash | Hashed client network identifiers. |
Request logs never contain raw API keys, raw idempotency keys, raw request or response bodies, raw source IPs, or user-agent strings — only the hashes above. They are organization-scoped and environment-scoped, readable by any active organization role in the portal or through the portal API:
GET /api/v1/developer-portal/organizations/{developer_org_id}/request-logs GET /api/v1/developer-portal/organizations/{developer_org_id}/request-logs/{request_log_id}
The list endpoint filters by environment, api_key_id, method, path, correlation_id, idempotency_key_hash, trustline_transaction_id, status_code, error_code, latency range, and time window. The practical pattern is to send your own X-Correlation-Id with each request, then filter request logs by that correlation id — or by the transaction id — to connect a log to the transaction it created and compare status_code, error_code, and latency_ms against what your client observed.
Monitoring Model
No single surface tells the whole story. Combine three:
| Surface | What it gives you | How to treat it |
|---|---|---|
| Webhooks | A near-real-time nudge when a transaction needs information | A hint — verify before acting |
| Request Logs | Method, path, status, latency, and correlation ids for every API call | Debugging and reconciliation, secret-free |
| Transactions view | The full status, decision, and reason lineage of each transaction | The authoritative state |
Treat webhooks as hints and the transaction resource as the source of truth. When underwriting.transaction.requires_information arrives, verify its signature, deduplicate on the webhook id, then read the transaction — and respond to the challenge with POST /api/v1/underwriting/transactions/{trustline_transaction_id}/challenge-response. For completion, failure, and challenge outcomes, where the corresponding events are still reserved, poll the transaction directly. This polling-first posture keeps your integration correct today and unchanged when the reserved events begin emitting.
For the broader portal layout, see Developer Portal; for the evidence trail behind a decision, see Compliance & Audit.