Quickstart

From an empty terminal to a post published on a real social account. Five calls, about ten minutes.

You will need an account with at least one connected social account and a plan that includes API access. If you have no account connected yet, do Connecting accounts first — you can do that over the API too.

1. Mint a key

In the dashboard go to Settings → API & Webhooks → API Keys and create a key. Give it the scopes this guide uses: brands:read, accounts:read, posts:read and posts:write.

The secret is shown once. When you close that dialog it is gone — we store only a hash. Copy it somewhere safe now. If you lose it, delete the key and mint another.

Keep it in your environment, not in your code:

export VD_API_KEY="vd_live_…"
export VD_API="https://vm.viraldashboard.io/api/public/v1"

A key beginning vd_test_ is a test key: it can read everything but every write returns 403 test_mode_write_forbidden. To publish, you need a vd_live_ key.

2. Find your brand

Everything in the API hangs off a brand, so start by listing the brands your key can reach.

curl "$VD_API/brands" \
  -H "Authorization: Bearer $VD_API_KEY"
{
  "data": [
    {
      "id": 412,
      "name": "Acme Coffee",
      "slug": "acme-coffee",
      "timezone": "Europe/London"
    }
  ]
}

Take the id412 here. Every path below is nested under it.

You will only ever see brands your key's user is a member of. Asking for someone else's brand returns 403 brand_forbidden, not an empty list — the API tells you that you were denied rather than pretending the brand does not exist.

3. Pick an account to publish to

curl "$VD_API/brands/412/social-accounts" \
  -H "Authorization: Bearer $VD_API_KEY"
{
  "data": [
    {
      "id": 3310,
      "platform": "telegram",
      "name": "testtelegramo",
      "status": "active"
    }
  ]
}

Publish only to accounts whose status is active. An expired account needs reconnecting before it will accept a post.

4. Publish a post

This is a write, so it needs an Idempotency-Key: any unique string you generate. It makes the call safe to retry — if the response never reaches you, sending the identical request again returns the original result instead of publishing twice.

curl

curl -X POST "$VD_API/brands/412/posts" \
  -H "Authorization: Bearer $VD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello from the ViralDashboard API 🚀",
    "schedule_type": "now",
    "accounts": [3310]
  }'

Node

import { randomUUID } from 'node:crypto'

const res = await fetch(`${process.env.VD_API}/brands/412/posts`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.VD_API_KEY}`,
    'Idempotency-Key': randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    content: 'Hello from the ViralDashboard API 🚀',
    schedule_type: 'now',
    accounts: [3310],
  }),
})

if (!res.ok) {
  const { error } = await res.json()
  throw new Error(`${error.code}: ${error.message}`)
}

const { data: post } = await res.json()
console.log(post.id, post.status)

PHP

use Illuminate\Support\Facades\Http;

$response = Http::withToken(env('VD_API_KEY'))
    ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
    ->post(env('VD_API').'/brands/412/posts', [
        'content' => 'Hello from the ViralDashboard API 🚀',
        'schedule_type' => 'now',
        'accounts' => [3310],
    ]);

if ($response->failed()) {
    $error = $response->json('error');
    throw new RuntimeException("{$error['code']}: {$error['message']}");
}

$post = $response->json('data');

You get back 201 and the post, including the per-account variant it just created:

{
  "data": {
    "id": 1000241,
    "content": "Hello from the ViralDashboard API 🚀",
    "status": "publishing",
    "platforms": ["telegram"],
    "variants": [
      {
        "id": 5521,
        "social_account_id": 3310,
        "platform": "telegram",
        "account_name": "testtelegramo",
        "status": "pending"
      }
    ]
  }
}

5. Watch it land

Publishing is asynchronous: the 201 means we accepted the post, not that the network has accepted it. Poll the lightweight status endpoint until it settles.

curl "$VD_API/brands/412/posts/1000241/status" \
  -H "Authorization: Bearer $VD_API_KEY"
{
  "data": {
    "id": 1000241,
    "status": "published",
    "scheduled_at": null,
    "published_at": "2026-07-12T09:00:04+00:00"
  }
}

status goes publishingpublished, or failed if the network rejected it. Fetch the full post to see each variant's outcome and its platform_post_url.

Polling is fine for one post. For anything at volume, subscribe to the post.published and post.failed webhooks and let us call you instead — see Webhooks.

Where to go next

  • Errors — every code you can get back, and what to do about each.
  • Idempotency — retry writes without publishing twice.
  • Webhooks — stop polling.
  • API reference — the other fifty endpoints.