Get the app Map (opens in a new tab)List your business

Developers

Events

webhooks

How we send you batched, signed events, how to verify the signature, sequence numbers and de-duplication, the retry schedule, and the events log.

Coming soonThe Partner API is not open yet. These docs are here so you can plan your integration; hosts and keys are issued at launch.Register as a developer →

On this page

How it works

When something concerns you, such as a new order or a new link, we send a POST to the webhook endpoint you registered in the developer dashboard.

  • We send as soon as the event happens.
  • If events pile up, we batch up to 100 in one message.
  • Every message is signed with your webhook secret whsec_xxx.
  • You can register up to 5 endpoints per environment, each with the event types it wants.
  • Each endpoint receives its own copy of every event, with its own delivery_id and its own retries. One endpoint failing never delays another.

Endpoint rules

  • https only, on port 443, on a public internet address.
  • We refuse private addresses (10.x, 172.16.x to 172.31.x, 192.168.x), loopback (127.0.0.1, ::1), link-local (169.254.x, fe80::) and IP-literal hosts.
  • We check at registration, then on every send after DNS resolution.
  • We do not follow redirects.
  • An address refused at registration answers 400 URL_NOT_ALLOWED.

The message

bash
POST /aurmap/webhooks HTTP/1.1
Host: pos.example.com
Content-Type: application/json
X-AurMap-Delivery-Id: dlv_01JA9Y3K
X-AurMap-Timestamp: 1760445006
X-AurMap-Signature: v1=5f2c…e81a
json
{
  "delivery_id": "dlv_01JA9Y3K",
  "mode": "live",
  "sent_at": "2026-10-14T12:30:06Z",
  "events": [
    {
      "event_id": "evt_01JA9Y3J8Q",
      "type": "order.created",
      "seq": 18233,
      "occurred_at": "2026-10-14T12:30:05Z",
      "link_id": "lnk_2h6j4n",
      "place_id": "plc_9q3w7e",
      "entity": { "kind": "order", "id": "ord_7m1x9c", "version": 1 },
      "data": { "order": { "order_id": "ord_7m1x9c", "status": "received", "…": "…" } }
    }
  ]
}
Field Description
event_id unique event id; use it to drop duplicates
type the event type
seq one sequence number for your account and environment, shared by all your endpoints
entity.version the object’s version; grows with every change to it
data the object’s full current state, not just the difference

Verifying the signature

The signature is HMAC-SHA256 with your webhook secret over:

TIMESTAMP + "." + raw_body
  1. Read the body as it arrived, raw bytes, before parsing JSON.
  2. Reject the message if X-AurMap-Timestamp is older than 5 minutes.
  3. Compute the signature and compare in constant time.
  4. During secret rotation the header may carry two values: v1=aaa,v1=bbb. Accept the message if either matches.

Test vector: secret whsec_example_secret_do_not_use, timestamp 1760443260, body {"delivery_id":"dlv_01","events":[]}, signature 01b801fe5feb09fd7c745f38de67287ffba48a5e801ad2b058ba4bdd69934b32.

Node (Express)

js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.AURMAP_WEBHOOK_SECRET;

function verified(req) {
  const ts = req.get('X-AurMap-Timestamp') ?? '';
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac('sha256', SECRET).update(ts + '.').update(req.body).digest();
  return (req.get('X-AurMap-Signature') ?? '')
    .split(',')
    .map((p) => p.trim().replace(/^v1=/, ''))
    .some((sig) => /^[0-9a-f]{64}$/.test(sig) && crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected));
}

app.post('/aurmap/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verified(req)) return res.status(401).end();
  const { events } = JSON.parse(req.body.toString('utf8'));
  for (const event of events) {
    if (await alreadySeen(event.event_id)) continue;   // drop duplicates
    await queue.add(event);                            // process later, answer fast
  }
  res.status(200).end();
});

PHP

php
<?php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_AURMAP_TIMESTAMP'] ?? '';
$hdr = $_SERVER['HTTP_X_AURMAP_SIGNATURE'] ?? '';

