Instagram (superadmin)
Experimental Instagram inbox for Patmos operators — not a merchant CRM feature
Last updated: 2026-09-03
Instagram inbox (experimental)
This is a Patmos superadmin experiment. It does not appear in Company settings, merchant CRM, or cashier POS.
Who can use it
Only users in the Sarah superadmin table. Shop admins and cashiers are redirected away from /superadmin/instagram and receive 403 on the inbox APIs. Those APIs prove the cookie JWT with the anon-key SSR client, then check the superadmin table — they do not treat the x-user header as the session.
Enablement is Connect Instagram (a connected_channels row). There is no env list of company ids and no hardcoded shop.
Meta apps
Connect is Instagram Login (https://www.instagram.com/oauth/authorize) with client_id = Patmos Channels Instagram App ID (INSTAGRAM_APP_ID=1223099003236295). That id is the Instagram product of the shared Patmos Channels connector (legacy dashboard name Landscape-IG, being renamed). It is not Landscape-the-product.
The Facebook app (META_APP_ID=1386194269924434) is Facebook Login / WhatsApp only. Meta rejects it as Instagram Login client_id.
The dialog requests instagram_business_basic and instagram_business_manage_messages. Token exchange uses INSTAGRAM_APP_SECRET if set, otherwise META_APP_SECRET.
The authorize URL always sends force_reauth=true and enable_fb_login=false so Connect uses Instagram professional credentials instead of a Facebook-linked personal account.
@sarah.patmos has no Facebook Page and does not need one.
URLs on Patmos Channels Instagram Login
| Use | URL |
|---|---|
OAuth callback (prod NEXT_PUBLIC_URL) | |
| OAuth callback (apex, also allowlisted) | |
These sit alongside landscap.app on the same Instagram Login settings.
Do not change the Patmos Channels webhook callback URL to sarah.ar if it currently points at landscap.app — that would break Landscape. After Connect, Sarah reads the profile with official
GET graph.instagram.com/{version}/me?fields=user_id,username,idid is IGSID, user_id is the professional 1784… id). If /me 400s, it falls back to GET /{ig-user-id} using the stored IGSID. DMs are polled at graph.instagram.com/{version}/{professional-user-id}/conversationsuser_id is stored — not /{IGSID}/conversations and not /me/conversations. The conversations list poll is slim (participants + last-message preview). Opening a thread requests messages with attachments{id,mime_type,name,size} (Instagram Login fields; not Facebook Page platform=instagram). Every Graph GET aborts after 8 seconds, including the response body read. The inbox API returns { conversations } from the database first and schedules Graph sync with after() (empty array is ok). The panel aborts the list fetch after 15 seconds, shows stored threads or “Todavía no hay conversaciones” instead of staying on “Cargando…”, and retries the list once if the first paint is empty. The connection block is compact (account, Connect, read-only; callback/webhook in a details row). The inbox fills remaining height: narrow list + message pane with a participant header. It is read-only: there is no composer; replies stay off pending Meta App Review. Opening the inbox refreshes the profile only when the professional id is not already stored, so an existing Connect does not need to connect again. The panel only shows threads for the currently connected Instagram channel. Each bubble is labeled: outbound is the connected @handle, inbound is the conversation participant. Audio DMs play through GET /api/superadmin/instagram/attachments?id= (superadmin cookie only; Graph file_url is resolved with the channel token and streamed — the browser never receives access_token). The Sarah route /api/webhooks/meta still verifies HMAC if Meta posts to it.
Env
See instagram-inbox.env.example. Instagram Login client_id is INSTAGRAM_APP_ID (default 1223099003236295). Token secret is INSTAGRAM_APP_SECRET or META_APP_SECRET. Also required: META_WEBHOOK_VERIFY_TOKEN, NEXT_PUBLIC_URL. META_IG_SEND_ENABLED defaults false.
Webhook HMAC (X-Hub-Signature-256) uses META_APP_SECRET and is fail-closed (403). Persist failures after a valid HMAC return 500. GET hub.challenge returns the challenge string.
Patmos tester account (not a code gate): @sarah.patmos, IG user id 17841470676628629.
WhatsApp stays on Twilio (/api/whatsapp). This inbox does not replace it.