Instagram DMs
Connects an Instagram professional account directly via Instagram Login (no Facebook Page required) so DMs sync to the inbox.
- API:
apps/api/src/instagram/(prefix/v1/instagram) - SDK:
@branofy/api/instagram - UI: the "Instagram DMs" section of
settings/integrations/meta/page.tsx - Schema:
instagram_connections
See README.md for the shared OAuth model, signed state, and env vars.
Endpoints
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| GET | /v1/instagram/connect | session/api‑key | Returns { url } — the Instagram OAuth authorize URL. |
| GET | /v1/instagram/callback | public | Exchanges code → long‑lived (~60d) token → stores account → subscribes messages → 302 to settings. |
| GET | /v1/instagram/status | session/api‑key | { connected, accounts: [{ id, igUserId, igUsername, connectedAt, tokenExpiresAt, daysUntilExpiry }] }. |
| DELETE | /v1/instagram/status?igUserId= | session/api‑key | Disconnect an account. |
Connect flow
sequenceDiagram
actor U as User (browser)
participant A as NestJS API (/v1/instagram)
participant O as api.instagram.com
participant G as graph.instagram.com
U->>A: GET /v1/instagram/connect → { url }
U->>O: redirect to /oauth/authorize → consent
O-->>A: GET /v1/instagram/callback ?code & state
A->>A: verify state
A->>O: POST /oauth/access_token (short-lived)
A->>G: GET /access_token (ig_exchange_token → ~60d)
A->>G: GET /me?fields=id,username,user_id
A->>A: upsert instagram_connections (by ig_user_id)
A->>G: POST /me/subscribed_apps (messages)
A-->>U: 302 → /settings/integrations/meta?ig=connected&username=...
- UI fetches
/api/instagram/connect, redirects toinstagram.com/oauth/authorize. - Provider redirects to
{API_PUBLIC_URL}/v1/instagram/callback?code&state. - Callback exchanges the code at
api.instagram.com/oauth/access_token(short token) thengraph.instagram.com/access_token(long‑lived, ~60 days), reads the profile (id,username,user_id), upsertsinstagram_connections, and subscribesme/subscribed_apps?subscribed_fields=messages. - Redirect →
{APP}/settings/integrations/meta?ig=connected&username=....
OAuth scopes
instagram_business_basic, instagram_business_manage_messages.
Credential resolution
INSTAGRAM_APP_ID/INSTAGRAM_APP_SECRET → workspace instagram_app_* settings →
the Meta app credentials. (Instagram Login uses the Instagram product's app id,
which may differ from the base Facebook app id.)
Reconnecting
flowchart TD
R[connect runs again] --> C{ig_user_id exists?}
C -- yes --> U[UPDATE row:<br/>new accessToken<br/>tokenExpiresAt = now + ~60d<br/>refresh igUsername]
C -- no --> I[INSERT new account<br/>multiple IG per workspace OK]
U --> S[re-subscribe messages]
I --> S
subgraph Lifecycle
T[token nears expiry] --> W["UI: daysUntilExpiry ≤ 7<br/>→ amber 'reconnect' prompt"]
W --> R
end
The connection is keyed by ig_user_id (unique). Reconnecting the same
account updates the existing row — new accessToken, a fresh
tokenExpiresAt (~60 days out), and the latest igUsername — rather than
inserting a duplicate.
Because long‑lived tokens expire in ~60 days, the UI shows daysUntilExpiry and
turns amber at ≤ 7 days, prompting the user to reconnect to refresh the token.
Connecting a different account inserts a new row (multiple IG accounts per
workspace are supported).
Troubleshooting
| Symptom | Cause / fix |
| --- | --- |
| ig=error&reason=not_configured | No Instagram/Meta app id — set INSTAGRAM_APP_* or the Meta app. |
| ig=error&reason=invalid_state | State expired (> 1h) or tampered — restart. |
| Token nearing expiry (amber card) | Click connect again; the row is updated in place. |
| DMs not arriving | Account must be Professional and the messages subscription active — reconnect re‑subscribes. |