Connecting accounts

You cannot publish until a social account is connected. Some platforms you can connect outright from the API; the rest need a human in a browser, and we give you a URL to send them to.

Ask what a platform needs

Never hard-code this — ask, because it varies per platform and can change:

curl "https://vm.viraldashboard.io/api/public/v1/brands/412/social-accounts/platforms" \
  -H "Authorization: Bearer $VD_API_KEY"
{
  "data": [
    {
      "platform": "telegram",
      "mode": "self_connect",
      "connect_method": "credentials",
      "credential_fields": [
        { "name": "bot_token", "label": "Bot token", "type": "password", "required": true },
        { "name": "chat_id",   "label": "Channel / chat id", "type": "text", "required": true }
      ]
    },
    {
      "platform": "facebook",
      "mode": "oauth",
      "connect_method": "authorize_url",
      "credential_fields": []
    }
  ]
}

connect_method tells you which of the two paths below to take.

Path A — credentials (no browser)

Platforms whose connect_method is credentials connect in a single call. Post the fields the discovery endpoint listed:

curl -X POST "https://vm.viraldashboard.io/api/public/v1/brands/412/social-accounts" \
  -H "Authorization: Bearer $VD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "telegram",
    "credentials": { "bot_token": "8012345678:AAH…", "chat_id": "@acmecoffee" }
  }'

A 201 means the account is connected and ready to publish to. Bad credentials come back as 422 invalid_credentials — we check them against the platform before saving, so a 201 genuinely means it works.

Path B — OAuth (a human, in a browser)

Facebook, Instagram, LinkedIn, X and the rest cannot be connected from a server: the platform demands that a human log in and consent. So you ask us for a URL, and you get that URL in front of the right person.

curl -X POST "https://vm.viraldashboard.io/api/public/v1/brands/412/social-accounts/authorize" \
  -H "Authorization: Bearer $VD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "facebook", "target": "page" }'
{
  "data": {
    "platform": "facebook",
    "authorization_url": "https://…/social/connect/facebook?state=…",
    "expires_in": 1800
  }
}

That URL is a secret, and it is a capability. Anyone who opens it can attach their social account to your brand — no login to ViralDashboard required, which is exactly what makes it useful for onboarding a client who has no dashboard account. Treat it like a password reset link:

  • Single use. The first person to complete it consumes it; a second attempt is rejected.
  • Short-lived. It expires after expires_in seconds (30 minutes).
  • Send it only to the intended person, over a channel you trust. Never log it, never put it in a URL you render publicly, never email it to a list.

Knowing when it worked

The person authorizes in their browser, and the flow completes without ever signing them into your dashboard. Your side gets nothing back synchronously — the completion signal is the account.connected webhook. Subscribe to it before you send the link:

{
  "event": "account.connected",
  "data": {
    "brand_id": 412,
    "social_account_id": 3311,
    "platform": "facebook"
  }
}

If you would rather not run a webhook listener, poll GET /brands/{brand}/social-accounts and watch for the new account to appear — but the webhook is the cheaper and faster signal.

Capacity is checked before the URL is issued

If the brand is already at its social_accounts plan cap, authorize returns 402 limit_reached and no URL is issued. That is deliberate: it would be worse to hand someone a link, let them log in and consent, and only then tell them there was no room.

Keeping accounts alive

CallDoes
POST /social-accounts/{account}/refresh Refreshes the token. 409 reconnect_required means it cannot be refreshed and a human must reconnect.
DELETE /social-accounts/{account} Disconnects. If the account is shared with other brands it is only detached (result: detached); if this was its last brand, the token is revoked and the account deleted (result: revoked). Fires account.disconnected.

Watch each account's status. Only active accounts publish; expired means a token went stale and the next publish will fail until it is refreshed or reconnected.

Mastodon and Shopify need an extra in-app step (you must supply your instance domain before the OAuth handshake can even start), so they cannot yet be connected purely over the API — they return 422 interactive_setup_required. Connect them in the dashboard for now.