Introduction
The ViralDM API is a REST API that takes and returns JSON. It is the same API the ViralDM mobile app and the MCP server use, so anything you can do there you can do from your own code.
Every path in these docs is relative to the base URL https://viraldm.app/api/v1. Send bodies as Content-Type: application/json.
403), read and write from Starter. Calling the API directly with a key works on the Growth, Agency and Agency Pro app plans, during the free first month, and on any API plan; on Free and Starter a direct call returns 403 with The developer API (API keys called directly) starts on the Growth plan…. See API pricing.Authentication
Create a key in Dashboard → API keys. A key looks like vdm_ followed by 48 hex characters. It is shown once, when you create it; ViralDM stores only a hash, so nobody can read it back later. Revoke a key there too.
Send the key in either header:
| Header | Example |
|---|---|
X-API-Key | X-API-Key: vdm_3f9c… |
Authorization | Authorization: Bearer vdm_3f9c… |
Your first request
GET /me returns the user and the key making the call. It is the quickest way to check a key works.
curl https://viraldm.app/api/v1/me \ -H "X-API-Key: $VIRALDM_API_KEY"
// Node 18+ has fetch built in const res = await fetch('https://viraldm.app/api/v1/me', { headers: { 'X-API-Key': process.env.VIRALDM_API_KEY } }); console.log(await res.json());
import os, requests r = requests.get('https://viraldm.app/api/v1/me', headers={'X-API-Key': os.environ['VIRALDM_API_KEY']}) print(r.json())
Read and write permissions
A key is read-only unless you give it write permission when you create it. GET requests need read. Every other method (POST, PUT, DELETE) needs write; a read-only key gets 403 with This credential is read-only.
OAuth tokens
AI assistants that connect over MCP get an OAuth 2.0 access token instead of a key. The API accepts it as a Bearer token with the same rules: mcp:read can read, mcp:write can also change things. Setup is on the MCP server page.
Quick start
- Sign up at viraldm.app. The free first month includes API access.
- Connect Instagram in the dashboard. It must be a Business or Creator account.
- Create a key in Dashboard → API keys. Tick write if you will create things.
- Find your account id with
GET /accounts. - Create an automation. The example below DMs everyone who comments LINK and replies to their comment in public.
# 1. Your connected accounts curl https://viraldm.app/api/v1/accounts -H "X-API-Key: $VIRALDM_API_KEY" # 2. Comment LINK, get a DM curl -X POST https://viraldm.app/api/v1/automations \ -H "X-API-Key: $VIRALDM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "LINK on Reels", "socialAccountId": "ACCOUNT_ID", "type": "COMMENT_TO_DM", "trigger": { "keywords": ["link"] }, "action": { "message": "Here is the guide: https://example.com/guide", "commentReply": "Sent it to your DMs" } }'
const api = (path, opts = {}) => fetch('https://viraldm.app/api/v1' + path, { ...opts, headers: { 'X-API-Key': process.env.VIRALDM_API_KEY, 'Content-Type': 'application/json' } }).then(r => r.json()); const { data: accounts } = await api('/accounts'); const created = await api('/automations', { method: 'POST', body: JSON.stringify({ name: 'LINK on Reels', socialAccountId: accounts[0].id, type: 'COMMENT_TO_DM', trigger: { keywords: ['link'] }, action: { message: 'Here is the guide: https://example.com/guide', commentReply: 'Sent it to your DMs' } }) }); console.log(created.data.id);
import os, requests API = 'https://viraldm.app/api/v1' H = {'X-API-Key': os.environ['VIRALDM_API_KEY']} accounts = requests.get(API + '/accounts', headers=H).json()['data'] r = requests.post(API + '/automations', headers=H, json={ 'name': 'LINK on Reels', 'socialAccountId': accounts[0]['id'], 'type': 'COMMENT_TO_DM', 'trigger': {'keywords': ['link']}, 'action': {'message': 'Here is the guide: https://example.com/guide', 'commentReply': 'Sent it to your DMs'} }) print(r.status_code, r.json())
Limits and quotas
Each request is checked against the allowance on your account: requests per second, calls per month and, on Free Dev, calls per day. An API plan sets its own allowance. An app plan with API access includes the allowance of one API plan, with read and write access: the free first month gets API Starter's, Starter gets API Starter's (through MCP only), Growth gets API Growth's, Agency gets API Scale's and Agency Pro gets API Agency's. Free through MCP gets Free Dev's, reads only.
| API plan | Per second | Per month | Per day | Writes |
|---|---|---|---|---|
| Free Dev | 2 | 500 | 50 | No |
| API Starter | 5 | 5,000 | No cap | Yes |
| API Growth | 10 | 25,000 | No cap | Yes |
| API Scale | 25 | 100,000 | No cap | Yes |
| API Agency | 50 | 500,000 | No cap | Yes |
Calls are counted per account, not per key, and OAuth calls from MCP count too. Months and days are calendar months and days. Some routes have their own ceilings on top:
- 3,000 requests a minute per
X-API-Key(per IP address for Bearer tokens), on every plan. - 200 requests a minute per IP address across
/api. - 10 a minute on the AI generation routes (
/ai/…) and 5 a minute on/profile-audit.
Response headers
| Header | Meaning |
|---|---|
X-RateLimit-Plan | The allowance that metered this call, for example api-starter, or included-growth when it comes with your app plan. |
X-RateLimit-Limit | Your monthly call quota. |
X-RateLimit-Remaining | Calls left this month. |
Over a limit you get 429. The body says which limit: Rate limit exceeded (per second, retry after 1 second), Monthly API quota exceeded or Daily API quota exceeded (free tier), with used and limit. The monthly quota is a hard stop until the next month or a bigger plan.
Errors
Errors come back as JSON with an error message, and often a code, reason or upgradeUrl.
| Status | When |
|---|---|
400 | A field is missing or invalid. The message names it. |
401 | No key, or the key is invalid, revoked or expired. |
402 | No AI call credits left. |
403 | Read-only key, a plan without API access, the Free Dev sandbox, or a plan limit (automations, keywords, a paid feature). |
404 | Not found, or not yours. |
409 | A DM outside the messaging window, or cancelling a call that already started. |
429 | A rate limit or quota. See above. |
5xx | Our side or Meta's. Safe to retry a GET after a short wait. |
{ "error": "This credential is read-only.", "detail": "POST requires write permission. This key has: read." }
Automations
An automation listens for a trigger (a comment keyword, a DM keyword, a story reply) and sends a reply. The same plan limits as the dashboard apply: the number of automations and keywords per automation.
Body
| Field | What it is |
|---|---|
socialAccountId | Required. From GET /accounts. |
type | One of the types below. Defaults to KEYWORD_DM; an unknown value is refused. |
name | Your label. |
trigger | keywords (array, required for keyword types) plus the conditions below. |
action | message (the DM), replyVariants (several, one picked at random), commentReply (the public reply to the comment). A message or a comment reply is required. |
aiConfig | AI replies: { "enabled": true, "persona": "…", "businessInfo": "…", "goal": "…" }. |
targetMediaIds | Run on these posts only (ids from GET /accounts/:id/media). Without it, the automation runs on every post. |
triggerMode | NEXT_POST to bind to the next post you publish, or CAPTION_CODE with captionCode. |
leadGate | Ask for an email or phone number before the DM is delivered. |
Types
| Type | Fires on |
|---|---|
COMMENT_TO_DM | A comment with a keyword. Sends a DM and, if set, a public reply. |
COMMENT_REPLY | A comment with a keyword. Public reply only. |
KEYWORD_DM | A DM that contains a keyword. |
DM_REPLY | Any incoming DM. |
STORY_REPLY | A reply to your story. |
MOOD_SHIELD | An angry or abusive message, answered calmly. |
LIVE_COMMENT | A comment during a Live. Starter and up. |
POST_SHARE | Someone shares your post to you in a DM. Starter and up. |
STORY_MENTION | Someone mentions you in their story: one thank-you DM per person per week (set cooldownMinutes to change it). Starter and up. |
Conditions
Set these inside trigger.
| Field | Effect |
|---|---|
requireFollow | Only people who follow you get the DM. |
notFollowingMessage | What non-followers get instead. A default is sent if you leave it empty. |
requireCommentFirst | For DM triggers: the sender must have commented first. |
cooldownMinutes | Do not reply to the same person again within this many minutes. |
Late to set one up? POST /automations/:id/rewind sends it to the last 7 days of commenters. GET on the same path is a dry run that tells you how many people qualify.
Accounts
Connect accounts in the dashboard; the API works with accounts that are already connected. Instagram needs a Business or Creator account.
| Method | Path | What it does |
|---|---|---|
| GET | /accounts | Connected accounts, with token expiry and whether the webhook is subscribed. |
| GET | /accounts/:id/media | Recent posts and Reels, to bind an automation to one. |
| GET | /accounts/:id/health | Whether the account can receive and send right now. |
| POST | /accounts/:id/resubscribe | Subscribe the account's webhook again. |
| POST | /accounts/:id/disconnect | Disconnect the account. |
Sending a DM
Body: socialAccountId, recipientId (the person's Instagram-scoped id) and message. API sends are automated messages, so Meta's rule applies: the person must have messaged you in the last 24 hours. Outside that window you get 409 with Cannot DM this user and a reason.
Subscribers and segments
| Query | Effect |
|---|---|
tag | Only subscribers with this tag. |
search | Matches username or full name. |
limit | Page size. Default 20, most 100. |
sort | name for A to Z. Default is newest first. |
The response has data, total (the full count, not the page) and limit.
Body: action (add or remove), subscriberIds and tag. A new tag starts any sequence that is triggered by it.
Named audiences with a live count. Pass an id as a broadcast's segment. Each one is worked out from current data, and people who opted out are never in any of them.
| Id | Who is in it |
|---|---|
all | Every subscriber who has not opted out. |
engaged_30d | Messaged you in the last 30 days. |
quiet_30d | No message from them in over 30 days. |
vip | Marked VIP. |
hot_leads | Lead score of 70 or more. |
with_email | Has an email, so you can reach them outside Instagram. |
new_7d | First seen in the last 7 days. |
customers | Has bought at least one product. |
abandoned | Started a checkout and never paid, and has not bought anything since. |
Sequences
Drip DMs: a series of messages with delays between them.
{ "name": "Welcome series", "trigger": { "type": "KEYWORD", "keywords": ["welcome", "start"] }, "steps": [ { "delayMinutes": 0, "message": "Hi, welcome in." }, { "delayMinutes": 60, "message": "Here is what we do…" }, { "delayMinutes": 1440, "message": "A day later: any questions?" } ] }
Trigger types are KEYWORD, COMMENT and TAG. A new-follower trigger is refused: Instagram does not send ViralDM a follow event, and you cannot DM someone who has not messaged you. A new sequence runs on your first active account; move it with PUT and socialAccountId.
Broadcasts
Creating a broadcast does not send it. You get the real recipient count back first; DMs go out only when you pass send: true.
{ "name": "Weekend sale", "message": "50% off this weekend only.", "segment": "customers", "send": false } // → { "id": "…", "recipientCount": 212, "status": "DRAFT", "scheduled": false }
| Field | What it does |
|---|---|
message | Required. Checked against Meta's messaging policy before it is stored. |
segment | all, tagged (with tag) or a segment id. An unknown value is refused, never widened to everyone. |
tag | Required when segment is tagged. |
scheduledAt | ISO date and time to send later. |
send | true sends now. Anything else saves a draft. |
socialAccountId | Optional. Defaults to your first active account. |
People who opted out are left out of the count and the send. Anyone outside the 24-hour messaging window is skipped, not failed.
Scheduled posts
GET /scheduled-posts takes platform (default all) and days ahead (default 7). A post has platform, scheduledFor (ISO), postType (POST, CAROUSEL, REEL or STORY), content and mediaUrl. A carousel takes mediaUrls with 2 to 10 items. PUT works only while the post is still SCHEDULED.
For your own files, POST /media/initiate gives an upload URL and POST /media/:mediaId/finalize returns the public URL. POST /media is a one-shot upload for small images.
Store and orders
Your digital store: files, locked content, courses, memberships, events, 1:1 sessions and external links.
499 for ₹499 and read it back as pricePaise: 49900. Sending paise on the way in would price the product at ₹49,900.{ "title": "Instagram Growth Playbook", "type": "DIGITAL_FILE", "price": 499, "comparePrice": 999, "gstPercent": 18, "stockLimit": 100, "deliverUrl": "https://drive.google.com/…", "deliverMessage": "Thanks! Your playbook is here." }
PUT is a partial update: send only what changes. New products start as DRAFT and are not on the web until you set { "status": "PUBLISHED" }. Statuses are DRAFT, PUBLISHED, UNLISTED and ARCHIVED.
| Type | What it is |
|---|---|
DIGITAL_FILE | A file or link handed over on payment. |
LOCKED_CONTENT | Content unlocked after purchase. |
COURSE | Lessons, released together or dripped. |
MEMBERSHIP | Recurring access to a Telegram or Discord group. |
EVENT | A ticketed live session or webinar. |
SESSION_1ON1 | A booked 1:1 call. |
EXTERNAL | An affiliate or off-platform product. You take the click. |
Orders
Buyers pay you directly over UPI. ViralDM is never in the money path, so confirming a payment is your call. POST /orders/:id/paid is for a server that has already seen the money arrive, such as your own payment webhook. It delivers the product and books the revenue, and it is idempotent: confirming twice does not deliver twice. It is deliberately not available to AI assistants over MCP.
GET /orders takes status (PENDING, PAID, CANCELLED, REFUNDED) and limit (up to 200). GET /store/stats?days=30 (1 to 365) returns:
{ "periodDays": 30, "summary": { "revenuePaise": 1247500, "paidCount": 25, "avgOrderPaise": 49900, "conversionPct": 42, "startedCount": 59 }, "series": [ { "date": "2026-08-01", "revenuePaise": 49900, "orders": 1 } ], "byProduct": [ { "title": "Growth Playbook", "paidCount": 25, "revenuePaise": 1247500 } ] }
Revenue counts on the day an order was paid, not the day checkout began. conversionPct is null when no checkout has started, which is not the same as 0%.
AI Calls
Phone a lead from code. An Indian number rings, the AI talks in the lead's language, and the transcript, summary and outcome fill in on the call. POST needs a write key.
curl -X POST https://viraldm.app/api/v1/calls \ -H "X-API-Key: $VIRALDM_API_KEY" -H "Content-Type: application/json" \ -d '{ "phone": "9876543210", "name": "Rahul", "purpose": "Asked about the Growth plan on Instagram", "context": "Runs a travel page", "consent": { "source": "call_request", "at": "2026-10-15T10:42:00+05:30", "note": "Asked for a call in DM" }, "config": { "agentName": "Priya", "voice": "female", "language": "hinglish" } }' # → 201 { "success": true, "immediate": true, "data": { "id": "…", "status": "…" } } curl https://viraldm.app/api/v1/calls/CALL_ID -H "X-API-Key: $VIRALDM_API_KEY" # status ends as COMPLETED, NO_ANSWER or FAILED; transcript, summary, outcome fill in
phone is a 10-digit Indian mobile number. config is optional, per call: agentName, voice (female or male), businessName, pronounce (how to say the business name; Devanagari works), language, goal, notes, maxMinutes. Anything left out uses your settings on the AI Calls page. Languages: auto, hi, hinglish, en, ta, te, bn, mr, gu, pa, kn.
consent with every call: source (call_request, dm, form, whatsapp, existing_customer or other), at (when the person agreed, ISO 8601) and an optional note. We store it with the call and return it on GET /calls/:id. Until 15 November 2026 a call without it is still queued, with a warning in the response and a Warning header; from 15 November 2026 it is refused with 400 consent-required.- One credit is one answered call up to 3 minutes, plus one per started minute after that. Unanswered calls are free.
- Calls go out between 09:00 and 21:00 IST, and once per number per 24 hours.
402no credits left (call packs),429daily cap or the same number within 24 hours,403a plan below Starter.- Only a
QUEUEDcall can be cancelled; otherwise409.
Webhooks
Outgoing webhooks POST JSON to your HTTPS endpoint, for n8n, Make, Zapier or your own app.
Create one with name, url and events (an array). The URL must be public https://; private and internal addresses are refused. The response includes a secret. Save it: you need it to check signatures.
POST /webhooks/:id/test sends { "event": "test", "data": {…}, "timestamp": "…" } with an X-ViralDM-Signature header: the HMAC-SHA256 of the raw body, in hex, keyed with your secret.
const crypto = require('crypto'); // Express: app.post('/viraldm', express.raw({ type: 'application/json' }), handler) function verified(req) { const expected = crypto.createHmac('sha256', process.env.VIRALDM_WEBHOOK_SECRET) .update(req.body).digest('hex'); const got = String(req.headers['x-viraldm-signature'] || ''); return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected)); }
import hmac, hashlib, os def verified(raw_body: bytes, signature: str) -> bool: expected = hmac.new(os.environ['VIRALDM_WEBHOOK_SECRET'].encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature or '')
Business events: leads, bookings and sales
Seven events fire when ViralDM itself produces something worth acting on. They are what Zapier, Make and Pabbly Connect flows usually start from: create a "catch hook" in the tool, add its URL as a webhook here, and subscribe to the events you want.
lead.captured: a DM gave an email or a phone number ViralDM did not have.bio.lead: someone left their details on your bio page.form.submitted: a form was answered.booking.created: a call was booked on your bio page, in a DM, or on a connected Calendly.order.paid: a store order was paid.payment.received: a DM sale was paid through your payment gateway.mention.story: someone mentioned you in their Instagram story. Carriesusername, a ready-to-readmessageandurl, the Instagram thread where you can repost it.
{
"event": "lead.captured",
"type": "lead.captured",
"name": "Asha K",
"username": "asha.k",
"email": "[email protected]",
"phone": "+919800000000",
"platform": "INSTAGRAM",
"message": "Wants the course link",
"source": "viraldm",
"at": "2026-10-17T10:00:00.000Z"
}
Fields that are not known are left out. order.paid and payment.received also carry amount in rupees. Every delivery is signed with X-ViralDM-Signature as above.
DM events: dm.received and dm.sent
dm.received fires when a DM arrives on a connected account. dm.sent fires after every DM ViralDM sends, whether from an automation, the inbox, the API or MCP. Subscribe with "events": ["dm.received", "dm.sent"]. A staff inbox built on the API can rely on these instead of polling, and webhook deliveries do not count against your API calls.
{
"event": "dm.received",
"conversationId": "…",
"subscriberId": "…",
"platform": "INSTAGRAM",
"accountId": "…",
"messageId": "…",
"text": "Is the course still available?",
"attachments": [ { "type": "image", "url": "https://…" } ],
"createdAt": "2026-10-15T10:00:00.000Z"
}
attachmentslists every media item of the message, each typedimage,video,voice_noteorfile(ondm.sentalsoaudio). A voice note recorded in Instagram arrives asvoice_note, and its transcript is intext. Shared posts, reels and story mentions are not included.- Attachment URLs are Meta's own links and expire. Download the file when the event arrives.
- Every delivery carries
X-ViralDM-Event,X-ViralDM-Delivery,X-ViralDM-TimestampandX-ViralDM-Signature(as above). A delivery that fails with a network error,429or a5xxis retried twice, after 2 and 10 seconds, with the sameX-ViralDM-Deliveryid so you can drop duplicates. - Missed something? Read it back with
GET /conversationsandGET /conversations/:id/messages(see AI Brain and inbox).
comment.received, subscriber.created and the other events offered in the dashboard are not delivered yet. Endpoints also receive the test event and events relayed from your connected integrations, named <service>.<event> (for example a Razorpay or Shopify event); relayed events are not signed yet.All other endpoints
Everything else on /api/v1, grouped. Same key, same rules. The MCP tools on the MCP page describe most of them in more detail.
AI and content
| Method | Path | What it does |
|---|---|---|
| POST | /ai/generate-content | Captions, hooks and hashtags in your tone. |
| POST | /ai/write-dm | Draft a DM. |
| POST | /ai/content-calendar | A 7- or 14-day content plan. |
| POST | /humanize | Rewrite stiff copy in natural wording. |
| POST | /content-safety/check | Check a message for spam patterns and policy risk. |
| POST GET | /profile-audit · /summary · /pdf | Score your profile and suggest fixes. |
AI routes need an app plan with AI. Starter has 30 generations a month; Growth and up are unlimited.
Analytics, templates, coupons, tests
| Method | Path | What it does |
|---|---|---|
| GET | /analytics | Automation results. period: today, last_7_days, last_30_days, last_90_days. |
| GET | /analytics/best-times | The hours your automations engaged people. |
| GET | /revenue | Revenue per automation and conversion. |
| GET POST | /templates | DM templates. GET, PUT, DELETE on /templates/:id. |
| GET POST | /coupons | Discount codes. PUT /coupons/:id; POST /coupons/:id/distribute sends one by DM. |
| GET POST | /ab-tests | Two versions of one automation's DM (automationId, variantA, variantB), split 50/50 per person; one open test per automation. Sends, clicks and sales per variant at /ab-tests/:id/results. |
| GET | /affiliate/stats | Your affiliate referrals, clicks and commissions. |
AI Brain and inbox
| Method | Path | What it does |
|---|---|---|
| GET POST | /brain/docs | Documents the DM assistant answers from. POST /brain/ask asks them a question. |
| GET POST | /brain/intents | Route DMs by meaning. PUT, DELETE on /:id. |
| GET POST | /brain/snippets | Canned replies for the human inbox. |
| GET POST | /brain/starters/:accountId | Up to 4 tappable questions in a new DM thread. |
| GET POST | /brain/ai-comments | AI Comments settings, per account on POST. |
| GET | /conversations | Inbox threads. Messages at /conversations/:id/messages; POST there replies with text (up to 1,000 characters), an optional attachment { "type": "image" | "video" | "audio" | "file", "url": "https://..." }, or both (the text goes first, then the media). The URL must be public HTTPS; Meta downloads it at send time and applies its own limits (image PNG/JPEG/GIF up to 8 MB; video, audio and PDF files up to 25 MB). The stored message returns attachmentUrl and attachmentType. GET /conversations returns the 100 most recent threads; GET /conversations/:id/messages returns the newest messages of a thread (?limit= up to 200), listed oldest first, with has_more and next_before: pass ?before=<next_before> to read further back. Each message has attachments, every media item it carried (messages stored before 15 October 2026 keep only the first). Replies sent with an API key or a connector follow Meta's 24-hour window like every automated send. A person replying from the ViralDM inbox up to 7 days later (Meta's human-agent tag) is coming soon: it waits for Meta's approval. |
Growth tools
| Method | Path | What it does |
|---|---|---|
| GET POST | /giveaways | Comment-to-enter contests. /:id/end, /:id/draw, /:id/notify, /:id/entries. |
| GET POST | /safety/surge | Viral-post safety mode, per account on POST. |
| POST | /pay-links | A UPI pay page for one subscriber. |
| GET POST | /referrals | Friend loops: invite codes in DMs, with a leaderboard. |
| GET POST | /circle | Creator Circle shout-outs in your niche. |
| POST GET | /visual-search | Match a follower's photo to a product. /stats, /toggle. |
| GET POST | /agent-templates | Ready-made DM agents. Switch one on at /:slug/activate. |
Account, trust and integrations
| Method | Path | What it does |
|---|---|---|
| GET | /me | The user and the key making the call. |
| GET | /subscription | Your plan, usage and limits. |
| GET | /invoices · /notifications | Invoices and in-app notifications. |
| GET PUT | /bio-page | Your link-in-bio page. Stats at /bio-page/stats. |
| GET | /consent-stats | DPDP consent state per purpose. |
| GET POST | /payment-verifications · /payment-expectations | UPI screenshot checks, and telling ViralDM a payment is expected. |
| POST | /evidence-pack | A dispute PDF with the transcript and the payment. |
| GET POST | /integrations | Third-party integrations. POST /integrations/:slug/:action. |
| GET | /shopify/products · /shopify/order | Stock and one person's order from your own Shopify store. |
| GET | /youtube/status | Connected YouTube channels and today's API quota. |
MCP server
If you want an AI assistant to use ViralDM rather than write code against it, use the MCP server at https://mcp.viraldm.app/mcp. It turns this API into 58 tools that Claude, ChatGPT, Cursor, Gemini CLI and n8n can call, with OAuth sign-in. Its calls count towards the same quota.
Help
Stuck on something, or found a mistake in these docs? Write to us with the request you sent and the response you got. Never include your API key.
