Opt-in and opt-out
Record a customer's opt-in or opt-out from your own system, by phone number or contact id, with the evidence an opt-in needs, and get told when consent changes.
Every contact has a consent record: each opt-in and opt-out, in the order they happened. The contact's consent_state is worked out from that record. The latest entry wins, and nothing is ever overwritten. A program can add to the record the same way a teammate can. See Opt-outs and consent for how it works in the product.
What consent changes
- Opted out stops every message to the customer: API sends, campaigns, flows, and replies from the inbox, even inside the 24-hour window. A send to them is refused
409 contact_opted_out. - Opted in is recorded with its evidence, so you can show the customer agreed.
- Not recorded (
unknown) is normal for a contact added by a send, a flow or an import without consent. It doesn't block anything: campaigns include these contacts too. A customer whose first message created the contact is opted in: the message is the evidence.
Record consent by phone number
When you hold a phone number rather than a contact id, for example from a sign-up form or a do-not-contact list, send it to POST /contacts/consent. If your organisation doesn't have a contact with that number yet, one is created.
curl -X POST https://iconengage.com/api/public/v1/contacts/consent \
-H "Authorization: Bearer $ICONENGAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+91XXXXXXXXXX",
"kind": "opted_in",
"evidence": { "url": "https://example.com/signup", "collected_at": "2026-10-22T09:15:00Z" }
}'to: the phone number in international format, with+and the country code, as a send takes it. Or usewa_idwith the digits only ("91XXXXXXXXXX"). Send one of the two, not both.kind:opted_inoropted_out.evidence: what shows the customer agreed. An opt-in needs it; an opt-out doesn't. Give aurl, aform_id, or amethod(web_form,whatsapp_message,paper,verbal,apiorimport_file) and when it wascollected_at. You can add areferenceand anote.reason(optional): up to 256 characters, for your team.occurred_at(optional): when the customer made the choice, if earlier than now. It can't be in the future.
The answer is 201 with the contact and the new state:
{
"contact_id": "2a7d…",
"contact_created": true,
"consent_state": "opted_in",
"consent_updated_at": "2026-10-22T09:15:04Z",
"event": { "id": "9c1e…", "kind": "opted_in", "source": "api", "occurred_at": "2026-10-22T09:15:04Z" }
}contact_created is true when this call created the contact. A contact created this way doesn't need your organisation's required custom fields, just as a contact created by a send doesn't.
The key needs contacts.update and contacts.create, whether or not the number is new. A key made with the Send WhatsApp messages preset has contacts.create but not contacts.update; one made with Send messages and sync contacts has both. See Create and manage API keys.
Record consent by contact id
If you already have the contact's id, POST /contacts/{contact_id}/consent takes the same body without to or wa_id. It needs contacts.update.
Refusals
| Status | code | Meaning |
|---|---|---|
400 | consent_evidence_required | An opt-in without evidence. Nothing is recorded and no contact is created. |
400 | validation_failed | A field is wrong: both or neither of to and wa_id, a number without + (to_needs_country_code), a kind other than opted_in or opted_out, or an evidence field that doesn't fit. details.fields names each one. |
403 | permission_denied | The key lacks contacts.update or contacts.create. details.permission names it. |
Every refusal is made before anything is written, so a refused call never adds a contact.
Retries
Consent calls take no Idempotency-Key. If you retry one, another entry with the same state is added to the record, and another webhook event is sent. That's harmless for the contact's state, which is the latest entry. Each event's id belongs to one entry, so dedupe redeliveries on it, and treat the contact's consent as the latest event by occurred_at, not as a count of events.
Get told when consent changes
Subscribe a webhook to API: contact opted out (contact.opted_out) and API: contact opted in (contact.opted_in). Each delivery names the contact and how the change was recorded: inbound for a customer's own message (a stop word, or the first message they send you), manual for a teammate, import, or api. See Event webhooks.
To read the current state, GET /contacts/{contact_id} returns consent_state.
Still need help?
Write to us at [email protected] and tell us what you were trying to do. Already a customer? Sign in to IconEngage — the same articles are under Help in the app.