Converza API
Put contacts into Converza from your own website, shop or scripts, record who agreed to be messaged, and import a CSV file.
Base URL: https://converza.site/api/v1
Contents
- Why consent is required
- Getting a key
- Authentication
- Errors
- Rate limits
- `GET /me`
- `POST /contacts`
- `POST /contacts/import`
- `POST /opt-in`
- Form submissions
- What happens after a contact arrives
Why consent is required
Every endpoint that creates a contact also asks how that person agreed to hear from you. This is not paperwork. Converza will not send a first message to somebody who has no record of agreeing, and that rule exists because of what happens without it: in September a workspace imported 186 numbers from a list, an automation began messaging them two minutes later, and WhatsApp closed the business's number after the fourteenth message. Fifty-seven of those people never got a message at all, because the number was already gone.
So a contact imported with no consent is saved, searchable and usable, but Converza will not open a conversation with them. You can still reach them with a WhatsApp Status post, which is one post however many people see it and interrupts nobody's chat.
Consent you declare is your word, not something Converza witnessed, which is why source is required and evidence is worth filling in. Write what you would point at if WhatsApp or a regulator asked.
| Basis | Means |
|---|---|
inbound_message | They messaged you first. Converza records this itself. |
form_submission | They filled in a Converza form. Recorded automatically. |
api_declared | Your system is declaring it, through this API. |
import_declared | Declared for the rows of a file you imported. |
manual | A team member recorded it by hand. |
Getting a key
Settings → Integrations → API keys → Create key.
The key is shown once. Only a hash of it is stored, so it cannot be recovered afterwards; if you lose it, revoke it and make another. Only admins and the owner can create or revoke keys.
A key is workspace-wide. Treat it like a password: keep it on your server, never in a web page, a mobile app, or anything a visitor's browser downloads.
Authentication
Send the key as a bearer token:
Authorization: Bearer cvz_live_xxxxxxxxxxxxxxxxX-API-Key: cvz_live_... also works, for tools that cannot set an Authorization header.
Errors
Every response carries ok. Failures carry a sentence meant to be read:
{ "ok": false, "error": "consent.source is required: say where this person agreed, for example \"checkout opt-in box\"." }| Status | Meaning |
|---|---|
| 400 | The body was not valid JSON. |
| 401 | Key missing, unknown, or revoked. |
| 413 | The import file has too many rows. Split it. |
| 422 | The request was understood but something in it is wrong or missing. |
| 429 | Rate limited. retry_after says how many seconds to wait. |
Rate limits
| Endpoint | Limit |
|---|---|
| Most endpoints | 120 requests per minute, per key |
POST /contacts/import | 6 requests per minute, per key |
Limits are per key, not per IP, so your own traffic is never affected by anybody else's.
GET /me
What this key can see. Call it first, to check the key works and points at the workspace you expect.
curl https://converza.site/api/v1/me \
-H "Authorization: Bearer $CONVERZA_KEY"{
"ok": true,
"workspace": { "id": "...", "name": "Amaka's Boutique", "timezone": "Africa/Lagos" },
"plan": { "tier": "Growth", "status": "active" },
"key": { "name": "Shopify checkout", "prefix": "cvz_live_a1b2c3", "scopes": ["contacts:write"] },
"whatsapp": { "transport": "cloud", "connected": true, "daily_message_limit": null }
}whatsapp.daily_message_limit is the cap on messages this workspace may send today, and it is the field most worth reading. On the free tier it is 5 a day and it does not grow. On a paid plan using the official WhatsApp Cloud API it is null, because Meta applies its own limits instead.
POST /contacts
Adds or updates one contact. Matching is by phone number, so sending the same number twice updates rather than duplicates.
curl -X POST https://converza.site/api/v1/contacts \
-H "Authorization: Bearer $CONVERZA_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "08031234567",
"name": "Amaka Obi",
"email": "amaka@example.com",
"tags": ["checkout", "lagos"],
"location": "Lagos",
"source": "Shopify checkout",
"consent": {
"basis": "api_declared",
"source": "checkout opt-in box",
"evidence": "order 10492, box ticked 2026-10-05T09:14:00Z"
}
}'| Field | Required | Notes |
|---|---|---|
phone | yes | Any format. A number starting 0 is read as Nigerian (+234). |
name | no | |
email | no | |
tags | no | Array of strings. Each one fires your tag_added automations. |
location | no | |
birthday | no | YYYY-MM-DD. Drives birthday automations. |
source | no | Where the contact came from. Shown in Converza. |
consent.basis | no | Defaults to api_declared. |
consent.source | yes | Where they agreed. |
consent.evidence | no | What you would point at if asked. Strongly recommended. |
{
"ok": true,
"contact": {
"id": "...",
"name": "Amaka Obi",
"phone": "+2348031234567",
"tags": ["checkout", "lagos"],
"messageable": true
}
}messageable: false means the consent record did not save, so Converza will not send this person a first message. Declare it again with POST /opt-in.
POST /contacts/import
A CSV file of contacts, in one request. Up to 2000 rows per call.
Two ways to send it. Raw, which is easiest from a shell:
curl -X POST https://converza.site/api/v1/contacts/import \
-H "Authorization: Bearer $CONVERZA_KEY" \
-H "Content-Type: text/csv" \
-H "X-Consent-Source: 2024 newsletter signups, double opt-in" \
-H "X-Consent-Evidence: Mailchimp list 'shop-news', export of 2026-10-01" \
--data-binary @contacts.csvOr as JSON, which is easier from code:
curl -X POST https://converza.site/api/v1/contacts/import \
-H "Authorization: Bearer $CONVERZA_KEY" \
-H "Content-Type: application/json" \
-d '{
"csv": "name,whatsapp,city\nAmaka Obi,08031234567,Lagos\nTunde Bello,08159876543,Ibadan\n",
"consent": {
"source": "2024 newsletter signups, double opt-in",
"evidence": "Mailchimp list shop-news, export of 2026-10-01",
"tags": ["newsletter-import"]
}
}'Columns
Headers are matched by name, in any order, and anything unrecognised is ignored. A phone column is required; everything else is optional.
| Field | Accepted headers |
|---|---|
| phone | phone, whatsapp, whatsapp number, mobile, msisdn, tel, number, contact |
| name | name, full name, customer, customer name, contact name, first name |
email, e-mail, email address | |
| tags | tag, tags, label, labels, segment, group |
| location | location, city, town, state, address, area |
| birthday | birthday, birth date, dob, date of birth |
Quoted fields work, so "Okafor, Amaka" and "Lagos, Nigeria" stay in one column. Tags inside a cell may be separated by commas, semicolons or pipes. Rows with no phone number are skipped and counted.
{
"ok": true,
"imported": 184,
"failed": 0,
"skipped_without_phone": 2,
"consent": {
"basis": "import_declared",
"source": "2024 newsletter signups, double opt-in",
"evidence": "Mailchimp list shop-news, export of 2026-10-01"
}
}If you cannot describe where a list came from, the request is refused with a
- That is the intended behaviour, and it is the check that would have saved
the number lost in September.
POST /opt-in
Records that somebody agreed to be messaged, or that they no longer do. Works by phone number and does not need the contact to exist yet.
Opting in:
curl -X POST https://converza.site/api/v1/opt-in \
-H "Authorization: Bearer $CONVERZA_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "08031234567",
"source": "WhatsApp updates checkbox on shop.example.com",
"evidence": "order 10492"
}'Opting out:
curl -X POST https://converza.site/api/v1/opt-in \
-H "Authorization: Bearer $CONVERZA_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "08031234567", "opted_in": false, "reason": "Unsubscribed by email" }'An opt-out is honoured everywhere in Converza immediately: campaigns, automations and Status audiences all exclude opted-out contacts. An opt-out for a number Converza has never seen creates the contact already opted out, so the promise is kept if they ever do arrive. Opting in again clears a previous opt-out, because the person has said so themselves.
Form submissions
Converza forms post to their own endpoint, which is not key-authenticated:
POST https://converza.site/api/forms/submit
Content-Type: application/json
{ "slug": "your-form-slug", "values": { "Name": "Amaka", "WhatsApp": "08031234567" }, "source": "instagram-bio" }It takes the form's slug rather than an API key on purpose. This call comes from a visitor's browser on your own page, and a browser cannot keep a secret: an API key in front-end JavaScript is a published API key. The form's slug is public by design, submissions are rate limited per IP, and the form itself decides which fields are required.
A form submission is consent that Converza witnessed, so these contacts are immediately messageable with no declaration needed.
Create a form under Capture in Converza to get a slug, a hosted page at /f/your-form-slug, and an embed snippet.
What happens after a contact arrives
- Your workspace's naming and tagging rules are applied (Settings → Workspace), so an API contact is labelled like any other.
- Each tag fires its `tag_added` automations, so a welcome sequence built in Converza runs for API contacts exactly as it does for form submissions.
- The contact is pushed to any connected address books (Google Contacts, iCloud) if the workspace syncs to them.
- Sending obeys the send guard: a first message needs consent, and the workspace's daily cap applies.
GET /metells you what that cap is.