Webhooks
Send data from workflow steps to external systems on state changes
Overview
Webhooks in Installer.com are configured on individual workflow steps and fire when a step transitions to a specific state. Each webhook sends an HTTP request with a JSON payload that you define using Handlebars templates, allowing you to control exactly what data is sent to your external system.
How to configure a webhook
Open the workflow editor
Go to Workflows in the Routing App. Open the workflow template you want to add a webhook to, or create a new one.
Select a step and add a notification
Click on a workflow step, then navigate to the Notifications tab. Click Add and select Webhook.
Choose the triggering state
Select which step state should trigger the webhook (e.g. active, done, rejected). See Workflow states for the full list.
Configure timing
Choose when the webhook fires relative to the state change:
| Timing | Description |
|---|---|
| On state | Fires immediately when the step enters the selected state |
| After X days | Fires X days after the step enters the selected state |
| Before X days | Fires X days before a booking date (only for steps connected to a booking) |
Set the URL, payload, and headers
- URL: The endpoint to call. Supports Handlebars variables (e.g.
https://api.example.com/orders/{{order.id}}). - Method: HTTP method (default:
POST). UseGETto skip the request body. - Payload: A JSON body using Handlebars templates. See Handlebars syntax for details.
- Headers: Key-value pairs sent with the request. Values support Handlebars templates.
Handlebars syntax
Webhook payloads, URLs, and headers are rendered with the Handlebars templating language. JSON strings containing Handlebars expressions ({{ }}) are replaced with actual values at execution time.
Because the standard Handlebars engine is used, all of its built-in helpers are available — including {{#each}} for iteration and {{#if}} for conditionals — in addition to the custom unix helper.
Basic variable interpolation
Use {{variableName}} or {{object.property}} to insert values:
{
"orderId": "{{order.id}}",
"orderName": "{{order.name}}",
"customerEmail": "{{order.email}}"
}Nested access
Access deeply nested properties with dot notation:
{
"installerName": "{{actor.primary-installer.organizationName}}",
"installerEmail": "{{actor.primary-installer.organizationResponsibleEmail}}"
}Variable values must be wrapped in double quotes to produce valid JSON. Writing "id": {{order.id}} (without quotes around the value) will result in invalid JSON and a failed webhook.
The unix helper
Use the unix helper to convert ISO date strings to Unix timestamps (seconds since epoch):
{
"bookingStart": "{{unix booking.startAt}}",
"bookingEnd": "{{unix booking.endAt}}"
}Iterating over offer lines
Use the built-in {{#each}} helper to loop over arrays such as offer.lines. When building a JSON array, use {{#unless @last}},{{/unless}} to add commas between items but not after the last one:
{
"orderId": "{{order.id}}",
"lines": [
{{#each latestOffer.lines}}
{
"name": "{{this.name}}",
"quantity": "{{this.quantity}}",
"unitAmount": "{{this.payerUnitAmount}}",
"productId": "{{this.productId}}",
"externalId": "{{this.externalId}}"
}
{{#unless @last}},{{/unless}}
{{/each}}
]
}Inside an {{#each}} block, this refers to the current item.
Full payload example
{
"orderId": "{{order.id}}",
"displayId": "{{order.displayId}}",
"orderName": "{{order.name}}",
"customerName": "{{order.contactPersonName}}",
"customerEmail": "{{order.email}}",
"customerPhone": "{{order.phoneNumber}}",
"address": "{{order.address}}",
"city": "{{order.city}}",
"postalCode": "{{order.postalCode}}",
"countryCode": "{{order.countryCode}}",
"state": "{{state}}",
"timestamp": "{{timestamp}}",
"bookingStart": "{{unix booking.startAt}}",
"bookingEnd": "{{unix booking.endAt}}",
"installerOrg": "{{actor.primary-installer.organizationName}}",
"installerContact": "{{actor.primary-installer.organizationResponsibleName}}",
"offer": [
{{#each latestOffer.lines}}
{
"name": "{{this.name}}",
"quantity": "{{this.quantity}}",
"unitAmount": "{{this.payerUnitAmount}}",
"productId": "{{this.productId}}",
"externalId": "{{this.externalId}}"
}
{{#unless @last}},{{/unless}}
{{/each}}
]
}Workflow states
A webhook is triggered when its step enters the configured state. The following states are available:
| State | Description |
|---|---|
active | The step is now the current step and requires action |
done | The step has been completed successfully |
skipped | The step was skipped |
rejected | The step was rejected (e.g. a dispatching was declined) |
cancelled | The step was cancelled |
expired | The step expired without being completed in time |
unconfirmed | The step is awaiting confirmation (e.g. dispatching sent to contractor) |
waiting | The step is waiting for a future event (e.g. a scheduled booking) |
overdue | The step was not completed within the expected time period |
The state variable is automatically injected into the template context with the value of the triggering state.
Available variables
All variables below can be used in the URL, payload, and header templates. Variables are populated from the order and workflow data at the time the webhook fires.
Order
| Variable | Type | Description |
|---|---|---|
order.id | string | Order UUID |
order.displayId | string | Human-readable order ID (e.g. P1-42) |
order.name | string | Order name |
order.email | string | Order contact email |
order.phoneNumber | string | Order contact phone number |
order.contactPersonName | string | Full name of the contact person |
order.firstName | string | First name (derived from contactPersonName) |
order.lastName | string | Last name (derived from contactPersonName) |
order.address | string | Street address |
order.city | string | City |
order.postalCode | string | Postal code |
order.countryCode | string | Country code (e.g. NO, NL) |
order.token | string | Order token |
order.createdAt | string | Creation timestamp |
Workflow
| Variable | Type | Description |
|---|---|---|
workflow.id | string | Workflow UUID |
workflow.orderId | string | Associated order UUID |
Router (organization)
| Variable | Type | Description |
|---|---|---|
router.id | string | Router organization UUID |
router.name | string | Organization name |
router.slug | string | Organization slug |
router.logoFileId | string | Logo file identifier |
router.locale | string | Organization locale |
router.timezone | string | Organization timezone |
router.senderId | string | SMS sender ID |
router.defaultEmailSenderName | string | Default email sender name |
router.address | string | Street address |
router.city | string | City |
router.postalCode | string | Postal code |
router.countryCode | string | Country code (e.g. NO, NL) |
router.state | string | State / region subdivision code (e.g. CA, NY) |
router.legalName | string | Legal organization name |
router.legalOrgId | string | Legal organization ID (e.g. company registration number) |
Responsible routing operator
| Variable | Type | Description |
|---|---|---|
responsibleRoutingOperator.firstName | string | Operator first name |
responsibleRoutingOperator.lastName | string | Operator last name |
responsibleRoutingOperator.name | string | Full name (first + last) |
responsibleRoutingOperator.email | string | Operator email |
responsibleRoutingOperator.phoneNumber | string | Operator phone number |
responsibleRoutingOperator.language | string | Preferred language |
Booking
Only available if a BookingCreate step exists in the workflow and is connected to the current step or its dependencies.
| Variable | Type | Description |
|---|---|---|
booking.id | string | Booking UUID |
booking.startAt | string | Booking start time (ISO 8601) |
booking.endAt | string | Booking end time (ISO 8601) |
booking.type | string | Booking type (e.g. installation, inspection) |
Offer / Latest offer
Only available if an OfferCreate step has been completed (status = Done) before the current step.
offer is the offer connected to the current step's group. latestOffer is the most recently completed offer regardless of step group.
| Variable | Type | Description |
|---|---|---|
offer.id | string | Offer UUID |
offer.name | string | Offer name |
offer.status | string | Offer status |
offer.payerTotalAmount | number | Total amount |
offer.lines | array | Offer line items |
Each item in offer.lines:
| Variable | Type | Description |
|---|---|---|
offer.lines.[index].name | string | Line item name |
offer.lines.[index].quantity | number | Quantity |
offer.lines.[index].payerUnitAmount | number | Unit amount |
offer.lines.[index].productId | string | Product id |
offer.lines.[index].externalId | string | External product id |
Step
| Variable | Type | Description |
|---|---|---|
step.id | string | Current workflow step UUID |
step.type | string | Step type (e.g. booking_create, custom, dispatching) |
step.index | number | Step position in the workflow |
Actor (dictionary by slug)
Only available for actors that have been dispatched (a Dispatching step has completed for that actor). Access actors by their slug.
| Variable | Type | Description |
|---|---|---|
actor.[slug].organizationName | string | Contractor organization name |
actor.[slug].organizationLegalName | string | Legal organization name |
actor.[slug].organizationLegalOrgId | string | Legal organization ID (e.g. company registration number) |
actor.[slug].organizationResponsibleName | string | Full name of the responsible person |
actor.[slug].organizationResponsibleFirstName | string | First name |
actor.[slug].organizationResponsibleLastName | string | Last name |
actor.[slug].organizationResponsibleEmail | string | Email address |
actor.[slug].organizationResponsiblePhoneNumber | string | Phone number |
Replace [slug] with the actor's slug from your workflow (e.g. primary-installer, inspector).
Actors (array)
The actors array contains all workflow actors with their full representative data:
| Variable | Type | Description |
|---|---|---|
actors.[index].id | string | Actor UUID |
actors.[index].slug | string | Actor slug |
actors.[index].name | string | Actor name |
actors.[index].activeRepresentative.user.firstName | string | Representative first name |
actors.[index].activeRepresentative.user.email | string | Representative email |
actors.[index].activeRepresentative.tenant.name | string | Organization name |
Active step actor and representative
| Variable | Type | Description |
|---|---|---|
activeStepActor.id | string | Actor assigned to the current step |
activeStepActor.slug | string | Actor slug |
activeStepRepresentative.user.firstName | string | Representative's first name |
activeStepRepresentative.user.email | string | Representative's email |
activeStepRepresentative.tenant.name | string | Organization name |
activeStepRepresentative is only available if the current step has an assigned actor with an active representative.
Custom fields
Custom fields are available in two formats:
By custom ID, for quick access to the field value:
| Variable | Type | Description |
|---|---|---|
customFields.[customId] | string | Value of the custom field with the given custom ID |
By field UUID, for full field metadata:
| Variable | Type | Description |
|---|---|---|
customField.[fieldId].value | string | Field value |
customField.[fieldId].type | string | Field type |
customField.[fieldId].customId | string | Custom identifier |
Custom field values are only available for fields that are required in earlier workflow steps or have default values.
State-dependent variable availability
Variables are populated based on which workflow steps have completed before the current step. If a required step has not yet been reached, the corresponding variables will be empty.
| Variable group | Requires |
|---|---|
booking | A BookingCreate step exists in the workflow and is connected to the current step or its dependencies |
offer / latestOffer | An OfferCreate step has been completed (status = Done) |
actor.[slug] | The actor has been dispatched (a Dispatching step for that actor is completed) |
activeStepRepresentative | The current step has an assigned actor with an active representative |
customFields | Fields are required in earlier steps or have default values |
System variables
These variables are always available regardless of which step the webhook is configured on:
| Variable | Description |
|---|---|
state | The workflow step state that triggered the webhook |
timestamp | ISO 8601 timestamp of when the webhook was triggered |
clientAppUrl | URL to the client-facing application |
routingOperatorAppUrl | URL to the routing operator application |
contractorAppUrl | URL to the contractor application |
Retry policy
If your endpoint returns a non-2xx status code or the request fails, Installer.com retries the delivery up to 2 times (3 attempts total) with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st attempt | Immediate |
| 2nd attempt (1st retry) | ~1 second |
| 3rd attempt (2nd retry) | Exponential backoff |
After 3 failed attempts, the webhook delivery is marked as failed. You can view the execution logs and status in the workflow step's notification history.
Handling webhooks
Your webhook endpoint should:
- Return 2xx quickly: respond within 5 seconds to avoid timeouts
- Process asynchronously: queue the event for background processing if it requires heavy work
- Be idempotent: the same webhook may fire more than once (e.g. if a step transitions back to a previous state)
Troubleshooting
| Issue | Solution |
|---|---|
| Not receiving webhooks | Verify the step actually transitions to the configured state. Check the notification history in the workflow step for error logs. |
| Invalid JSON in payload | Ensure all Handlebars expressions are wrapped in double quotes: "{{order.id}}" not {{order.id}} |
| Empty variable values | The required preceding step may not have been completed. See State-dependent variable availability. |
| Timeout errors | Return 200 immediately and process the event asynchronously |
| Template variable error | A referenced variable is null or undefined. Verify the variable exists for the step's position in the workflow. |