Skip to content

Receive webhooks inside your network

Who this is for: admins whose systems send webhooks with data that must not leave the company, for example an ERP or an HR system in your own network.

Before you start: check these three things.

  • A runner group with a running runner. See Run automations in your own environment.
  • A way for the sender to reach the runner, for example your own reverse proxy or load balancer that ends TLS and forwards to the runner. The runner itself speaks plain HTTP.
  • The workflow must run on that group with runs-on: self-hosted/<group>.

Normally Liitora receives a webhook, checks it and hands the payload to the runner. With receive: runner, the runner receives the webhook itself. It checks the delivery the same way Liitora does, keeps the payload in a private folder on its own machine and tells Liitora only that something arrived. Liitora then queues the run, and the runner reads the payload from its folder when it runs it.

Liitora gets only the event id, the endpoint name, when the webhook arrived, how many bytes the body had and salted hashes of the idempotency key, the replay key and the body. It never gets the body, the headers or the signing secret.

Add receive: runner to the webhook trigger of a workflow that has runs-on:

name: Import new orders from the ERP
description: Receives new orders from the ERP inside our own network.
runs-on: self-hosted/on-prem
on:
webhook:
endpoint: erp-orders
receive: runner
steps:
- id: import
name: Import the order
run: |
export default async function (ctx) {
return { order: ctx.event.body.order };
}

The steps read the delivery the same way as a webhook that Liitora receives. You don’t create a webhook endpoint in Liitora for this. The endpoint and its secret live only on the runner. Liitora refuses receive: runner without runs-on with webhook_receiver_needs_runner.

Approve and activate the workflow as usual. Where your data goes then says No, received by the runner at webhook endpoint erp-orders under Webhook payloads enter Liitora, and the runner holds the endpoint’s secret.

  1. Write an endpoints file, for example endpoints.json. It lists the endpoints the runner accepts:

    [
    { "name": "erp-orders", "secret": "erp_orders_hook" }
    ]
  2. Put the signing secret into the runner’s secrets, under the name from the file. With environment variables, that is LIITORA_SECRET_ERP_ORDERS_HOOK. With Vault or OpenBao, it is the field value of the secret erp_orders_hook. Use a long random value and give the same value to the sender.

  3. Add these settings to the runner’s settings file:

    LIITORA_RUNNER_WEBHOOK_LISTEN=0.0.0.0:8088
    LIITORA_RUNNER_WEBHOOK_ENDPOINTS=/etc/liitora-runner/endpoints.json
  4. Start the runner with the endpoints file mounted read-only and the port published only to your proxy, for example with -v ./endpoints.json:/etc/liitora-runner/endpoints.json:ro -p 127.0.0.1:8088:8088.

  5. Point your proxy at the runner, and the sender at your proxy. The address of an endpoint is /hooks/<name>, for example https://hooks.example.internal/hooks/erp-orders.

The runner checks the endpoints file when it starts. If something is wrong, it stops and the log names the endpoint and the field.

Field What it does
name The endpoint’s name, the same as on.webhook.endpoint in the workflow: 1 to 63 lowercase letters, digits and - (required)
secret The name of the secret that checks deliveries, in the runner’s secrets (required)
auth hmac_sha256 (the default), bearer or basic
provider generic (the default), github or stripe. GitHub and Stripe need hmac_sha256
signature_header The header with the signature for generic with hmac_sha256 (default x-liitora-signature)
replay_window_seconds How old a signed delivery may be, 1 to 3600 (default 300)
rate_limit_per_minute Deliveries a minute, 1 to 10000 (default 60)
max_body_bytes The largest body, up to 10 MiB (default 256 KiB)

The file can list up to 100 endpoints. Signatures, tokens and passwords work as in Webhook signing.

  1. Put the new value into the secret’s value field, and the old one into its previous field. With environment variables, the old one goes into LIITORA_SECRET_ERP_ORDERS_HOOK__PREVIOUS.
  2. Restart the runner. Both values now work.
  3. Give the sender the new value.
  4. Remove the old value and restart the runner again.

The runner keeps each delivery as a file in webhook-events in its state folder, or in LIITORA_RUNNER_WEBHOOK_STORE_DIR. Only the runner’s user can read the folder and its files. The runner removes a delivery at once when it starts no run, for example a repeat of an earlier delivery. All others are removed after 14 days. Set LIITORA_RUNNER_WEBHOOK_RETENTION_DAYS (1 to 365) to keep them for a shorter or longer time. The runner checks once an hour.

A run whose delivery is no longer there fails with webhook_event_unavailable. Ask the sender to send it again.

A run started by a delivery is always run by the runner that received it, because only that runner has the payload. If you remove that runner, its waiting runs fail with webhook_event_unavailable.

Repeats are recognised by the salted hash of the Idempotency-Key header. Each runner makes its own salt, so a repeat sent to another runner of the group is not recognised as one. If your proxy spreads deliveries over several runners, give them the same salt: a random value of at least 32 characters in LIITORA_RUNNER_WEBHOOK_SALT on every runner of the group. Keep it as secret as a password.

The same answers as from Liitora: 202 when the delivery was accepted, 200 with "duplicate": true for a repeat, 401 for a wrong signature or an unknown endpoint, and the other codes in Webhook delivery responses. The runner answers 503 and keeps the delivery when it can’t reach Liitora or the endpoint’s secret is missing. Most senders retry by themselves.

Setting What it does
LIITORA_RUNNER_WEBHOOK_LISTEN The address and port the listener binds, for example 0.0.0.0:8088. Without it the runner receives no webhooks
LIITORA_RUNNER_WEBHOOK_ENDPOINTS The endpoints file (required with the listener)
LIITORA_RUNNER_WEBHOOK_STORE_DIR The folder for the deliveries (default: webhook-events in the state folder)
LIITORA_RUNNER_WEBHOOK_RETENTION_DAYS Days a delivery is kept, 1 to 365 (default 14)
LIITORA_RUNNER_WEBHOOK_SALT A salt shared by all runners of the group, at least 32 characters