Installer.com Docs

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:

TimingDescription
On stateFires immediately when the step enters the selected state
After X daysFires X days after the step enters the selected state
Before X daysFires 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}}). A step webhook is always POST; an automation webhook picks its method.
  • 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:

StateDescription
activeThe step is now the current step and requires action
doneThe step has been completed successfully
skippedThe step was skipped
rejectedThe step was rejected (e.g. a dispatching was declined)
cancelledThe step was cancelled
expiredThe step expired without being completed in time
unconfirmedThe step is awaiting confirmation (e.g. dispatching sent to contractor)
waitingThe step is waiting for a future event (e.g. a scheduled booking)
overdueThe 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

VariableTypeDescription
order.idstringOrder UUID
order.displayIdstringHuman-readable order ID (e.g. P1-42)
order.namestringOrder name
order.emailstringOrder contact email
order.phoneNumberstringOrder contact phone number
order.contactPersonNamestringFull name of the contact person
order.firstNamestringFirst name (derived from contactPersonName)
order.lastNamestringLast name (derived from contactPersonName)
order.addressstringStreet address
order.citystringCity
order.postalCodestringPostal code
order.countryCodestringCountry code (e.g. NO, NL)
order.tokenstringOrder token. Step webhooks only: an automation webhook strips it, because it opens the customer portal.
order.createdAtstringCreation timestamp
order.tagsarrayEvery tag on the order visible to your organization, each with id, name and color. Automation webhooks only.
order.externalIdstringYour own reference for the order

Workflow

VariableTypeDescription
workflow.idstringWorkflow UUID
workflow.orderIdstringAssociated order UUID
workflow.workflowTemplateIdstringThe workflow template version the order was created from. A new value after every publish.

Router (organization)

VariableTypeDescription
router.idstringRouter organization UUID
router.namestringOrganization name
router.slugstringOrganization slug
router.logoFileIdstringLogo file identifier
router.localestringOrganization locale
router.timezonestringOrganization timezone
router.senderIdstringSMS sender ID
router.defaultEmailSenderNamestringDefault email sender name
router.addressstringStreet address
router.citystringCity
router.postalCodestringPostal code
router.countryCodestringCountry code (e.g. NO, NL)
router.statestringState / region subdivision code (e.g. CA, NY)
router.legalNamestringLegal organization name
router.legalOrgIdstringLegal organization ID (e.g. company registration number)

Responsible routing operator

VariableTypeDescription
responsibleRoutingOperator.firstNamestringOperator first name
responsibleRoutingOperator.lastNamestringOperator last name
responsibleRoutingOperator.namestringFull name (first + last)
responsibleRoutingOperator.emailstringOperator email
responsibleRoutingOperator.phoneNumberstringOperator phone number
responsibleRoutingOperator.languagestringPreferred language

Booking

Only available if a BookingCreate step exists in the workflow and is connected to the current step or its dependencies.

VariableTypeDescription
booking.idstringBooking UUID
booking.startAtstringBooking start time (ISO 8601)
booking.endAtstringBooking end time (ISO 8601)
booking.typestringBooking 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.

VariableTypeDescription
offer.idstringOffer UUID
offer.namestringOffer name
offer.statusstringOffer status
offer.payerTotalAmountnumberTotal amount
offer.linesarrayOffer line items

Each item in offer.lines:

VariableTypeDescription
offer.lines.[index].namestringLine item name
offer.lines.[index].quantitynumberQuantity
offer.lines.[index].payerUnitAmountnumberUnit amount
offer.lines.[index].productIdstringProduct id
offer.lines.[index].externalIdstringExternal product id

Step

VariableTypeDescription
step.idstringCurrent workflow step UUID. Unique to this order.
step.rootTemplateStepIdstringIdentifies which step of the workflow this is. The same value in every order, and unchanged by a publish.
step.titlestringStep name. Empty on a built-in step type that was never renamed.
step.typestringStep type (e.g. booking_create, custom, dispatching)
step.variantstringBooking type on a BookingCreate step (e.g. installation), otherwise empty
step.indexnumberStep position in the workflow. Renumbered when a step is added to a running order.

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.

