> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Instagram

> Send and receive Instagram Direct Messages via the Orbit Messaging API

# Instagram

Instagram messaging connects your business to users via the Instagram Graph API. Orbit handles the OAuth onboarding, page-level permission scopes, and the async webhook fan-in for inbound DMs, story replies, and quick-reply postbacks.

A single Orbit account can attach multiple connected Instagram Business or Creator accounts; messages are billed per-conversation per Meta's pricing tiers.

## Send a message

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/instagram \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "<recipient_igsid>",
    "messaging_type": "RESPONSE",
    "message": { "text": "Thanks for reaching out! Our team will be in touch shortly." }
  }'
```

The body requires `to`, `messaging_type`, and `message`.

### `messaging_type`

Mirrors Meta's Messenger Platform messaging type and controls when a send is allowed:

| Value         | When to use                                                                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RESPONSE`    | Replying to a user inside the 24-hour messaging window.                                                                                                  |
| `UPDATE`      | A proactive, non-promotional update inside the messaging window.                                                                                         |
| `MESSAGE_TAG` | Sending outside the 24-hour window. Requires a top-level `tag` (e.g. `HUMAN_AGENT`, `ACCOUNT_UPDATE`, `POST_PURCHASE_UPDATE`, `CONFIRMED_EVENT_UPDATE`). |

### `message`

The `message` object carries the content. Provide **either** `text` **or** an `attachment`:

```jsonc theme={null}
// Text
{ "to": "<recipient_igsid>", "messaging_type": "RESPONSE", "message": { "text": "Hello!" } }

// Media attachment — type is one of image | audio | video | file
{
  "to": "<recipient_igsid>",
  "messaging_type": "RESPONSE",
  "message": {
    "attachment": {
      "type": "image",
      "payload": { "url": "https://cdn.example.com/welcome.png" }
    }
  }
}
```

Optional top-level fields: `tag` (required when `messaging_type` is `MESSAGE_TAG`), `metadata` (a string→string map), and `scheduled_at` (ISO-8601 timestamp). There is no top-level `type`, `text`, or `from` field — the connected Instagram account is resolved from your API key.

### Generic templates

Instagram's only structured outbound payload is the **generic template** — a horizontally scrollable carousel of cards, each with an image, title, subtitle, and buttons. Button and receipt templates are Messenger-only and are not supported on Instagram.

Because `metadata` is a string→string map, a generic template is requested through two metadata keys rather than a dedicated `message` shape:

| `metadata` key  | Value                                                                  |
| --------------- | ---------------------------------------------------------------------- |
| `template_type` | The string `generic`.                                                  |
| `elements`      | A JSON-encoded **string** holding the array of template card elements. |

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/instagram \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "<recipient_igsid>",
    "messaging_type": "RESPONSE",
    "message": { "text": "Here are your order options" },
    "metadata": {
      "template_type": "generic",
      "elements": "[{\"title\":\"Order #1024\",\"subtitle\":\"Ships tomorrow\",\"image_url\":\"https://cdn.example.com/order.png\",\"buttons\":[{\"type\":\"web_url\",\"url\":\"https://example.com/orders/1024\",\"title\":\"Track\"}]}]"
    }
  }'
