Inbound hook, webhook, or API key?
Three things in Settings → API sound alike and do opposite things. The quickest way to choose is to ask who starts it:
The deciding question is whether you can hand the other side a key. If you can, issue an API key with exactly the scopes it needs — you get permissions, rate limits, and revocation. If you can’t, because their console has one field and it says URL, that’s what an inbound hook is for.
Create a hook
1
Open Settings → API
The Inbound hooks card sits between Connected agents and Webhooks. This section is for owners and admins only.
2
Click New hook
Name it after the service that will call it — “Payfast payment notifications”, “Calendly”, “website order form”. The name is how you tell hooks apart afterwards, and your bot sees it too.
3
Copy the URL
Paige mints one and shows it to you straight away. It looks like
https://api.paigeme.dev/hooks/<secret>.4
Paste it into the other service
Find its notification, callback, or “webhook” setting and paste the URL in. There are no headers to configure and nothing to sign up for on their side.
Copying it again, and switching it off
Unlike an API key or a webhook signing secret, a hook URL is not show-once.- Show URL displays and copies it again, any time. Useful when you’re setting up a second service or re-entering it after changing providers. The URL doesn’t change.
- Rotate is how you invalidate one. You get a fresh URL and the old one stops working the moment you confirm — so paste the new one into the provider afterwards, or their notifications stop arriving.
- The on/off switch pauses a hook without replacing it. While it’s off the row reads Off — requests are refused, and anything sent to it is turned away until you switch it back on.
- Delete removes it for good.
Checking that requests are arriving
Each hook shows when it last received something, so “is this working?” is answerable at a glance. Expand Recent activity and you’ll see the 20 most recent arrivals — when each one came in, and whether your bot handled it or hit an error.
That’s usually enough to tell the two common problems apart: nothing listed at all means the other service isn’t sending — re-check the URL you pasted on their side, and make sure the hook isn’t switched off. Arrivals that show an error mean it is reaching you and your bot’s handling is what needs fixing.
Recent activity is a rolling troubleshooting view, not a permanent record. Arrivals are cleared out after about a month.
hook:<name>. That’s where to look when arrivals are landing but the bot isn’t doing what you expected.
Handling it in your bot
Paige delivers the request to one file in your project:src/routes/inbound.js, which exports a function called handleInbound. Paige creates that file for you the first time you mint a hook, with a commented scaffold to fill in — and never overwrites it afterwards, because from then on it’s your code.
handleInbound is called once per request, with everything about it:
Whatever you return is recorded on the arrival receipt in Settings. It is not what the provider sees — Paige answers them
200 the moment the request is accepted, before your bot runs. Throwing marks the receipt failed with your error message, which is the outcome you want for a request you’ve decided not to trust.
Inside the handler you have the whole bot — your services, your database helpers, your message senders — so you can look the customer up, write to a table, and message them on WhatsApp from right there.
Your bot decides whether to trust the sender
This is the one thing worth understanding properly. Paige checks that the URL is valid, logs the arrival, and passes on whatever came in — exactly as it came in — without opening it, changing it, or vouching for who sent it. Paige has no idea whether a request claiming to be a successful payment really came from your payment provider. Put the provider’s signing secret in Tools → Secrets and read it withprocess.env. Never hard-code it, and never log a secret or the hook URL.
Verify against rawBody, never JSON.stringify(body)
An HMAC-SHA256 header, which is what Stripe, Shopify, Slack, and GitHub use:
timingSafeEqualHex ships in the scaffold Paige creates for you, so it’s already there in the same file. A plain === exits at the first differing character, which leaks — over many attempts — how much of a forged signature was correct.
Payfast’s ITN is the MD5 variant of the same idea: rebuild the parameter string from the posted fields in the order they arrived, drop the signature field, URL-encode each value, append your passphrase if you set one, then MD5 it and compare. Rebuilding that string from rawBody rather than from a parsed object is what makes it work.
Good to know
- Only
POSTrequests are delivered. JSON, form-encoded, and plain-text bodies are all accepted, up to 128 KB. - Your hook runs your deployed code. Editing
src/routes/inbound.jsisn’t live until you deploy — until then the live hook keeps running the previously deployed handler. - Sub-paths work. Anything you add after the secret —
…/hooks/<secret>/payments/success— arrives aspath, so one hook can serve several callbacks from the same provider. - Paige always answers
200on acceptance. Providers treat anything else as “retry”, so an error inside your bot never turns into a retry storm. Your handler’s outcome lands on the receipt instead. - Rejections are deliberately uninformative. An unknown, rotated, or switched-off URL all return the same
404, so nobody can probe for which is which. - Hooks are rate limited. A hook being hit far faster than any real provider would sends requests to
rejectedrather than to your bot.