if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}
$expected = hash_hmac('sha256', $ts . '.' . $raw, getenv('AURMAP_WEBHOOK_SECRET'));
$ok = false;
foreach (explode(',', $hdr) as $part) {
    $sig = preg_replace('/^v1=/', '', trim($part));
    if (hash_equals($expected, $sig)) {
        $ok = true;
    }
}
if (!$ok) {
    http_response_code(401);
    exit;
}

$payload = json_decode($raw, true);
foreach ($payload['events'] as $event) {
    if (already_seen($event['event_id'])) {
        continue;
    }
    enqueue($event);
}
http_response_code(200);

Ordering and duplicates

  • An event may reach you more than once. Store event_id and ignore what you have seen.
  • Within one message events are in ascending seq. But a retried message may arrive after a newer one.
  • seq has no gaps in the events log. An endpoint that receives only some event types sees gaps; that is normal and does not mean an event was lost.
  • So compare entity.version with what you hold. If it is not newer, ignore the event.
  • Each event carries the object’s full state, so the latest event is enough on its own.

Answering and retries

  • Answer 2xx within 10 seconds. That means the whole message arrived. Process the events after answering, not before.
  • Any other answer, or a timeout, means the whole message is retried.
Attempt After
2 30 seconds
3 2 minutes
4 10 minutes
5 30 minutes
6 1 hour
7 2 hours
8 and later every 4 hours

After 24 hours we stop sending, and the event stays in the events log. If an endpoint fails for 72 hours in a row we disable it and e-mail you; re-enable it from the dashboard.

An order’s accept window does not wait for retries. If your server was down, ask for waiting orders as soon as you are back.

The events log

If you missed something, read events after the last seq you processed:

bash
aurmap GET '/v1/events?after_seq=18200&limit=100&types=order.created,order.cancelled'
json
{ "data": [ { "event_id": "evt_01JA9Y3J8Q", "type": "order.created", "seq": 18233, "…": "…" } ], "next_after_seq": 18300, "has_more": true }

We keep events for 30 days, as they were delivered, then delete them. So the customer’s number stays masked in them unless the link holds customer:contact. Order events follow the same rule: phone_masked always, phone_e164 only with that scope.

After 7 days we mask customer data inside stored events, even with customer:contact. Keep what you need from an order when it arrives.

Event types

Type When
link.created the merchant approved a link code
link.updated scopes changed, or the menu source moved
link.removed the link was removed; carries removed_by
place.updated the name, location or opening hours changed
place.ordering_changed a pause, or a new accept window or payment methods
menu.changed a new menu version is visible to customers: a full publish, a structure patch or a state update, from you or from the merchant; carries version, prev_version, kind, hash
menu.removed the menu left the place; carries reason
order.created a new order
order.status_changed an order’s status changed, including your own changes
order.cancelled an order was cancelled; the reason is in cancel.reason, and a customer no-show is customer_no_show in cancel.detail
order.payment_updated a full refund (refunded) or a partial one (partially_refunded) with refunded_minor, or cash recorded
endpoint.test the dashboard’s “send test” button
catalog.products.processed a product batch finished Coming soon
offer.expired an offer ended Coming soon
accounting.day_closed an accounting day closed Coming soon
payout.created, payout.paid a payout to the merchant Coming soon

We may add new types. Ignore any type you do not know, and answer it with 200.

Example: menu.changed

json
{
  "event_id": "evt_01JA7Z0STAT",
  "type": "menu.changed",
  "seq": 18240,
  "occurred_at": "2026-10-14T18:00:00Z",
  "link_id": "lnk_2h6j4n",
  "place_id": "plc_9q3w7e",
  "entity": { "kind": "menu", "id": "plc_9q3w7e", "version": 43 },
  "data": { "place_id": "plc_9q3w7e", "prev_version": 42, "version": 43, "kind": "state", "hash": "a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1" }
}
  • kind: full, structure or state.
  • hash is the menu hash after the change. Keep it with version for the nightly reconcile.
  • If prev_version is not the last version you hold, you missed a change. Read the menu with GET /places/{place_id}/menu.
  • menu.removed carries place_id, version and reason: partner, link_removed, source_replaced or aurmap.