Docs
Webhooks

Local testing

Receive Agoo webhooks on your own machine while you build, with a tunnel, test events, the planned Agoo CLI or requests you sign yourself.

Preview· P9

This is a published preview. Names and fields may still change before general availability; changes will be listed in the changelog.

Agoo delivers webhooks only to public HTTPS URLs, so it can't reach http://localhost:3000 directly. While you build, use one of these approaches:

ApproachGood forAvailable
A tunnel to your machineReal deliveries, retries and test events end to endWith the API (P9)
Test eventsChecking signature handling on demandWith the API (P9)
The Agoo CLIForwarding test-mode events without a public URLPlanned
Requests you sign yourselfUnit tests and offline work, with no Agoo accountAny time

Do all of this in test mode, with a test key and a test endpoint. Test events never touch real visitors.

Use a tunnel

A tunnel gives your local server a temporary public HTTPS address. Any tunnelling tool works; two common ones:

cloudflared tunnel --url http://localhost:3000

It prints an address such as https://<random-words>.trycloudflare.com.

Then register that address as a test-mode endpoint, using a test key:

curl https://api.agoo.ardent.africa/v1/webhook-endpoints \
  -H "Authorization: Bearer $AGOO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://YOUR-TUNNEL-ADDRESS/webhooks/agoo",
    "description": "Local development",
    "enabled_events": ["*"]
  }'

Save the secret from the response as AGOO_WEBHOOK_SECRET for your local server. Free tunnel addresses usually change each time you start the tunnel; update the endpoint with PATCH/webhook-endpoints/{whep_id} and { "url": "…" } rather than creating a new one, so the secret stays the same.

To make events happen, drive test visits through their life with the API (see environments), or send a test event.

Send a test event

POST/webhook-endpoints/{whep_id}/test sends one signed event of the type you choose straight away, with a sample object, and returns your endpoint's response:

curl -X POST https://api.agoo.ardent.africa/v1/webhook-endpoints/whep_01m2jf0cg0ffyrmjbzsk9t0gtc/test \
  -H "Authorization: Bearer $AGOO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "type": "booking.created" }'
Response
{
  "endpoint_id": "whep_01m2jf0cg0ffyrmjbzsk9t0gtc",
  "event_id": "evt_01m4wwgwc0f8y8gskxtb18r86q",
  "type": "booking.created",
  "attempted_at": "2026-10-14T09:40:00Z",
  "outcome": "succeeded",
  "status_code": 200,
  "duration_ms": 182,
  "error": null
}

Test events are sent once, aren't retried and don't appear in GET /events. They work on disabled endpoints too, so you can check a fix before you re-enable one.

Forward events with the Agoo CLI

Planned

The Agoo CLI (@ardent-africa/agoo-cli) is planned to ship with or after the API in phase P9. The commands below show how it is designed to work.

agoo listen will receive your organisation's test-mode events and forward them to a URL on your machine, signed like real deliveries, with no tunnel or public URL:

agoo login
agoo listen --forward-to http://localhost:3000/webhooks/agoo

It prints a signing secret for the session; use it as AGOO_WEBHOOK_SECRET while agoo listen runs. In another terminal, agoo trigger makes an event happen in test mode:

agoo trigger visit.checked_in

See the CLI reference for every command and flag.

Sign requests yourself

For unit tests, or to work without an Agoo account or network, sign a saved payload with any whsec_ secret your local server is configured with, and post it. Copy a payload from the event catalogue or an event's page in the API reference into event.json.

sign-and-send.mts
// Usage: node sign-and-send.mts event.json [url]
import { createHmac } from "node:crypto"
import { readFileSync } from "node:fs"

const secret = process.env.AGOO_WEBHOOK_SECRET! // the same secret your local server verifies with
const body = readFileSync(process.argv[2] ?? "event.json") // the exact bytes to send
const url = process.argv[3] ?? "http://localhost:3000/webhooks/agoo"

const id: string = JSON.parse(body.toString("utf8")).id
const timestamp = Math.floor(Date.now() / 1000).toString()
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
const signature = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest("base64")

const res = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "webhook-id": id,
    "webhook-timestamp": timestamp,
    "webhook-signature": `v1,${signature}`,
  },
  body,
})
console.log(res.status, await res.text())
node sign-and-send.mts event.json

What to test

Before you go live, check that your endpoint:

  • accepts a correctly signed request and returns 2xx within 15 seconds;
  • rejects a request whose body was changed by one character, with 401;
  • rejects a request whose webhook-timestamp is more than 5 minutes old;
  • processes the same webhook-id only once when it arrives twice;
  • copes with events out of order, such as visit.checked_out before visit.checked_in;
  • returns 2xx for an event type it doesn't handle;
  • still verifies during a secret rotation, when webhook-signature holds two signatures.

On this page