Outgoing Webhooks
Register and manage HTTPS webhook endpoints over the authenticated JSON:API, GraphQL, and MCP surfaces to receive item event callbacks, with optional bearer-token auth, delivery tracking, and private-IP SSRF safeguards.
Overview
Outgoing webhooks let you receive an HTTPS POST callback whenever an event happens on your items. You register a webhook endpoint (a target URL plus the event types you care about), and Truestamp delivers a JSON payload to that URL each time a matching event fires. Endpoints are owned by the individual account that creates them, so a webhook receives events for the items that account submitted, optionally narrowed to a single team.
Two things are separate here: the endpoint (your standing subscription) and the delivery (one recorded attempt to POST an event to your endpoint). You manage endpoints through the JSON:API, GraphQL, and MCP surfaces, and you can read the delivery history to see what was sent and whether it succeeded. All three require authentication. Deliveries can optionally carry a bearer token so your receiver can verify the request comes from your endpoint, and every target URL is screened so a webhook can never be used to reach private or reserved network addresses.
Managing endpoints over the authenticated API
Endpoint management is exposed over JSON:API (under /api/json/webhook-endpoints) and GraphQL (under /gql), both requiring an API key or an OAuth 2.1 bearer token in the Authorization: Bearer header. There is no unauthenticated way to create, read, update, or delete endpoints, and each account can see and modify only its own endpoints.
The JSON:API surface offers standard CRUD on endpoints:
| Operation | Method and path |
|---|---|
| List your endpoints | GET /api/json/webhook-endpoints |
| Create an endpoint | POST /api/json/webhook-endpoints |
| Get one endpoint | GET /api/json/webhook-endpoints/:id |
| Update an endpoint | PATCH /api/json/webhook-endpoints/:id |
| Delete an endpoint | DELETE /api/json/webhook-endpoints/:id |
GraphQL exposes the same operations as the listWebhookEndpoints / getWebhookEndpoint queries and the createWebhookEndpoint / updateWebhookEndpoint / destroyWebhookEndpoint mutations.
Creating an endpoint requires a write-capable credential: an API key, or an OAuth token holding the write scope for the surface you are calling. The same JSON:API and GraphQL surfaces are described in the general API references; see the JSON:API reference and the GraphQL reference.
An LLM agent can manage endpoints the same way through the MCP server: its Lua code-mode surface whitelists the same create, read, update, and destroy operations on endpoints, plus read access to deliveries, authenticated with an OAuth 2.1 session rather than an API key and gated on the mcp:write scope for writes.
Examples
Both examples create a bearer-authenticated endpoint that subscribes to item.created and item.committed. Replace <api_key> with your credential and set your own auth_secret; see Endpoint configuration for the full field set.
A JSON:API create request (POST /api/json/webhook-endpoints) with the vnd.api+json body:
curl -X POST https://www.truestamp.com/api/json/webhook-endpoints \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-d '{
"data": {
"type": "webhook_endpoint",
"attributes": {
"url": "https://example.com/webhook",
"resource_type": "item",
"events": ["item.created", "item.committed"],
"description": "My production webhook",
"auth_type": "bearer",
"auth_secret": "my-secret-token"
}
}
}'
The equivalent GraphQL createWebhookEndpoint mutation:
mutation {
createWebhookEndpoint(input: {
url: "https://example.com/webhook"
resourceType: "item"
events: ["item.created", "item.committed"]
description: "My production webhook"
authType: "bearer"
authSecret: "my-secret-token"
}) {
result {
id
url
events
enabled
}
errors {
message
code
fields
}
}
}
Endpoint configuration
An endpoint is defined by the URL to call, the resource type to listen on, and the specific events to receive.
| Field | Required | Notes |
|---|---|---|
url |
yes | Must be an https:// URL. http:// is rejected. |
resource_type |
yes | The resource to listen on, currently "item". Set only at create time; the update action does not accept it. |
events |
yes | Non-empty list of event types, each consistent with resource_type (for example ["item.created", "item.committed"]). |
description |
no | Optional human-readable label. |
auth_type |
no | "none" (default) or "bearer". |
auth_secret |
conditional | The bearer token. Required when auth_type is "bearer". Stored encrypted and never returned on any read surface. |
custom_headers |
no | A map of extra header name/value pairs sent with every delivery. Stored encrypted. |
team_filter_id |
no | When set, only events for items in that team are delivered. |
enabled |
no | Defaults to true. Set through the update action to pause an endpoint. |
By default an endpoint receives events for every item the owning account submitted, across all of that account’s teams. Setting team_filter_id narrows delivery to items belonging to that one team.
The number of endpoints you can create is capped by your plan’s limit; once you reach it, further create requests are rejected. Some plans allow unlimited endpoints, and a plan with a zero limit cannot create endpoints at all.
The auth_secret and custom_headers values are write-only secret material. They are encrypted at rest and are never echoed back in JSON:API or GraphQL responses, so you cannot read a secret out again after setting it.
Endpoint dispatch is governed by a feature flag, so an operator can pause all outgoing deliveries platform-wide without deleting anyone’s endpoints.
Events
Event names follow the pattern {resource_type}.{action}. An endpoint may only subscribe to events that match its resource_type, and the events list must be non-empty. For the item resource the subscribable events are:
| Event | Meaning |
|---|---|
item.created |
An item was submitted. |
item.committed |
An item was included in a finalized block. |
item.redacted |
An item’s claims were redacted. |
item.unredacted |
An item’s claims were restored. |
item.deleted |
An item was permanently deleted. |
Subscribing to any event outside this set is rejected at validation time.
Delivery payload
Each delivery is an HTTPS POST with a JSON body in a fixed envelope:
{
"type": "item.created",
"timestamp": 1737039000,
"data": { }
}
typeis the event name.timestampis Unix time in seconds (an integer), captured when the payload is built.datais the resource-specific payload.
For item.created, item.committed, item.redacted, and item.unredacted, the data object carries item metadata such as the item id, state, claims_hash, item_hash, visibility, tags, team id, and timestamps. The item’s claims content itself is never included, only its claims_hash, so a webhook cannot leak the underlying claim data. For item.deleted, the item no longer exists when the delivery is built, so data carries only the item id.
Every delivery carries a stable set of headers so your receiver can identify and de-duplicate it:
| Header | Value |
|---|---|
content-type |
application/json |
user-agent |
Truestamp-Webhook/1.0 |
webhook-id |
Unique message id for idempotent de-duplication. |
x-truestamp-event |
The event type. |
x-truestamp-delivery |
The delivery record id. |
x-truestamp-timestamp |
Unix timestamp (seconds) of this delivery attempt. |
authorization |
Bearer <secret>, only when auth_type is "bearer". |
Any custom_headers you configured are merged in as well.
Authenticating deliveries
Truestamp signs deliveries by sending a shared secret you control, not by computing a payload signature. There is deliberately no HMAC payload signature on outgoing deliveries.
- With
auth_type: "none"(the default), no auth header is sent and security rests on keeping the endpoint URL secret. - With
auth_type: "bearer", every delivery includes anAuthorization: Bearer <auth_secret>header carrying the secret you configured, which your receiver can check to confirm the request originated from your endpoint.
Independently of auth_type, you can supply custom_headers to add your own headers to every delivery. A restricted set of protocol-controlled headers cannot be set this way (host, content-length, content-type, transfer-encoding, connection, and authorization), and header names and values may not contain carriage-return, line-feed, or NUL characters, which blocks header-injection attempts.
Delivery tracking
Every delivery attempt is recorded so you can audit what was sent and diagnose failures. Delivery records are read-only everywhere: you can list and fetch your own deliveries, but you cannot create or modify them, and you only ever see deliveries for your own endpoints.
| Operation | JSON:API | GraphQL |
|---|---|---|
| List your deliveries | GET /api/json/webhook-deliveries |
listWebhookDeliveries |
| Get one delivery | GET /api/json/webhook-deliveries/:id |
getWebhookDelivery |
A delivery record captures the event type, the resource type and id that triggered it, the unique message id sent in the webhook-id header, a status (pending, success, failed, or cancelled), the HTTP status returned by your receiver, any error message, the number of attempts so far, and the round-trip duration. The outgoing payload and a truncated copy of your receiver’s response body are also stored on the record for debugging; both are omitted from the default fields on a plain read, but either can be requested by name if you need to inspect exactly what was sent or received.
Deliveries follow a simple retry model. A 2xx response marks the delivery success. Transient failures (a 408 or 429, any 5xx, or a network error) are retried a bounded number of times with exponential backoff; if they never succeed the delivery ends as failed. Other 4xx responses, a redirect response, or a target that resolves to a private address are treated as permanent and the delivery is cancelled without further retries. If an endpoint is disabled before a queued delivery runs, that delivery is cancelled. Old delivery records are pruned automatically after 31 days, so the delivery log is a recent history rather than a permanent archive. This pruning is governed by its own feature flag, independent of the dispatch flag, so an operator can turn automatic pruning off (leaving old records in place) without affecting whether new deliveries are dispatched, and vice versa. When you delete your account, all of your webhook endpoints and their delivery records are removed as part of the deletion.
SSRF safeguards
Because a webhook makes Truestamp send an outbound request to a URL you supply, endpoints are screened to prevent them being used to reach internal or reserved network addresses (a server-side request forgery, or SSRF).
At create and update time the URL is validated: it must use https, it must not embed credentials in the URL, the literal host localhost is blocked, and the host is resolved and rejected if it points at any private, loopback, link-local, unique-local, carrier-grade-NAT, or otherwise reserved address in either IPv4 or IPv6. A host that simply fails to resolve at validation time is allowed through, because the delivery-time check will catch it later.
At delivery time the safeguard is re-applied. Just before each POST, the host is resolved again and re-checked against the same private and reserved ranges, and the request is pinned to the exact vetted address so DNS cannot be swapped for a private target between the check and the connection (a DNS-rebinding defense). Redirects are never followed, so a redirecting receiver cannot bounce the delivery to an unvetted internal address. Any delivery whose target resolves to a private or reserved address is cancelled rather than sent.
Limitations
- The only resource type currently supported is
item; the subscribable events are the five item events listed above. - Deliveries are not signed with an HMAC or other payload signature. Sender verification, when you want it, is done with the bearer token you configure.
- Endpoints are owned by the account that creates them. A webhook receives events for items that account submitted, optionally filtered to one team, and is not a team-wide subscription owned by the team itself.
- Item
claimscontent is never sent, onlyclaims_hash, so a webhook cannot be used to exfiltrate claim data. - Delivery records are retained for 31 days before being pruned.