VariableTypeDescription
actor.[slug].organizationNamestringContractor organization name
actor.[slug].organizationLegalNamestringLegal organization name
actor.[slug].organizationLegalOrgIdstringLegal organization ID (e.g. company registration number)
actor.[slug].organizationResponsibleNamestringFull name of the responsible person
actor.[slug].organizationResponsibleFirstNamestringFirst name
actor.[slug].organizationResponsibleLastNamestringLast name
actor.[slug].organizationResponsibleEmailstringEmail address
actor.[slug].organizationResponsiblePhoneNumberstringPhone 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:

VariableTypeDescription
actors.[index].idstringActor UUID
actors.[index].slugstringActor slug
actors.[index].namestringActor name
actors.[index].activeRepresentative.user.firstNamestringRepresentative first name
actors.[index].activeRepresentative.user.emailstringRepresentative email
actors.[index].activeRepresentative.tenant.namestringOrganization name

Active step actor and representative

VariableTypeDescription
activeStepActor.idstringActor assigned to the current step
activeStepActor.slugstringActor slug
activeStepRepresentative.user.firstNamestringRepresentative's first name
activeStepRepresentative.user.emailstringRepresentative's email
activeStepRepresentative.tenant.namestringOrganization 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:

VariableTypeDescription
customFields.[customId]stringValue of the custom field with the given custom ID

By field UUID, for full field metadata:

VariableTypeDescription
customField.[fieldId].valuestringField value
customField.[fieldId].typestringField type
customField.[fieldId].customIdstringCustom 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 groupRequires
bookingA BookingCreate step exists in the workflow and is connected to the current step or its dependencies
offer / latestOfferAn OfferCreate step has been completed (status = Done)
actor.[slug]The actor has been dispatched (a Dispatching step for that actor is completed)
activeStepRepresentativeThe current step has an assigned actor with an active representative
customFieldsFields are required in earlier steps or have default values

Trigger

