Webhook endpoints
Get notified in real time, verify signatures, and handle retries.
Webhooks let Trimline tell your systems when something happens, such as a vendor replying, a draft waiting for approval, or savings being confirmed, instead of you polling for changes. We send an HTTPS POST with a JSON event to each endpoint that subscribes to that event type.
Endpoints
- GET/v1/webhook_endpointsList
- POST/v1/webhook_endpointsCreate
- PATCH/v1/webhook_endpoints/:idUpdate events or URL
- DELETE/v1/webhook_endpoints/:idDelete
- POST/v1/webhook_endpoints/:id/testSend a test event
Create an endpoint
curl -X POST https://api.trimline.tech/v1/webhook_endpoints \
-H "Authorization: Bearer $TRIMLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.northwind.example/trimline",
"enabled_events": ["draft.created", "offer.received", "renewal.deadline_approaching"],
"description": "Slack approvals bot"
}'
{
"id": "we_1Mz4Rt",
"object": "webhook_endpoint",
"url": "https://hooks.northwind.example/trimline",
"enabled_events": ["draft.created", "offer.received", "renewal.deadline_approaching"],
"status": "enabled",
"secret": "whsec_kT9v2uYq…",
"livemode": false
}
The secret is only returned when the endpoint is created. Store it. You need it to verify signatures. Pass ["*"] as enabled_events to receive everything.
Payload
POST /trimline HTTP/1.1
Content-Type: application/json
Trimline-Signature: t=1796323200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"id": "evt_4Ks9Lw2",
"object": "event",
"type": "draft.created",
"created_at": "2026-12-03T16:44:02Z",
"livemode": false,
"data": {
"object": { "id": "drf_3Nc81Ab", "object": "draft", "status": "pending", "...": "..." }
}
}
Verifying signatures
Every request is signed with your endpoint’s secret. The Trimline-Signature header contains a timestamp t and an HMAC-SHA256 signature v1 of the string {t}.{raw_body}. Always verify it, and reject events more than five minutes old, to protect against replay attacks.
import crypto from "node:crypto";
export function verifyTrimline(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < toleranceSec;
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!fresh || !valid) throw new Error("Invalid Trimline signature");
return JSON.parse(rawBody);
}
import hmac, hashlib, json, time
def verify_trimline(raw_body: bytes, header: str, secret: str, tolerance: int = 300):
parts = dict(p.split("=", 1) for p in header.split(","))
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts["t"])) > tolerance:
raise ValueError("Stale Trimline event")
if not hmac.compare_digest(expected, parts["v1"]):
raise ValueError("Invalid Trimline signature")
return json.loads(raw_body)
Compute the signature over the raw request body, exactly as received. If a framework parses and re-serializes the JSON first, the signature won’t match.
Responding and retries
- Return any
2xxstatus within 10 seconds to acknowledge an event. Do slow work asynchronously. - If we don’t get a
2xx, we retry with exponential backoff for up to 3 days: after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 6 hours. - An endpoint that fails continuously for 3 days is disabled, and the account owner gets an email.
- Events can arrive more than once and out of order. Deduplicate on the event
id, and use each object’s own timestamps to work out the latest state.
Testing
POST /v1/webhook_endpoints/:id/test with { "type": "draft.created" } sends a realistic sample event of that type to the endpoint. In the sandbox, you can also trigger real event chains with the test helpers.
Something unclear or missing? Email developers@trimline.tech.