Webhooks
Subscribe to Monument events, receive signed deliveries, and verify the signature on your server.
Webhooks push events to your server as they happen, so you do not have to poll. You register a subscription with a URL and a list of events, and Monument sends a signed JSON POST to that URL whenever a matching event occurs.
Create a subscription
Create a webhook with a name, a url, and at least one event. Creating, updating, and deleting subscriptions requires an admin key. The response includes a signing secret prefixed with whsec_; store it, because you need it to verify deliveries.
curl -X POST 'https://api.monument.so/api/v1/webhooks' \
-H 'X-API-Key: monument_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Billing sync",
"url": "https://example.com/hooks/monument",
"events": ["invoice.paid", "invoice.overdue"]
}'Events you can subscribe to
Subscribe to any of these event types. You can also fetch the current list from GET /api/v1/webhooks/events.
- Invoices:
invoice.created,invoice.sent,invoice.paid,invoice.overdue - Quotes:
quote.created,quote.sent,quote.accepted,quote.rejected - Projects and tasks:
project.created,project.completed,task.created,task.completed - Time and expenses:
time_entry.created,expense.created,expense.approved - Milestones:
milestone.completed
What a delivery looks like
Monument POSTs each event to your URL with the payload as the body and these headers:
X-Monument-Event: invoice.paid
X-Monument-Signature: sha256=6b2f…
X-Monument-Delivery: 2f9c…The body is the event payload:
{
"event": "invoice.paid",
"timestamp": "2026-07-10T12:00:00.000Z",
"data": { }
}Respond with a 2xx status quickly. A non-2xx response or a timeout is retried, up to five attempts, with the delivery recorded either way.
Verify the signature
Compute an HMAC-SHA256 of the raw request body using your webhook secret, then compare it to the hex value after sha256= in the X-Monument-Signature header. Use the raw bytes you received, not a re-serialized object, or the signatures will not match.
import { createHmac, timingSafeEqual } from 'node:crypto';
function isValidSignature(rawBody, signatureHeader, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const received = signatureHeader.replace('sha256=', '');
const a = Buffer.from(expected);
const b = Buffer.from(received);
return a.length === b.length && timingSafeEqual(a, b);
}Manage subscriptions
- List and inspect:
GET /api/v1/webhooksandGET /api/v1/webhooks/:id - Update or delete:
PATCH /api/v1/webhooks/:idandDELETE /api/v1/webhooks/:id - Rotate the secret:
POST /api/v1/webhooks/:id/rotate-secret - Review delivery history:
GET /api/v1/webhooks/:id/deliveries - Send a test delivery:
POST /api/v1/webhooks/:id/test
See the Webhooks domain in the reference for the full request and response schemas.