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=...
    
    1. UI fetches /api/instagram/connect, redirects to instagram.com/oauth/authorize.
    2. Provider redirects to {API_PUBLIC_URL}/v1/instagram/callback?code&state.
    3. Callback exchanges the code at api.instagram.com/oauth/access_token (short token) then graph.instagram.com/access_token (long‑lived, ~60 days), reads the profile (id, username, user_id), upserts instagram_connections, and subscribes me/subscribed_apps?subscribed_fields=messages.
    4. 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. |