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


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.

BasisMeans
inbound_messageThey messaged you first. Converza records this itself.
form_submissionThey filled in a Converza form. Recorded automatically.
api_declaredYour system is declaring it, through this API.
import_declaredDeclared for the rows of a file you imported.
manualA 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_xxxxxxxxxxxxxxxx

X-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\"." }
StatusMeaning
400The body was not valid JSON.
401Key missing, unknown, or revoked.
413The import file has too many rows. Split it.
422The request was understood but something in it is wrong or missing.
429Rate limited. retry_after says how many seconds to wait.

Rate limits

EndpointLimit
Most endpoints120 requests per minute, per key
POST /contacts/import6 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"
    }
  }'
FieldRequiredNotes
phoneyesAny format. A number starting 0 is read as Nigerian (+234).
nameno
emailno
tagsnoArray of strings. Each one fires your tag_added automations.
locationno
birthdaynoYYYY-MM-DD. Drives birthday automations.
sourcenoWhere the contact came from. Shown in Converza.
consent.basisnoDefaults to api_declared.
consent.sourceyesWhere they agreed.
consent.evidencenoWhat 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.csv

Or 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.

FieldAccepted headers
phonephone, whatsapp, whatsapp number, mobile, msisdn, tel, number, contact
namename, full name, customer, customer name, contact name, first name
emailemail, e-mail, email address
tagstag, tags, label, labels, segment, group
locationlocation, city, town, state, address, area
birthdaybirthday, 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

  1. 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

  1. Your workspace's naming and tagging rules are applied (Settings → Workspace), so an API contact is labelled like any other.
  2. 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.
  3. The contact is pushed to any connected address books (Google Contacts, iCloud) if the workspace syncs to them.
  4. Sending obeys the send guard: a first message needs consent, and the workspace's daily cap applies. GET /me tells you what that cap is.