BetaappTown is live but still in beta: things may change while we finish it.
appTown Docs Open appTown

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 Accepted and 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.