Webhooks
Webhooks push a signed HTTP request to your server whenever something changes in your workspace — new time entries, approvals, billing packets and more. Events are written in the same database transaction as the change itself, so a delivery may be retried, but it is never lost.
Creating an endpoint
Only workspace owners can manage webhooks. In the app, open Webhooks in the sidebar and add an endpoint with the URL that should receive deliveries, an optional description, and the events you care about.
Events are grouped by app, then entity. Leaving all event checkboxes unticked subscribes the endpoint to every event — including types added in the future.
Each endpoint gets its own signing secret, shown once at creation. You can rotate it at any time; from that moment deliveries are signed with the new secret.
Use the Test button to send a ping event, and the delivery log on each endpoint to inspect attempts, response codes and errors.
The delivery request
Deliveries are HTTP POST requests with a JSON body. The envelope is the same for every event; data carries the affected entity (deletions may carry only its id):
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "time_entry.created",
"createdAt": "2026-08-17T09:30:00.000Z",
"organizationId": "…",
"data": { "id": "…" }
}Every delivery carries these headers:
Content-Type | application/json |
User-Agent | Shrimp-Time-Webhooks/1 |
X-Shrimp-Event | The event type this delivery announces. |
X-Shrimp-Delivery | Unique id of this delivery — together with the envelope id, your key for deduplication. |
X-Shrimp-Signature | Signature over the raw body; see below. |
Verifying signatures
The X-Shrimp-Signature header is an HMAC-SHA256 of the raw request body, keyed with the endpoint's secret, hex-encoded and prefixed with sha256=. Verify it against the raw bytes before parsing the JSON, and reject anything that does not match:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(rawBody, signatureHeader, secret) {
const expected = `sha256=${createHmac("sha256", secret)
.update(rawBody)
.digest("hex")}`;
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Use a constant-time comparison as in the example — a plain string comparison leaks timing information.
Responding and retries
Answer with any 2xx status within 10 seconds — anything else, including a timeout, counts as a failed attempt. Acknowledge first, process after.
Failed deliveries are retried up to 6 times with growing pauses; after the sixth failure — roughly 8.5 hours after the event — the delivery is marked failed for good:
1m · 5m · 30m · 2h · 6h · 12hEndpoints are never disabled automatically. Failed deliveries show up in the delivery log; fix the receiver and future events flow again.
New events usually arrive within a few seconds of the change.
Event types
These are all event types an endpoint can subscribe to. The ping event sent by the Test button is special: it is always delivered, regardless of the subscription.
customer.createdcustomer.updatedcustomer.deletedproject.createdproject.updatedproject.deletedproject.member_assignedproject.member_unassignedtask.createdtask.updatedtask.deletedtime_entry.createdtime_entry.updatedtime_entry.deletedtime_entry.approvedtime_entry.rejectedtimer.startedtimer.stoppedday_off.createdday_off.deletedday_off.approvedday_off.rejectedmember.role_updatedmember.requires_approval_updatedmember.removedorganization.updatedbillable_packet.createdbillable_packet.sentbillable_packet.deletedemployee.createdemployee.updatedemployee.deletedcontract.createdcontract.updatedcontract.deletedvacation_adjustment.createdvacation_adjustment.deletedcrm_company.createdcrm_company.updatedcrm_company.archivedcrm_company.restoredcrm_company.customer_linkedcrm_contact.createdcrm_contact.updatedcrm_contact.archivedcrm_contact.restoredcrm_deal.createdcrm_deal.updatedcrm_deal.archivedcrm_deal.restoredcrm_deal.stage_changedcrm_activity.createdcrm_follow_up.completed
CRM events
Company, contact and deal events contain the entity after the change in data. Company names use the resolved display name. crm_deal.stage_changed also includes previousStage. crm_follow_up.completed contains id (the deal ID), nextAction, nextActionDate and activityId. crm_activity.created contains the stored activity, including parent IDs, type, body, rich text and details.
Specific actions emit specific events, not an additional updated event. Won and lost deals emit crm_deal.stage_changed. A deal win can also create a customer, link its company and create system activities; each change emits its own event. New system activities also emit crm_activity.created. Repeated actions that change nothing emit no event. CRM entities are archived and restored, not deleted.
CRM changes and delivery records are saved in the same database transaction. A rollback saves neither. Delivery starts only after commit and uses the existing retry process. Existing all-event subscriptions receive CRM events automatically. crm.changed is a historical audit event, not a public webhook event.
Managing webhooks via the API
Everything the webhooks page does — creating endpoints, rotating secrets, listing deliveries — is also available on the REST API through the webhooks router. It is an administrative router, so it requires an admin-level API key.