Start a WhatsApp conversation
Send an approved template from your CRM and let an Intake Bot take the reply.
Use this when a form in your CRM creates a lead and you want ioZen to write to that person on WhatsApp. The call finds or creates the contact, opens the conversation, sends one approved template, and returns the ids. Replies follow the Intake Bot you name, or the workspace routing rules when you omit it.
The same Idempotency-Key returns the original result and does not send a second WhatsApp message.
Before you call
- The workspace is on a plan with the inbox.
- A WhatsApp connection is active. If the workspace has more than one, send
connection_id. - The template is approved, and its category is UTILITY or AUTHENTICATION. MARKETING templates return
422 template_category_not_allowed. - The workspace has credits for the send.
- The API key includes the
send:whatsappscope. Existing keys do not receive it.
Send the template
curl -X POST https://app.iozen.ai/api/v1/whatsapp/conversations \
-H "Authorization: Bearer $IOZEN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-1234-consulta" \
-d '{
"phone": "+593991234567",
"contact": {
"first_name": "Ana",
"last_name": "Torres",
"email": "ana@example.com",
"external_id": "crm-1234"
},
"template": { "name": "consulta_inicio", "language": "es", "params": {} },
"intake_bot_id": "your-published-intake-bot-id"
}'phone is normalized to E.164. Contact fields fill only when the matched contact has them empty. external_id is stored on the contact as customFields.external_id and is not replaced once set. If the WhatsApp identity is already linked to a contact, that contact is the one returned.
Template params you send win. Blank names that match a contact field (first_name, full_name, email, and the other contact variables) are filled from the contact. Anything still blank returns 422 template_params_missing.
A 201 body looks like this:
{
"success": true,
"data": {
"contact": { "id": "contact_id", "created": true },
"conversation": { "id": "conversation_id", "created": true },
"message": {
"id": "message_id",
"status": "sent",
"template": { "name": "consulta_inicio", "language": "es" }
},
"idempotent": false
}
}Read the status later with GET /api/v1/whatsapp/messages/{id}. Status moves through sending, sent, delivered, read, or failed.
CRM recipe
- On a new lead,
POSTwith a stableIdempotency-Key(the CRM lead id plus the template name is enough). - Store
contact.id,conversation.id, andmessage.id. - Keep your existing pull for answers. This call does not add a per-message webhook.
- Repeat the same key after a timeout. You get
idempotent: trueand no second message. - If the person is already chatting, you get
409 conversation_active. Do not retry until that chat is idle.
An API send never switches the conversation to human and never notifies agents.
Limits
| Limit | Window |
|---|---|
| 60 sends per API key | 1 hour |
| 3 sends per workspace and phone | 24 hours |
These sit on top of the plan's per-minute API limit, the outbound risk check, and the credit check.
Errors
| Code | HTTP | When |
|---|---|---|
validation_failed | 400 | Bad body, invalid phone, or missing Idempotency-Key |
feature_not_available | 403 | Plan lacks the inbox |
opted_out | 409 | The person opted out |
connection_inactive | 409 | No active WhatsApp connection |
conversation_active | 409 | Inbound inside 24 hours, or the conversation is in human mode |
risk_blocked | 409 | Outbound risk check blocked the send |
connection_required | 422 | More than one connection and connection_id was omitted |
template_not_available | 422 | Template is missing, not approved, or the language does not match |
template_category_not_allowed | 422 | Template category is not UTILITY or AUTHENTICATION |
template_params_missing | 422 | A template variable is still empty |
plan_limit_exceeded | 402 | Contact limit, or no credits (details.detail is insufficient_credits) |
rate_limited | 429 | Key cap or phone cap |
send_failed | 502 | WhatsApp rejected the send. details.message_id is the stored message |