Webhooks
Push events from Worksible into your systems instead of polling. Webhooks let you react to invoice payments, contract signatures, timesheet approvals, KYB outcomes and more, with signed payloads and at-least-once delivery.
/v1/companies/me/invoices?status=paid is what most customers do today). Subscribe to the changelog to be notified.Webhooks will be available on Global and above. Professional customers can keep using polling; we don't plan to gate read endpoints behind a higher tier.
Planned events
Each event maps to a specific business transition. Names use resource.action in lowercase.
| Event | When |
|---|---|
| invoice.issued | An invoice transitioned from draft to issued. |
| invoice.paid | Payment was received against an invoice. |
| invoice.overdue | An issued invoice passed its due date without payment. |
| payment.succeeded | An inbound payment cleared. |
| payment.failed | An inbound payment was declined or returned. |
| payout.sent | A payout to a freelancer left our account. |
| payout.delivered | The payout reached the recipient bank. |
| payout.failed | A payout was rejected by the rail. |
| contract.signed | All parties signed a contract. |
| contract.declined | A signer declined or expired the contract. |
| timesheet.submitted | A freelancer submitted a timesheet for approval. |
| timesheet.approved | An approver approved a timesheet. |
| kyb.approved | An organization passed KYB review. |
| kyb.rejected | An organization failed KYB review. |
Payload shape
Every event uses the same envelope. The data field is a snapshot of the resource at the time of the event — subsequent state changes produce new events; we never silently update a previously delivered event.
{
"id": "evt_2026q3_8f3a",
"type": "invoice.paid",
"createdAt": "2026-08-12T10:14:22.000Z",
"organizationId": "org_2024_AbCd",
"data": {
"id": "inv_456",
"invoiceNumber": "WK-202608-019",
"totalCents": 151250,
"currency": "EUR",
"status": "paid",
"paidAt": "2026-08-12T10:14:21.000Z"
}
}Signing
Each request carries an X-Worksible-Signature header containing a HMAC-SHA256 of {timestamp}.{rawBody} signed with the endpoint's secret. Compute the same on your side and reject any request whose signatures don't match.
X-Worksible-Signature: t=1755013241,v1=4f3a0c1b2d...
X-Worksible-Event-Id: evt_2026q3_8f3a
Content-Type: application/jsonimport { createHmac, timingSafeEqual } from "node:crypto";
export function verifySignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((s) => s.split("=")),
);
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
// Reject anything older than 5 minutes (replay protection).
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
return timingSafeEqual(
Buffer.from(parts.v1, "hex"),
Buffer.from(expected, "hex"),
);
}Delivery semantics
- At-least-once.If your endpoint doesn't answer with a 2xx within 15 seconds, we retry with exponential backoff (1m, 5m, 30m, 2h, 12h, 24h). After 24 hours of failure we mark the endpoint disabled and email the workspace owner.
- Out of order.Don't assume events arrive in the order they happened. Each event carries
createdAt; if you store last-seen, ignore older events. - Deduplicate by event id.
idis unique even across retries. If you've seen an id, the safe move is to ack with 200 and skip processing.
Polling pattern (today)
Until webhooks ship, the canonical pattern is a 5-minute cron that reads each list endpoint you care about with a createdAt[gte] filter set to your last successful sync. Worksible-grade integrations work fine on polling; we run a few of them ourselves.
# Look back 10 minutes (last-seen 5min, plus 5min jitter).
LAST_SEEN=$(date -u -d "-10 minutes" +"%Y-%m-%dT%H:%M:%SZ")
curl "https://api.worksible.com/v1/companies/me/invoices?status=paid&paidAt[gte]=${LAST_SEEN}" \
-H "X-API-Key: $WORKSIBLE_API_KEY"