What the trigger itself was about, resolved into names rather than ids. Keys are absent when the trigger does not carry them, so {{#if trigger.tag}} tells you whether a tag was involved.

The {{ picker in the builder offers the subset that is specific to the trigger you chose; the rest of the table is reachable by typing it.

VariableAvailable onDescription
trigger.typeevery triggerThe trigger the automation fired on
trigger.atevery triggerWhen the trigger happened (ISO 8601)
trigger.isManualevery triggertrue when an operator started the run by hand
trigger.isTestevery triggertrue on a test order
trigger.step.*any trigger that names a stepThe step's id, type, title and index. prevStatus, nextStatus, action and actionReason only on a status change. Absent on triggers that are not about a step.
trigger.actor.* / trigger.representative.*any workflow.actor.*The actor, and the organization and person the transition was about
trigger.tag.*a tag being assigned or removedid, name, color
trigger.customFieldsFilled / .customFieldsClearedcustom fields changingArrays of { id, customId, name, value }
trigger.chat.*a chat messageid, messageId, senderType. The message text is never sent.
trigger.clock.*a timer or scheduleanchor and dueAt. No offset: the stored value has no direction, so "two hours before" and "two hours after" would look identical.
trigger.event.*a timer counting from an eventThe anchor event's id, type and occurredAt

event.payload still carries the raw trigger data and is unchanged, so existing templates keep working. trigger.* is the resolved form and is what new templates should use.

System variables

These variables are always available regardless of which step the webhook is configured on:

VariableDescription
stateThe workflow step state that triggered the webhook
run.isManualtrue when an operator started this run by hand rather than a trigger firing
timestampISO 8601 timestamp of when the webhook was triggered
clientAppUrlURL to the client-facing application
routingOperatorAppUrlURL to the routing operator application

Retry policy (step webhooks)

This section describes webhooks configured on a workflow step. An automation webhook retries on its own schedule and is covered under Webhooks from automations.

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:

AttemptDelay
1st attemptImmediate
2nd attempt (1st retry)~1 second
3rd attempt (2nd retry)~2 seconds

After 3 failed attempts, the delivery is marked as failed. You can view the execution logs and status in the workflow step's notification history.

The retry window is about 3 seconds in total. A deploy or restart longer than that will drop deliveries permanently, so treat a delivery as something to be accepted quickly and processed later rather than something we will keep attempting.

Webhooks from automations

An automation can also send a webhook. It shares the templating and variables described above, and adds the guarantees below. Both kinds may be enabled at once.

Retries

An automation webhook backs off on its own schedule (roughly 30 seconds, then 2 minutes, then 8 minutes, then every 25 minutes) up to the automation's attempt limit, and honours Retry-After when you send one. A receiver that fails repeatedly has its endpoint paused for a few minutes rather than being hammered; deliveries queue and resume.

Signed requests

Every request is signed following the Standard Webhooks specification, so you can verify it with the standard library for your language rather than implementing ours.

HeaderMeaning
webhook-idIdentifies the message. Stable across automatic retries.
webhook-timestampUnix seconds, fresh on every attempt.
webhook-signaturev1,<base64>. A space-separated list during a secret rotation, so verify against any entry.

The signed string is {webhook-id}.{webhook-timestamp}.{raw request body}, and the signature is HMAC-SHA256 of that string using the endpoint's signing secret. The secret is shown once when the endpoint is created and again when it is rotated.

We also send, always and never absent:

HeaderMeaning
x-installer-delivery-idIdentifies this attempt. Quote it to support.
x-installer-webhook-attempt1-based attempt number.
x-installer-webhook-testtrue for a test run.
x-installer-webhook-sourceWhich part of the platform produced the message.

One more is sent whenever the message has a trigger, which is every webhook an automation sends:

HeaderMeaning
x-installer-event-typeThe trigger, so you can route without parsing the body.

A header you set yourself is dropped if it uses one of these names. We always send our own value, so yours would have been overwritten anyway.

Authenticating to your endpoint

The signature above proves a request came from us. If your endpoint also requires a credential from us, an operator can attach one to the endpoint. Both options are set on the endpoint, not on the automation, so rotating them needs no republish and takes effect on a delivery already queued for retry.

A static header. Any header your API expects, for example X-Api-Key: <key>. Values are write-only: once saved they are never returned by the API, and only their names are shown.

OAuth2 client_credentials. We exchange a client id and secret for a bearer token before the delivery and send it as Authorization: Bearer <token>. Tokens are cached until shortly before they expire, so a burst of deliveries is one grant rather than one per request.

FieldNotes
Token URLWhere the grant is exchanged. Must be https.
Client ID / secretThe secret is write-only and never shown again.
ScopeRequired by AWS Cognito; optional elsewhere.
AudienceUsed by Auth0 and similar; ignored by Cognito.
Credential placementBasic header or request body. Providers disagree on which they accept, so pick the one yours documents.

Credentials live on the endpoint, so every automation sending to that host uses them without repeating the configuration. If we get a 401 from your endpoint we discard the cached token, so the next attempt uses a fresh one.

Two failure modes are worth knowing about, because they look different in the delivery log: a failed token exchange never reaches your endpoint and is recorded against the token step, so a provider outage does not read as your endpoint rejecting us. A rejected grant (invalid_client, invalid_scope) is not retried, because resending the same credentials cannot succeed.

Choosing a method

An automation webhook sends POST unless you pick otherwise. PUT, PATCH and DELETE carry the body you write; GET sends no body at all, and no Content-Type header with it. The method is frozen on the message alongside the rendered bytes, so a retry repeats the verb it first sent.

Authoring against real data

The builder renders the request before you publish it. Typing {{ in the address, a header value or the body lists what this trigger and workflow actually populate, and Show preview renders the whole request (address, headers and body) without sending anything.

Once a webhook is saved you can also run it against one real order from Run automation on…. A webhook may be rehearsed while it is still a draft.

A run started this way has no triggering event, so state and the trigger-specific trigger.* variables are empty. A path that uses one fails on a manual run even when the preview renders it.

Changes to existing behaviour

A webhook already published keeps sending what it sent before. Its step, booking, offer and representative are picked the way they always were.

The next time you publish it, it picks them from what triggered the run instead. Check these before republishing:

TriggerBeforeAfter republishing
Timer counting from a bookingbooking.* is the order's latest bookingThe booking the timer fired for
Timer counting from a stepstep.* is the first active stepThe step the timer counted from
Actor dispatched or rejectedstep.* is the first active stepThe dispatching step, and actor.<slug> names the organisation the event is about
Step actor changedbooking.* and offer.* blank when the step owns noneThe order's latest booking and offer

A path such as /steps/{{step.id}} on a dispatch trigger posts to a different step after republishing. On an order with one booking and one active step, nothing changes.

Still only on workflow-step webhooks

These have no equivalent on an automation webhook yet, so a step webhook using one cannot move across:

FeatureNote
A templated hostAn automation's address keeps a literal scheme and host, which is what makes the delivery-time host check well defined. Only the path accepts variables.
{{dnoFile}} / {{dnoFileOptional}}The ENA Connect attachment upload runs only on the step-task path.
Per-order overridesA step webhook's address and body can be edited on a single order and retried. An automation's version is immutable by design.
Literal \n strippingA step webhook has every literal \n removed from its body and its headers before rendering. An automation's are left alone, so a template relying on that renders differently.

What to rely on

  1. Delivery is at-least-once. Dedupe on webhook-id: the same message may arrive more than once, and every copy carries identical bytes.
  2. Ordering is not guaranteed. Retries and independent automations interleave, so order on your own payload timestamps rather than on arrival.
  3. Two events can describe one fact, so also key on the subject id and event type, not only on webhook-id.
  4. Reject a stale timestamp. Five minutes of tolerance is a reasonable window; never accept 0.
  5. We may add fields. Ignore unknown ones. Removing or renaming a field gets a new event type rather than a silent change.
  6. A manual resend carries a new webhook-id and does not cancel retries that are already pending.

Responses we act on

You returnWe do
2xxTreat the delivery as successful.
410 GoneDisable the endpoint. Use this only when you mean permanently.
408, 425, 429, any 5xxRetry, honouring Retry-After when you send it.
Any other 4xxTreat it as a misconfiguration and stop, unless the automation opts into retrying.
3xxTreat it as a failure. Redirects are never followed — configure the final URL.

The request times out after 10 seconds by default, configurable per endpoint up to 30. Only https is accepted, and literal private or link-local addresses are refused.

Handling webhooks

Your webhook endpoint should:

  1. Return 2xx quickly: respond within 5 seconds to avoid timeouts
  2. Process asynchronously: queue the event for background processing if it requires heavy work
  3. Be idempotent: the same webhook may fire more than once (e.g. if a step transitions back to a previous state)

Troubleshooting

IssueSolution
Not receiving webhooksVerify the step actually transitions to the configured state. Check the notification history in the workflow step for error logs.
Invalid JSON in payloadEnsure all Handlebars expressions are wrapped in double quotes: "{{order.id}}" not {{order.id}}
Empty variable valuesThe required preceding step may not have been completed. See State-dependent variable availability.
Timeout errorsReturn 200 immediately and process the event asynchronously
Template variable errorA referenced variable is null or undefined. Verify the variable exists for the step's position in the workflow.

On this page