Automations
Webhooks
Webhooks connect appTown with services that speak plain HTTP: incoming webhooks let another service start an automation; outgoing signed webhooks tell another service when your records change.
#Incoming webhooks
Give an automation the trigger Another service calls a webhook URL. It gets a secret URL; anyone with the URL can start it, so treat the URL like a password. Paste it into the service that should call it (a form builder, a payment provider, a script).
- Send JSON in the body (up to 256 KB). Steps read it with
INPUT("path"):INPUT("email"),INPUT("customer.name"),INPUT("items.0.sku")(list items count from 0; a path that isn't there is blank). - The call is answered at once with
202 Acceptedand the run's ID, and the automation runs in the background. To answer with something else (some services expect a certain reply), set a fixed status and body on the trigger. - Incoming webhook runs act with the app's own authority; no appTown account is needed.
Services that can send an API key should use the automation API instead: runs then act as a real person, with their permissions.
#Outgoing signed webhooks
The Send a signed webhook step POSTs the event and the trigger record to an https URL
as JSON, for example on "record created":
{
"id": "0193…:0193…",
"event": "record_created",
"app": "0193…",
"automation": "Tell the CRM",
"run": "0193…",
"test": false,
"sent_at": "1730700000",
"record": {"id": "0193…", "table": "orders", "version": 1, "created_at": "…", "updated_at": "…", "fields": {"customer": "Ada", "total": 25}}
}
The record looks exactly like it does in the REST API (null
when the trigger has no record). The request has these headers:
| Header | Value |
|---|---|
X-Apptown-Timestamp |
when it was sent, in Unix seconds |
X-Apptown-Signature |
v1= + the hex HMAC-SHA256 of <timestamp>.<body>, keyed with the step's signing secret (shown on the step) |
X-Apptown-Event |
the trigger, e.g. record_updated |
Idempotency-Key |
the same as id: a retry sends the same value |
#Check the signature
Compute the HMAC of the timestamp, a dot and the raw body with your signing secret, compare it to the signature in constant time, and reject timestamps more than five minutes old.
In Python:
import hmac, hashlib, time
def valid(secret, timestamp, body, signature):
mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest("v1=" + mac, signature) and abs(time.time() - int(timestamp)) < 300
In Node.js:
import crypto from "node:crypto";
function valid(secret, timestamp, body, signature) {
const mac = crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
const a = Buffer.from(`v1=${mac}`), b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b) && Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}
#Answers and retries
Answer with any 2xx status. A 5xx, 429 or timeout is retried with the same id;
other 4xx answers fail the run without a retry. Use id (or Idempotency-Key) to
ignore a delivery you have already handled.