Quickstart
Five steps from an API key to the event feed. You need a terminal with curl, Node.js 18+ or Python 3.
Prompt for your AI assistant
Paste this text into Claude, ChatGPT, Cursor or another assistant. It will read the API docs and follow our integration rules. The text contains no key.
Show the prompt text
# AI agent prompt: integrating with the Meliora API
You are helping a developer connect to the Meliora public API. Meliora is a service for automated Telegram outreach and conversations for B2B. It has an organization, workspaces, campaigns (message sequences), leads (a contact's participation in a campaign), stored conversations and an event feed.
## Read the docs first
Before you write code or answer, read these files. They are more accurate than your memory:
1. https://docs.melioralab.io/en/llms.txt: the docs table of contents, with links to the Markdown of every page.
2. https://docs.melioralab.io/en/llms-full.txt: the full docs in one file.
3. https://docs.melioralab.io/en/openapi.json: the OpenAPI 3.1 spec, the only source of truth for routes, parameters and fields.
If you don't have network access, ask the user to send you the contents of these files.
## What the API can do
- Version v1, read-only. There is no write access: you can't add contacts or start or stop a sequence through the API. That is done in the Meliora app.
- Base URL: https://api.melioralab.io/v1
- Routes, all GET: /me, /workspaces, /campaigns, /campaigns/{id}, /leads, /leads/{id}, /leads/{id}/messages, /events.
- Responses are JSON. Timestamps are UTC ISO 8601 with milliseconds. Errors use application/problem+json with the fields status, code, param and request_id. Handle errors by status and code, not by message text.
## Key and security
- Send the key only in the header Authorization: Bearer <key>. Never put the key in the request URL.
- Read the key from the environment variable MELIORA_API_KEY. Don't ask the user to paste the key into the chat, and never print the key in replies, code, logs or commits.
- Call the API only from a server, never from a browser.
- The first step is always GET /v1/me: it returns the key's permissions (scopes), scope and limits. Check that the permissions cover the task before you write the rest of the code.
## Integration rules
- Lists are paginated with a cursor: repeat the request with cursor=next_cursor and the same filters while has_more is true. limit is up to 100, up to 200 for /events. A list cursor lives for 15 minutes.
- The /events feed has two event types: lead.first_message_sent and lead.replied. Save next_cursor from every response, even an empty one. Apply a page's events and save the new cursor in one transaction. Deduplicate events by event.id. When has_more is false, wait between one minute and one hour before polling again.
- First run of the feed: GET /v1/events?start=now returns a cursor. Then reconcile leads through /v1/leads, then read the feed from that cursor.
- A 410 cursor_expired or 400 cursor_mismatch response from the feed: record the gap, get a new cursor with start=now, reconcile campaigns and leads, and continue polling.
- 429 and 503 responses: retry the same request after Retry-After seconds with random jitter, and increase the delay on repeated 503s. An error doesn't move the cursor. Don't retry requests that got 401 in a loop.
- CRM mapping: the link key is fields.record_id on the lead record (/v1/leads, /v1/leads/{id}), and only when record_id_status is valid. Events don't carry record_id yet: match an event to a lead by data.lead.id. A Telegram username is not unique, so don't use it as a key.
- Only move CRM statuses forward: a lead's reply must not roll back to the "In progress" status.
- Message text is available only if the key has the messages:read permission and "Message text" is enabled.
- Not returned yet: delivery_state values failed and waiting_queue, automation_state value completed, reason_code values schedule_window and service_unavailable. In the reply event, automation_stopped is always null. The conversation archive and the feed may be incomplete: see meta.completeness and meta.coverage. The value unknown is normal, not an error.
## What not to do
- Don't invent routes, parameters, fields or values that are not in openapi.json. If something you need doesn't exist, say so and suggest a solution using the existing routes.
- Don't promise write access through the API: v1 doesn't have it.
- Check anything uncertain against the docs and link to the relevant page.
Docs: https://docs.melioralab.io/en/
Support: https://t.me/meliora_support
#Get an API key
During early access, we issue keys for you: contact Support and tell us which permissions and scope you need. To get started, the Campaigns, Leads and Events permissions are enough.
If your organization has Settings › Integrations › API enabled, an Owner or Admin can create a key with the "Create key" button.
The key is shown only once. Save it right away: you can't recover it, only rotate it.
#Store the key in an environment variable
Don't hardcode the key or pass it in the request URL. Store it in an environment variable:
export MELIORA_API_KEY="mel_live_..."#Check access
GET /v1/me returns the organization, permissions and limits of the key. It works with any set of permissions.
curl https://api.melioralab.io/v1/me \
-H "Authorization: Bearer $MELIORA_API_KEY"const res = await fetch('https://api.melioralab.io/v1/me', {
headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` },
});
console.log(res.status, await res.json());import os
import requests
resp = requests.get(
"https://api.melioralab.io/v1/me",
headers={"Authorization": f"Bearer {os.environ['MELIORA_API_KEY']}"},
timeout=15,
)
print(resp.status_code, resp.json())In the response, check scopes and scope: these are the key's permissions and scope. If you get an error, look up its code and how to fix it in Errors. The most common one is invalid_api_key: the key was copied only partially or with an extra space.
#List campaigns and leads
Campaigns are returned in pages, oldest first:
curl -G https://api.melioralab.io/v1/campaigns \
-H "Authorization: Bearer $MELIORA_API_KEY" \
-d status=activeTake the id of the campaign you need and request its leads:
curl -G https://api.melioralab.io/v1/leads \
-H "Authorization: Bearer $MELIORA_API_KEY" \
-d campaign_id=4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14 \
-d limit=100If the response has has_more: true, pass the next_cursor value as the cursor parameter of your next request. See Pagination for details.
#Subscribe to events
To learn about new messages and replies without walking through every lead, read the event feed. The first request with start=now returns an empty page and the cursor the feed starts from:
curl -G https://api.melioralab.io/v1/events \
-H "Authorization: Bearer $MELIORA_API_KEY" \
-d start=nowSave next_cursor and request GET /v1/events?cursor=... once a minute. Every response contains a new cursor, even when there are no events: always save it.
#What's next
- Polling events: a loop that never loses or duplicates events, with Node.js and Python examples.
- CRM mapping: how to match a lead to a CRM record with
record_id. - Rate limits and retries: how to handle 429 and 503.