> ## Documentation Index
> Fetch the complete documentation index at: https://omni.fireflo.au/llms.txt
> Use this file to discover all available pages before exploring further.

# POST /v1/cdp/contacts/{contact_id}/consent

> Record a contact's consent for a channel, or all, and a purpose — with proof of what they said.

Records a change to a contact's consent: an opt-in, a stop, or back to the account's default — for one channel or all of them, and for marketing, transactional messages or both. Every change is kept with its proof and never edited; a request that changes nothing records nothing. A change sends the `consent.changed` [webhook event](/developers/customer-data#webhook-events).

<Note>Needs the `cdp.contacts:write` scope.</Note>

## Path parameters

| Field | Type | Required | Notes |
| :- | :- | :- | :- |
| `contact_id` | string | Yes | The contact's id. |

## Body

| Field | Type | Required | Notes |
| :- | :- | :- | :- |
| `channel` | string | Yes | A channel people can stop — `sms`, `whatsapp`, `rcs` or `voice` (Voice), those your account has — or `all`. |
| `purpose` | string | Yes | `marketing`, `transactional` or `all`. |
| `status` | string | Yes | `granted` (opted in), `revoked` (stopped) or `cleared` (back to the account's default). |
| `proof` | string | Yes | What they said, and where. Up to 500 characters; kept with the change. |

`channel` `all` with `purpose` `all` and `status` `revoked` stops everything, as a STOP ALL reply does.

Send an `Idempotency-Key` header to record it once however often the request is retried.

## Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.fireflo.au/v1/cdp/contacts/3f6b2a1e-8c4d-4e2a-9b7f-5d1c0e8a9f42/consent" \
    -H "Authorization: Bearer $OMNI_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: consent-asha-sms-2026-10-06" \
    -d '{
      "channel": "sms",
      "purpose": "marketing",
      "status": "granted",
      "proof": "Said yes to SMS offers on a call with the Pune store, 6 Oct"
    }'
  ```

  ```python Python theme={null}
  import os

  import requests

  response = requests.post(
      "https://api.fireflo.au/v1/cdp/contacts/3f6b2a1e-8c4d-4e2a-9b7f-5d1c0e8a9f42/consent",
      headers={
          "Authorization": f"Bearer {os.environ['OMNI_API_KEY']}",
          "Idempotency-Key": "consent-asha-sms-2026-10-06",
      },
      json={
          "channel": "sms",
          "purpose": "marketing",
          "status": "granted",
          "proof": "Said yes to SMS offers on a call with the Pune store, 6 Oct",
      },
  )
  print(response.status_code, response.json())
  ```

  ```javascript Node theme={null}
  const response = await fetch("https://api.fireflo.au/v1/cdp/contacts/3f6b2a1e-8c4d-4e2a-9b7f-5d1c0e8a9f42/consent", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OMNI_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": "consent-asha-sms-2026-10-06",
    },
    body: JSON.stringify({
      "channel": "sms",
      "purpose": "marketing",
      "status": "granted",
      "proof": "Said yes to SMS offers on a call with the Pune store, 6 Oct"
    }),
  });
  console.log(response.status, await response.json());
  ```
</CodeGroup>

## Response

`200 OK` — where the contact now stands, as [GET](/api-reference/customer-data/get-consent) answers.

```json theme={null}
{
  "contact": {
    "id": "3f6b2a1e-8c4d-4e2a-9b7f-5d1c0e8a9f42",
    "name": "Asha Rao",
    "phone": "+919876543210"
  },
  "stopped_all": false,
  "require_marketing_opt_in": false,
  "channels": [
    {
      "channel": "sms",
      "marketing": {
        "allowed": true,
        "status": "granted",
        "reason": ""
      },
      "transactional": {
        "allowed": true,
        "status": "cleared",
        "reason": ""
      }
    },
    {
      "channel": "whatsapp",
      "marketing": {
        "allowed": true,
        "status": "granted",
        "reason": ""
      },
      "transactional": {
        "allowed": true,
        "status": "cleared",
        "reason": ""
      }
    },
    {
      "channel": "rcs",
      "marketing": {
        "allowed": true,
        "status": "cleared",
        "reason": ""
      },
      "transactional": {
        "allowed": true,
        "status": "cleared",
        "reason": ""
      }
    }
  ],
  "history": [
    {
      "channel": "sms",
      "purpose": "marketing",
      "status": "granted",
      "source": "api",
      "proof": "Said yes to SMS offers on a call with the Pune store, 6 Oct",
      "at": "2026-10-06T11:30:44+05:30"
    },
    {
      "channel": "whatsapp",
      "purpose": "marketing",
      "status": "granted",
      "source": "api",
      "proof": "Ticked the WhatsApp offers box at checkout on acme.in",
      "at": "2026-10-06T10:42:17+05:30"
    },
    {
      "channel": "sms",
      "purpose": "marketing",
      "status": "revoked",
      "source": "keyword",
      "proof": "Replied STOP",
      "at": "2026-09-14T19:03:51+05:30"
    }
  ]
}
```

## Errors

Every refusal is `{"error": {"code", "message", "field"}}`; `field` is there when one input is at fault.

| Status | Error code | When |
| :- | :- | :- |
| 404 | `not_found` | No contact has that id on this account. |
| 400 | `invalid_request` | `field` names `channel`, `purpose`, `status` or `proof` (missing, or not one of its values). |
| 403 | `scope_missing` | The key doesn't have the `cdp.contacts:write` scope. |
| 404 | `module_off` | The account doesn't have Customer data on. |

Any request can also be refused for its key, its account or its rate (`key_required`, `invalid_key`, `account_suspended`, `plan_excludes_api`, `address_not_allowed`, `rate_limited`); see [the overview](/api-reference/overview).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.