Trimline is in private beta. Public launch: June 2027.Join the waitlist →
Docs / Webhooks

Webhook endpoints

Get notified in real time, verify signatures, and handle retries.

Beta. The Trimline API is available to private beta customers. Endpoints may change before general availability in June 2027, and every breaking change is announced in the changelog. Request access.

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

Create an endpoint

POST/v1/webhook_endpoints
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 2xx status 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.