```

If `elements` is missing or is not valid JSON, the send falls back to the plain `message.text` body.

### Response

```json theme={null}
{
  "data": {
    "id": "msg_ig_abc123",
    "status": "queued",
    "channel": "instagram"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-05-09T00:00:00Z"
  }
}
```

## Capabilities

* **Text + media** — text, image, video, audio, file attachments (up to Meta's per-type size caps).
* **Quick-reply postbacks (inbound)** — when a user taps a quick-reply button you presented to them, the tap is delivered as a `message.received` webhook event with `quick_reply.payload` populated. Attaching quick-reply buttons to an outbound DM is not currently supported by the send API.
* **Generic templates** — multi-card carousels with image, title, subtitle, and buttons. Triggered on a send via `metadata` fields — see [Generic templates](#generic-templates) below.
* **24-hour messaging window** — inside the standard messaging window, replies are free; outside the window, only `MESSAGE_TAGS` (HUMAN\_AGENT, ACCOUNT\_UPDATE, POST\_PURCHASE\_UPDATE, CONFIRMED\_EVENT\_UPDATE) can be used.
* **Story mention + story reply** — delivered as standard `message.received` webhook events. A story mention carries `metadata.story_mention.url`; a story reply carries `metadata.story_reply` (`url` and/or `id`). There are no dedicated `instagram.story.*` event types — subscribe to `message.received` and branch on the `metadata` fields.
* **Inbound webhooks** — DMs and postbacks are POSTed to your registered webhook endpoint. Because Instagram messaging runs on Meta's platform, these callbacks are signed with Meta's `X-Hub-Signature-256` header (the same scheme as WhatsApp), *not* the Standard Webhooks / Svix scheme Orbit uses for its own outbound webhooks. The header value is `sha256=<hex>`, where `<hex>` is `HMAC-SHA256(rawBody)` keyed with your Meta app secret. Verify it by recomputing the HMAC over the raw request body and comparing with a timing-safe check; requests with a missing or invalid signature are rejected with `401 INVALID_SIGNATURE`.

## Rate limits

Meta enforces a **200 DM / hour limit per Instagram account** (reduced from 5000 in February 2026). Orbit runs a per-tenant, Redis-backed sliding-window counter ahead of every send so you stay under Meta's quota and avoid `RATE_LIMIT_EXCEEDED` (429) responses and account restrictions. When a send would exceed the window it is rejected with `RATE_LIMIT_EXCEEDED` before it ever reaches Meta.

The hourly ceiling is operator-tunable **without a redeploy** — set the env var (Secret Manager / cluster env) and restart the pod:

| Env var                          | Default | Description                                                                                                                                                                                                                  |
| -------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEVOTEL_INSTAGRAM_HOURLY_LIMIT` | `200`   | Maximum outbound Instagram DMs allowed per tenant in any rolling 1-hour window. Lower it if Meta tightens the quota for your app tier; raising it above Meta's actual limit will not help — Meta still enforces its own cap. |

## Onboarding flow

1. From the dashboard, navigate to **Channels → Instagram**.
2. Click **Connect Instagram** and authorise via Meta's OAuth dialog. The dialog requests `instagram_basic`, `instagram_manage_messages`, `pages_messaging`, and `pages_show_list` scopes.
3. Select the Instagram Business / Creator account you want to attach.
4. Verify the inbound webhook subscription registered automatically; the webhook URL points to `https://api.orbit.devotel.io/api/v1/webhooks/inbound/instagram`.

## Common errors

The `error.code` you can branch on is one of three values. Meta-side rejections (expired token, outside the messaging window, non-Business account, opted-out recipient) all surface as `MESSAGE_SEND_FAILED` — Orbit does not re-expose Meta's raw error string, so inspect `details.provider_status` for the upstream HTTP status to tell those cases apart.

| Code                  | HTTP      | Cause                                                                                                                                                                                                                                                                                | Fix                                                                                                                                                                                                                   |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_RECIPIENT`   | 422       | The `to` field is missing or empty — a recipient IGSID/PSID is required.                                                                                                                                                                                                             | Send a non-empty `to`. Use the user's Instagram-scoped ID from an inbound `message.received` webhook.                                                                                                                 |
| `RATE_LIMIT_EXCEEDED` | 429       | The send would exceed the per-tenant hourly Instagram quota (200/hour by default; see [Rate limits](#rate-limits)).                                                                                                                                                                  | Back off and retry later; for high volume, batch via the campaigns API.                                                                                                                                               |
| `MESSAGE_SEND_FAILED` | 4xx / 502 | Meta rejected the send or its Graph API was unreachable. Covers an expired/revoked page token, a send outside the 24-hour window without a valid `tag`, a non-Business/Creator account, and a blocked or opted-out recipient. `details.provider_status` carries the upstream status. | Reconnect via **Channels → Instagram → Reconnect** if the token expired; use a `MESSAGE_TAG` for sends outside the window; convert the account to Business/Creator in Instagram settings if it is a personal profile. |

## Pricing

Pay-as-you-go conversation pricing per Meta's Messaging Tiers. International destinations vary; the dashboard shows the exact rate per destination before send. See the [pricing page](https://orbit.devotel.io/pricing) for public starter rates.
