Webhooks
Webhooks
Webhooks let you receive real-time event callbacks whenever data changes in your Coastline workspace. Register a target URL with a trigger, and Coastline will POST a JSON payload to that URL each time the event fires. Use webhooks to push data into Zapier, Make, n8n, your own backend, or any HTTP endpoint you control.
How it works
- Create an API key in Settings → API Keys.
- Subscribe to a trigger by sending a
POST /v1/subscriptionswith atrigger_idand yourtarget_url. - Coastline POSTs a signed JSON payload to your URL each time the event fires.
- Respond with an HTTP
2xxwithin 15 seconds to acknowledge delivery.
See the subscriptions reference for the full create / list / revoke API.
Available triggers
The trigger_id field on a subscription must match one of the values below.
| trigger_id | Label | Description |
|---|---|---|
| opportunity.created | New Opportunity | Fires when a new opportunity (lead) is created. |
| opportunity.project.stage.updated | Opportunity Stage Changed | Fires whenever an opportunity or project moves to a new pipeline stage. |
| opportunity.project.approved | Project Approved | Fires when an opportunity is approved and converted into a project. |
| opportunity.estimate_sent | Estimate Sent | Fires when an estimate is sent to a customer. |
| opportunity.estimate_approved | Estimate Approved | Fires when a customer approves an estimate. |
| opportunity.estimate_cancelled | Estimate Cancelled | Fires when an estimate is cancelled. |
| opportunity.d4d.spotted | New Lead Spotted (Drive Mode) | Fires when a new lead is captured in Drive Mode. |
| envelope.signed | Document Signed | Fires when a recipient signs a document. |
| envelope.completed | Document Fully Signed | Fires when every recipient has signed a document. |
| call.inbound.completed | Inbound Call Answered | Fires when the AI assistant finishes answering a call to the workspace number. |
| call.outbound.completed | Outbound Call Completed | Fires when an AI-placed call connects and finishes. |
| call.missed | Call Missed | Fires when a call ends without a conversation (abandoned inbound or unanswered outbound). |
| expense.created | New Expense | Fires when a purchase or mileage expense is created. The receipt is attached in a second step, so this event usually carries no receipt yet. |
| expense.receipt_attached | Expense Receipt Captured | Fires when a receipt lands on an expense that had none. This is the event to subscribe to if your integration needs the receipt image. |
| expense.updated | Expense Updated | Fires when a business field on an expense changes. A reconciliation write-back does not count as a change, so stamping an expense never re-fires this. |
| expense.deleted | Expense Deleted | Fires when an expense is removed. data is null because the record is gone; the fields are in metadata. |
| photo.uploaded | New Project Photo | Fires the moment a photo or video is fetchable on a project. A row can exist before its file does, so this waits for the bytes to land rather than firing on creation. |
| photo.deleted | Project Photo Deleted | Fires when a photo is moved to the trash. Restoring one fires photo.uploaded again, so a mirror that dropped it puts it back. |
Payload shape
Every webhook delivery is a JSON object with the same envelope. The data field contains the full record that triggered the event (e.g. the opportunity row for an opportunity event).
| Field | Type | Description |
|---|---|---|
| id | string | Unique event id. Use this to deduplicate. |
| trigger | string | The trigger_id this event matches (e.g. opportunity.created). |
| entity_type | string | The entity that emitted the event (opportunity, contact, task, contractor, envelope, expense, photo). |
| entity_id | string | UUID of the entity record. |
| occurred_at | string | ISO 8601 timestamp of when the event happened. |
| performed_by | string | null | UUID of the workspace user who took the action, or null for system events. |
| metadata | object | Trigger-specific context (e.g. previous_stage_id, new_stage_id for stage changes). |
| data | object | null | The full record that triggered the event. Same shape returned by the resource endpoints. |
Acting on a delivery alone
A handler that has to make three authenticated calls before it can do its job fails partway through three times as often, so data carries what a consumer actually needs:
- Expense events carry the full expense object, including a signed receipt URL that needs no authentication to fetch, and the linked project with its address and custom fields. It is the same shape GET /v1/expenses returns, so one field mapping serves both.
- Photo events carry the photo with signed links to the original and its display and thumbnail renditions, plus a
content_hashso an integration mirroring photos elsewhere can tell what it already has without keeping its own ledger. Same shape GET /v1/photos returns. - Opportunity events carry
data.custom_fields, resolved to stable field keys. See custom fields. - Delete events have
data: null, since the record no longer exists to read. The fields it held are inmetadata.
Signed receipt URLs are valid for 24 hours and are minted fresh on every delivery attempt, so a retried delivery never carries an expired link. Treat one as a credential: anyone holding it can fetch the image until it expires.
Signature verification
Every request includes an X-Coastline-Signature header of the form sha256=<hex>. The signature is an HMAC-SHA256 of the raw request body, keyed by the signing_secret returned when you created the subscription. Verify the signature on every request, never trust an unsigned payload.
You will also receive these headers on each delivery:
| Header | Description |
|---|---|
| X-Coastline-Signature | sha256=<hex> HMAC of the request body. |
| X-Coastline-Event | The trigger_id of the event (e.g. opportunity.created). |
| X-Coastline-Event-Id | Unique event id, same as the id field in the payload. Use to deduplicate. |
| User-Agent | CoastlineCRM-Webhooks/1.0 |
Retries and delivery
- Coastline expects a
2xxresponse within 15 seconds. - Failed deliveries are retried with exponential backoff at 1, 5, 15, 60, and 240 minutes (6 attempts total).
- Permanent
4xxresponses (other than 408 / 429) are not retried. - Respond with HTTP
410 Goneto permanently revoke a subscription from the receiver side. - Deliveries are at-least-once. Use the
idfield to deduplicate.
Deduplicating on id only protects a single process that remembers what it has seen. If handling an event creates a record in another system, keep the durable proof on the Coastline record itself: stamp the expense with an external reference and skip anything already stamped. That is what keeps a retry, a replay, and an overlapping nightly sweep from all creating the same purchase order.
Sample payloads
Fetch up to three of your most recent real events for any trigger via GET /v1/triggers/{trigger_id}/sample. If your workspace has not yet emitted that trigger, a synthetic payload is returned so you can map fields ahead of time.