Instagram · Facebook · Threads
A webhook URL Meta will actually accept.
Meta checks the callback URL before it will save it, then stops trying after 36 hours if nobody acknowledges. AnyHook answers the check, stores every event, and keeps retrying your handler long after Meta has given up.
The timing problem
Meta sends a GET to your callback URL carrying hub.mode=subscribe and a challenge, and will not save the URL unless you echo that challenge back as the entire response body. A tunnel gets you past it once. The next time ngrok hands you a new hostname you are back in the App Dashboard doing it again, and the subscription you already made is pointing at nothing.
Once events start flowing the deadline is not per request, it is 36 hours. Meta retries with decreasing frequency over that window and then drops whatever was never acknowledged. For an auto-reply bot that window is the difference between answering a comment and never knowing it existed.
Point the callback at AnyHook, verify one header
The inbound URL never changes, so the App Dashboard becomes a one-time step. AnyHook checks Meta's X-Hub-Signature-256 at the edge, stores the event, then re-signs the delivery to your handler.
// 1. In AnyHook: set the app's source to Meta, paste your App Secret.
// Optionally set a Verify Token. You invent that string, it is not
// something you copy out of Meta.
//
// 2. In Meta App Dashboard -> Webhooks:
// Callback URL: https://in.anyhook.net/{user-slug}/{app-slug}
// Verify Token: the same string you chose
//
// AnyHook answers the GET handshake, so the URL saves first time.
import { verifyWebhook } from "anyhook-verify";
export async function POST(req: Request) {
const ok = await verifyWebhook(req, process.env.ANYHOOK_SIGNING_SECRET!);
if (!ok) return new Response("invalid signature", { status: 401 });
const event = await req.json();
const change = event.payload.entry?.[0]?.changes?.[0];
// Meta already has its acknowledgement. Take as long as you need here.
if (change?.field === "comments") {
await replyToComment(change.value); // an LLM call, a few seconds
}
return new Response("ok", { status: 200 });
}Questions teams ask
- Meta says my callback URL could not be validated. Why?
- That check is a GET carrying hub.mode=subscribe and hub.challenge, and it fails unless your URL echoes the challenge back as the whole body, with no JSON wrapper and no trailing newline. AnyHook answers it, so an inbound URL saves on the first try. If you set a Verify Token in AnyHook, type the same string into the App Dashboard.
- Do Instagram, Facebook and Threads need three separate setups?
- No. They are one Graph API webhook contract with the same X-Hub-Signature-256 signature, so a single AnyHook app covers all three. Choose which subscriptions you want in the App Dashboard.
- What happens to comments and DMs while my bot is redeploying?
- AnyHook stores the event the moment it arrives and retries your endpoint on backoff, so a deploy or a crash does not cost you the message. Meta sees one acknowledgement and stops worrying about it.
- Is the Verify Token the same as the App Secret?
- No, and they should not be the same string. The App Secret signs every payload and never travels in a URL. The Verify Token is one you make up, it arrives in the query string of the setup handshake, and it is only used at that moment. Putting your App Secret in a query string would spill it into every log along the way.
Change one URL. Keep your Instagram handler.
Free tier covers 3K events / month. No SDK, no code changes, just point Instagram at your AnyHook inbound URL.