API reference

Convoly API

Read your Instagram conversations, send DMs and check your automations from your own application, over a plain REST API.

Introduction

The Convoly API is organised around REST. It uses predictable, resource-oriented URLs, returns JSON for every response including errors, and relies on standard HTTP status codes.

Base URL
https://www.convoly.in/api/v1

What you can do

  • Read the contacts who have messaged your Instagram account
  • Export the phone numbers people shared as leads, and keep a CRM in sync with them
  • Read full conversation history, including shared reels and posts
  • Send a DM, with Instagram’s 24-hour window enforced for you
  • Inspect your automations and their run statistics
  • Track how many messages you have sent this month
One key, one Instagram account. Each API key is scoped to a single connected Instagram account. To act on several accounts, create a key for each.

Authentication

Authenticate with an API key, sent in the Authorization header of every request. Create keys in Settings → API keys inside Convoly.

Authorization header
Authorization: Bearer cvy_live_YOUR_KEY

A bare Authorization: cvy_live_… without the Bearer prefix is also accepted.

Keep your key secret. It can send messages from your Instagram account. Never put it in client-side code, a mobile app or version control — call the API from your server. The key is shown once when you create it and stored only as a hash, so if you lose it, revoke it and make a new one.
Example request
curl https://www.convoly.in/api/v1/account \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"

Errors & status codes

Every error returns the same envelope: a stable machine-readable error you can branch on, and a message written for a person.

{
  "error": "send_not_allowed",
  "message": "More than 24 hours have passed since this person's last message, so an automated reply cannot be sent."
}
StatusErrorMeaning
400invalid_requestA required field is missing or malformed. The message says which.
400invalid_jsonThe request body is not valid JSON.
400invalid_cursorThe cursor was not issued by this API. Pass next_cursor back exactly as received.
401missing_api_keyNo Authorization header was sent.
401invalid_api_keyThe key is wrong, or it has been revoked.
403account_inactiveThe Instagram account behind this key is disconnected. Reconnect it in Convoly.
404contact_not_foundNo contact with that identifier belongs to this account.
404lead_not_foundNo lead with that id belongs to this account.
422send_not_allowedInstagram's messaging window forbids this send. Check reason and messaging_window.
500internal_errorSomething went wrong on our side. Safe to retry with backoff.
GET

Get account

GET https://www.convoly.in/api/v1/account

Returns the Instagram account this API key belongs to. Useful as a health check and to confirm which account a key acts on.

Request
curl https://www.convoly.in/api/v1/account \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "account": {
    "id": "3f0c…",
    "instagram_user_id": "17841444482386124",
    "username": "languageiquanta",
    "display_name": "Language With iQuanta",
    "account_type": "BUSINESS",
    "followers_count": 222,
    "status": "active"
  }
}
GET

List contacts

GET https://www.convoly.in/api/v1/contacts

People who have messaged your account. Each contact carries its current messaging_window, so you can tell whether a send would be permitted without reimplementing Instagram's rules.

Query parameters
ParameterTypeRequiredDescription
limitintegerNoHow many to return. Default 50, maximum 200.
Request
curl "https://www.convoly.in/api/v1/contacts?limit=2" \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "contacts": [
    {
      "id": "b71e…",
      "instagram_user_id": "1099883142544299",
      "name": "Lakshay Panwar",
      "tags": ["lead"],
      "opted_out": false,
      "last_inbound_at": "2026-08-30T10:20:11.000Z",
      "messaging_window": "open"
    }
  ],
  "has_more": true
}
Notes
  • messaging_window is one of: open (you may reply freely), human_agent_only (past 24h — only a human-typed reply is lawful), closed (past 7 days), no_conversation (they have never messaged you).
GET

List leads

GET https://www.convoly.in/api/v1/leads

Phone numbers people shared with your account — typed in a DM, or collected by an automation — together with who shared them and which automation captured them. Leads come oldest change first, so you can import everything once and then fetch only what changed.

Query parameters
ParameterTypeRequiredDescription
limitintegerNoLeads per page. Default 100, maximum 500.
cursorstringNonext_cursor from the previous page, passed back unchanged. Omit for the first page.
updated_sinceISO 8601NoOnly leads created or changed at or after this instant, e.g. 2026-10-16T00:00:00Z.
statusstringNonew, contacted, converted or not_interested.
sourcestringNoautomation (collected by an automation step) or chat (typed in a conversation).
First page
curl "https://www.convoly.in/api/v1/leads?limit=200" \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Only changes since the last sync
curl "https://www.convoly.in/api/v1/leads?updated_since=2026-10-16T08:42:34.123Z" \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "leads": [
    {
      "id": "a41e…",
      "phone": "+919876543210",
      "status": "new",
      "source": "automation",
      "automation": { "id": "5d2f…", "name": "GUIDE comment flow" },
      "contact": {
        "id": "8b1c…",
        "instagram_user_id": "1784…",
        "username": "priya.sharma",
        "name": "Priya Sharma",
        "profile_picture_url": "https://…"
      },
      "first_shared_at": "2026-10-14T11:02:51.120Z",
      "last_shared_at": "2026-10-15T09:30:12.004Z",
      "updated_at": "2026-10-15T09:30:12.004Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJ0IjoiMjAyNi0xMC0xNSAwOTozMDoxMi4wMDQ…"
}
Notes
  • Moving leads into a CRM: call with no parameters, then keep requesting with cursor=next_cursor until has_more is false. Store the updated_at of the last lead you received.
  • Keeping the CRM current: on each later run, pass that stored value as updated_since and page through the same way. Any lead created, re-shared or changed in status since then comes back; nothing else does. A lead can appear in two runs, so upsert on id.
  • phone is in E.164 format (+ and country code). One person who shares two numbers is two leads with the same contact.id.
  • automation is null when the number was typed in an ordinary conversation rather than collected by an automation step.
PATCH

Update a lead's status

PATCH https://www.convoly.in/api/v1/leads/:lead_id

Writes a follow-up outcome back to Convoly, so the Lead Manager shows the same status as your CRM. This changes updated_at, so the lead also appears in your next updated_since sync.

Path parameters
ParameterTypeRequiredDescription
lead_idstringYesThe lead's id, from List leads.
Body
ParameterTypeRequiredDescription
statusstringYesnew, contacted, converted or not_interested.
Request
curl -X PATCH https://www.convoly.in/api/v1/leads/a41e… \
  -H "Authorization: Bearer cvy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "converted"}'
Responses
200 OKSuccess
{ "lead": { "id": "a41e…", "status": "converted" } }
404 Not FoundUnknown lead
{ "error": "lead_not_found", "message": "No lead with that id on this account." }
GET

List conversations

GET https://www.convoly.in/api/v1/conversations

Every conversation with a message count and a preview of the most recent message.

Query parameters
ParameterTypeRequiredDescription
limitintegerNoHow many to return. Default 50, maximum 200.
Request
curl https://www.convoly.in/api/v1/conversations \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "conversations": [
    {
      "contact_id": "b71e…",
      "instagram_user_id": "1099883142544299",
      "name": "Lakshay Panwar",
      "message_count": 24,
      "last_message": {
        "direction": "in",
        "text": "Hey",
        "sent_at": "2026-08-30T10:20:11.000Z"
      }
    }
  ],
  "has_more": false
}
GET

