Skip to main content
Send Instagram DMs with the Messenger-style recipient / message body, pointed at the HookMyApp gateway. The gateway swaps your token for the real Meta credential server-side, so your Meta token never leaves HookMyApp.

The basic call

Every send is a POST to ${INSTAGRAM_GRAPH_API_URL}/${INSTAGRAM_USER_ID}/messages with Authorization: Bearer ${INSTAGRAM_ACCESS_TOKEN} and a JSON body of the form { "recipient": { "id": "<igsid>" }, "message": { "text": "..." } }. Both the sandbox and your own account go through the HookMyApp gateway. The base URL is the only difference: the sandbox env sets INSTAGRAM_API_URL + INSTAGRAM_ACCOUNT_ID, your own account sets INSTAGRAM_GRAPH_API_URL + INSTAGRAM_USER_ID, and both point at the gateway (<gateway>/<graph-version>). The token is a scoped HookMyApp gateway token (hmat_…), not your raw Meta token; the gateway authenticates it, swaps in the real Meta token, and forwards to Meta. The recipient id is the Instagram-scoped sender id (IGSID) you read off the inbound webhook. For the full message-object reference see Meta’s Instagram messaging docs.

curl example

curl -X POST "${INSTAGRAM_GRAPH_API_URL}/${INSTAGRAM_USER_ID}/messages" \
  -H "Authorization: Bearer ${INSTAGRAM_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": { "id": "17841400000000000" },
    "message": { "text": "Hello from my app" }
  }'

Node/Express example

Verbatim from the webhook starter kit (src/providers/instagram.js). It reads whichever base-URL and account-id variables your env has (sandbox sets INSTAGRAM_API_URL + INSTAGRAM_ACCOUNT_ID; your own account sets INSTAGRAM_GRAPH_API_URL + INSTAGRAM_USER_ID), so the same code runs against both.
export async function send(to, text) {
  const base = process.env.INSTAGRAM_API_URL ?? process.env.INSTAGRAM_GRAPH_API_URL;
  const accountId = process.env.INSTAGRAM_ACCOUNT_ID ?? process.env.INSTAGRAM_USER_ID;
  const url = `${base}/${accountId}/messages`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.INSTAGRAM_ACCESS_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ recipient: { id: to }, message: { text } }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(`Instagram API error ${res.status}: ${JSON.stringify(err)}`);
  }
  return res.json();
}

The 24-hour messaging window

You can reply within 24 hours of the user’s last message. Instagram enforces a standard messaging window. Outside the window, a plain text reply is rejected. Plan your flows to respond inside the window, or use a message tag where one applies.

Sandbox versus your own account

Both run through the same HookMyApp gateway with your hmat_… token; only the env keys differ. The sandbox sets INSTAGRAM_API_URL + INSTAGRAM_ACCOUNT_ID; a real connected channel sets INSTAGRAM_GRAPH_API_URL + INSTAGRAM_USER_ID. The request body shape is identical. See Sandbox for the sandbox env keys.

Next steps

  • Receive webhooks: Handle the inbound message and read the IGSID.
  • Sandbox: Try the send flow end-to-end with a test account.