EventsLocationsSpacesSolutionsPricingIntegrationsAPIBlogHelp
EventsLocationsSpacesSolutionsPricingIntegrationsAPIBlogHelp
© 2026 AOA
Getting startedArtistsFor artistsOrganizersVenuesPast eventsCareersPartnersBubble avatarBento pageGitHub
AboutWhat is AOAPrivacySecurityTermsMerchantCookies
Pricing
UAEN
PricingSupport
FOR PEOPLE
People nearbyHow it worksAOA ConnectSuspended coffee
FOR BUSINESS
VenuesOrganizersFor space ownersFor artists and bands
Solutions
Event check-inEvent networkingTelegram botBento pageLoyalty programSuspended coffeeNFC tags for venues
Extend AOA
IntegrationsAPI & SDK
Help centerDocumentationBlogAbout AOAContact

API reference

  • Introduction
  • Authentication
  • Sandbox
  • Rate limits
  • Errors
  • Versioning
  • Events
  • Reservations
  • Checkout

Webhooks

  • Introduction
  • Event types
  • Verifying signatures
  • Make, Zapier, n8n

Webhooks

Verifying signatures

How to make sure a request really comes from AOA: the HMAC-SHA256 AOA-Signature, a 5-minute time window, deduplication and examples in Node.js and Python.

Your handler URL will become known sooner or later: it shows up in proxy logs, browser history, screenshots. The signature is the only thing that tells our request apart from any other. Always verify it, before you even parse the body.

HTTP
AOA-Signature: t=1800000000,v1=5f2a9c...64-char-hex

t is the signing time in seconds, v1 is an HMAC-SHA256 of the string {t}.{body} with your secret. The scheme matches Stripe’s, so ready-made examples from there work almost unchanged.

Three rules

  • Use the raw body. The signature is computed over the bytes that arrived. If your framework has already parsed the JSON and you serialize it back, key order or whitespace will change and the signature won’t match.
  • Compare safely. timingSafeEqual instead of ===: differences in response time let an attacker guess the signature character by character.
  • Check the request’s age. Without it, a request intercepted once can be replayed at any time, since its signature stays valid forever. A sensible window is 5 minutes.

Node.js

JavaScript
import crypto from 'node:crypto';

const TOLERANCE_SECONDS = 300;

export function verifyAoaSignature(header, rawBody, secret) {
  const parts = header.split(',');
  const timestamp = parts.find((p) => p.startsWith('t='))?.slice(2);
  const signature = parts.find((p) => p.startsWith('v1='))?.slice(3);
  if (!timestamp || !signature) return false;

  // Protects against replaying an old request.
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');
  if (a.length !== b.length) return false;

  return crypto.timingSafeEqual(a, b);
}

Express

JavaScript
// The body must be RAW: express.json() changes the string and the signature won't match.
app.post(
  '/hooks/aoa',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const raw = req.body.toString('utf8');

    if (!verifyAoaSignature(req.get('AOA-Signature'), raw, process.env.AOA_WEBHOOK_SECRET)) {
      return res.status(401).end();
    }

    const event = JSON.parse(raw);

    // Respond right away, do the heavy work after the response.
    res.status(200).end();

    void handleEvent(event);
  },
);

Python

Python
import hmac, hashlib, time

TOLERANCE_SECONDS = 300

def verify_aoa_signature(header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    timestamp, signature = parts.get("t"), parts.get("v1")
    if not timestamp or not signature:
        return False

    if abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Deduplication

One event can arrive twice. The typical case: you processed the request, but the response didn’t reach us in time, so we retried. The AOA-Delivery-Id header stays the same on a retry, so store the identifiers you’ve processed and skip the ones you’ve seen.

You don’t need to keep them forever: retries last about nine hours, so keeping them for a day covers every retry with room to spare.

If the secret leaks

Delete the URL and register it again: a new secret is issued on creation. The old one stops working immediately, together with the deleted URL.

We don’t follow redirects

A signed request doesn’t follow 3xx: otherwise a changed redirect on your host would forward the signature and the buyer’s data to someone else’s server. Give the final URL straight away.
PreviousEvent typesNextMake, Zapier, n8n