Get messages

GET https://www.convoly.in/api/v1/conversations/:contact_id/messages

The message history for one conversation, oldest first. Shared reels and posts arrive as attachments with an instagram.com permalink and an empty text field.

Path parameters
ParameterTypeRequiredDescription
contact_idstringYesThe contact's Convoly id.
Query parameters
ParameterTypeRequiredDescription
limitintegerNoMost recent N messages. Default 50, maximum 200.
Request
curl https://www.convoly.in/api/v1/conversations/b71e.../messages \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "contact_id": "b71e…",
  "messages": [
    { "id": "…", "direction": "in", "text": "Hey", "attachments": [],
      "source": "dm", "sent_at": "2026-08-30T10:20:11.000Z" },
    { "id": "…", "direction": "in", "text": "", "source": "dm",
      "attachments": [
        { "kind": "share", "media": "reel", "title": null,
          "url": "https://www.instagram.com/reel/DWVusbwDQba/" }
      ],
      "sent_at": "2026-08-30T10:21:02.000Z" }
  ]
}
404 Not FoundUnknown conversation
{
  "error": "conversation_not_found",
  "message": "No conversation with that contact id."
}
POST

Send a DM

POST https://www.convoly.in/api/v1/messages

Sends a direct message. This runs the same 24-hour window check as the rest of Convoly, so a send that Instagram would reject is refused here first, with a reason you can act on.

Body
ParameterTypeRequiredDescription
textstringYesThe message. 1–1000 characters.
contact_idstringNoThe contact's Convoly id. Provide this or instagram_user_id.
instagram_user_idstringNoThe contact's Instagram-scoped id. Provide this or contact_id.
Request
curl -X POST https://www.convoly.in/api/v1/messages \
  -H "Authorization: Bearer cvy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "b71e…",
    "text": "Thanks for reaching out — here is the link you asked for."
  }'
Responses
201 CreatedSent
{
  "message": {
    "id": "…",
    "mid": "aWdfZAG1faXRlbTo…",
    "contact_id": "b71e…",
    "text": "Thanks for reaching out …",
    "status": "sent"
  }
}
422 UnprocessableOutside the messaging window
{
  "error": "send_not_allowed",
  "message": "More than 24 hours have passed since this person's last message, so an automated reply cannot be sent.",
  "reason": "outside_24h_window_and_not_human_agent",
  "messaging_window": "human_agent_only"
}
404 Not FoundUnknown contact
{
  "error": "contact_not_found",
  "message": "No contact matches that identifier for this account. A contact exists only after they message you first."
}
Notes
  • Instagram has no cold outreach. A contact can only be messaged after they message you first — otherwise you get reason: user_never_messaged_first.
  • The HUMAN_AGENT tag, which extends the window to 7 days, is intentionally not available over the API. Meta permits it only for text a person actually typed, and an API call cannot attest to that. Use the Live Chat inbox for those replies.
  • A 422 is a normal outcome, not a fault. Handle it rather than retrying — retrying will not change the window.
GET

List automations

GET https://www.convoly.in/api/v1/flows

Your automations, their trigger configuration and their run statistics.

Request
curl https://www.convoly.in/api/v1/flows \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "flows": [
    {
      "id": "…",
      "name": "Auto-DM links from comments",
      "status": "live",
      "trigger": { "type": "comment", "match": "contains", "keywords": ["link"] },
      "stats": { "runs": 42, "sent": 39, "clicks": 12 }
    }
  ]
}
GET

Get usage

GET https://www.convoly.in/api/v1/usage

Sends in the current calendar month, broken down by outcome — including the ones the window check stopped before they cost you anything.

Request
curl https://www.convoly.in/api/v1/usage \
  -H "Authorization: Bearer cvy_live_YOUR_KEY"
Responses
200 OKSuccess
{
  "period": "current_calendar_month",
  "sent": 142,
  "blocked_by_window": 9,
  "blocked_opted_out": 0,
  "failed": 1,
  "total_attempted": 152
}