# Consent and DNC

**Availability: Available**

RelationsAI enforces calling rules on every call, whether it is placed from the console or the API. This guide explains the do-not-call list, consent, calling hours and AI disclosure, how to manage DNC entries through the API and what a refused call looks like.

These rules exist to protect the people you call, and breaking them can carry legal penalties for your business. No API key, scope or request option can switch them off, and the API refuses to start or resume a campaign whose do-not-call screening a person switched off in the console. For campaigns the timing matters: see [When the list takes effect](#when-the-list-takes-effect).

## Before you begin

- `dnc:read`: Listing and looking up do-not-call entries
- `dnc:write`: Adding and removing entries
- `contacts:write`: Recording consent on contacts

The workspace's compliance settings (whether consent is required, the calling window, the per-number daily limit) are set by an admin in the console. No API key can read or change them; that is deliberate.

## Rules that always apply

Every single call (placed with [`POST /calls`](/api/calls#place-a-call) or from the console) passes these checks before anything is dialled, and every campaign applies them to all of its recipients when it starts and again when it resumes. Campaigns are **not** re-checked before each call: see [When the list takes effect](#when-the-list-takes-effect). The order of the checks is:

1. **Do-not-call list**: calls to the number are refused, and campaigns leave it out when they start or resume (always, in both modes). Refusal: [`dnc_listed`](/errors/dnc_listed).
2. **Consent**: when the workspace requires it, a contact without recorded consent is not called (when an admin switched it on). Refusal: [`consent_required`](/errors/consent_required).
3. **Calling hours**: calls only inside the workspace's calling window (when an admin set a window). Refusal: [`outside_calling_hours`](/errors/outside_calling_hours).
4. **Per-number daily limit**: at most N calls to one number in a rolling 24 hours (when an admin set a limit). Refusal: [`contact_daily_limit_reached`](/errors/contact_daily_limit_reached).

You see the first rule that blocks the call. A refused call is **not placed and not charged**.

**AI disclosure**: an agent cannot be published unless its instructions tell the people it calls that they are speaking with an AI. This is checked when the agent is published; RelationsAI does not verify what is said on each call, so keep the disclosure at the start of your agent's instructions and check it in transcripts.

Test mode enforces the same rules with test-mode data: a test-mode do-not-call entry or a test-mode contact without consent refuses a test call exactly as in live mode. Test-mode entries never affect live calls, and live entries never affect test calls.

## Managing the do-not-call list

Add a number **as soon as someone asks not to be called**, from whichever channel they asked in (a call, an email, your website, an agent's note).

[`POST /dnc-entries`](/api/do-not-call-list#add-a-number-to-the-do-not-call-list) adds a number. It is safe to repeat: adding a number that is already listed returns the existing entry with `200` instead of `201`.

```bash
curl -X POST https://voice-api.relationsai.com/v1/dnc-entries \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "phone_number": "+12025550107"
}'
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/dnc-entries", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "phone_number": "+12025550107"
  }),
});
const entry = await response.json();
```
```python

response = requests.post(
    "https://voice-api.relationsai.com/v1/dnc-entries",
    headers={
        "Authorization": "Bearer <token>",
    },
    json={
        "phone_number": "+12025550107",
    },
    timeout=30,
)
entry = response.json()
```

Complete runnable example: [cURL script](/examples/guides/consent-and-dnc/add-dnc-entry.sh) · [JavaScript](/examples/guides/consent-and-dnc/add-dnc-entry.mjs) · [Python](/examples/guides/consent-and-dnc/add-dnc-entry.py)

The entry (`201`):

```json
{
  "id": "dnc_4Ty8Ui2Op6As3Df9Gh7Jk5",
  "object": "dnc_entry",
  "livemode": true,
  "phone_number": "+919000000003",
  "contact_id": "ct_5Qw8Er2Ty6Ui9Op3As7Df4",
  "created_at": null
}
```

What adding a number does, immediately and in the key's mode:

- Calls to it are refused with `dnc_listed`.
- Campaigns leave it out at start, and a paused campaign that still holds it **cannot be resumed** (see [Running campaigns](/guides/running-campaigns)).
- A campaign that is **already running** is not screened again: see below.
- If a contact has this number, the contact is marked `do_not_call: true` and its consent is withdrawn. If no contact has it, a contact holding only the number is created, so the number is visible in the console.
- `created_at` is recorded in test mode; in live mode it is `null` today because the time is not stored yet.

### When the list takes effect

The list is checked at three moments, in both modes:

| Moment | What happens to a listed number |
|---|---|
| Every [`POST /calls`](/api/calls#place-a-call) | Refused with `dnc_listed` |
| Every campaign start | Left out of the run (`suppressed`) |
| Every campaign resume | The resume is refused (`campaign_recipients_blocked`) while it is a remaining recipient |

A campaign that is **already running** is not checked again before each call. If you add a number while a campaign is running and that person has not been called yet, the run still calls them. To apply the new entry at once, **pause the campaign and resume it** (the resume is refused while the number is a remaining recipient; then follow the remedy in [Running campaigns](/guides/running-campaigns)), or **stop it**. Test mode behaves the same way, so you can try this safely.

Matching ignores formatting: a number matches by its digits, and by its last ten digits when both numbers have at least ten. So `+919000000003` also blocks a contact stored as `9000000003`.

To check one number, list entries filtered by it:

```bash
curl -X GET "https://voice-api.relationsai.com/v1/dnc-entries?phone_number=%2B12025550107" \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/dnc-entries?phone_number=%2B12025550107", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const entries = await response.json();
```
```python

response = requests.get(
    "https://voice-api.relationsai.com/v1/dnc-entries",
    headers={
        "Authorization": "Bearer <token>",
    },
    params={
        "phone_number": "+12025550107",
    },
    timeout=30,
)
entries = response.json()
```

A listed number comes back as one entry in `data`; an empty `data` means the number is not on the list.

Complete runnable example: [cURL script](/examples/guides/consent-and-dnc/check-number.sh) · [JavaScript](/examples/guides/consent-and-dnc/check-number.mjs) · [Python](/examples/guides/consent-and-dnc/check-number.py)

You do not need to check before calling: the API checks every call. Check when you want to show the status in your own system.

To mirror your list into RelationsAI, page through [`GET /dnc-entries`](/api/do-not-call-list#list-or-look-up-do-not-call-entries) (a stable order; live entries have no time added yet) and add what is missing on your side, and add every number your system marks as do-not-call. Keep the two lists in sync in both directions.

## Removing an entry

[`DELETE /dnc-entries/{dnc_entry_id}`](/api/do-not-call-list#remove-a-number-from-the-do-not-call-list) removes a number from the list, so it can be called again. Removal should be rare and deliberate: do it only when the entry was added by mistake, or the person has clearly asked to be called again. Keep a record on your side of who asked and when.

```bash
curl -X DELETE https://voice-api.relationsai.com/v1/dnc-entries/dnc_4Ty8Ui2Op6As3Df9Gh7Jk5 \
     -H "Authorization: Bearer <token>"
```
```js
const response = await fetch("https://voice-api.relationsai.com/v1/dnc-entries/dnc_4Ty8Ui2Op6As3Df9Gh7Jk5", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer <token>",
  },
});
const deleted = await response.json();
```
```python

response = requests.delete(
    "https://voice-api.relationsai.com/v1/dnc-entries/dnc_4Ty8Ui2Op6As3Df9Gh7Jk5",
    headers={
        "Authorization": "Bearer <token>",
    },
    timeout=30,
)
deleted = response.json()
```

Response `200`:

```json
{
  "id": "dnc_4Ty8Ui2Op6As3Df9Gh7Jk5",
  "object": "dnc_entry",
  "livemode": true,
  "deleted": true
}
```

Complete runnable example: [cURL script](/examples/guides/consent-and-dnc/remove-dnc-entry.sh) · [JavaScript](/examples/guides/consent-and-dnc/remove-dnc-entry.mjs) · [Python](/examples/guides/consent-and-dnc/remove-dnc-entry.py)

- The removal is recorded in the workspace audit log with the API key that made it.
- **Consent is not restored.** If the workspace requires consent, record it again on the contact only if the person gives it.
- A contact whose number is on the list cannot be deleted ([`contact_on_dnc_list`](/errors/contact_on_dnc_list)), so deleting a contact can never quietly remove a protection.

## Consent

When an admin switches on the consent requirement, a call to a number that matches a contact whose `consent_granted` is `false` is refused with `consent_required`. A number that matches no contact is not blocked by this rule: there is no consent record to contradict.

Record consent through the contacts API, from your own records of when and how the person agreed:

```json
{ "phone_number": "+12025550100", "consent_granted": true }
```

sent to [`POST /contacts/upsert`](/api/contacts#create-or-update-a-contact-by-phone-number) (or `PATCH` on the contact). `true` is recorded with the time and the API as its source; `false` records a withdrawal. Only set `true` when the person really consented. Setting consent to make a refusal go away is exactly what the rule exists to prevent.

Consent is withdrawn automatically when a number is added to the do-not-call list. **The do-not-call list always wins**: while a number is listed, calls to it are refused whatever `consent_granted` says, and recording consent for it does not make it callable. Removing the entry later does not restore consent either.

## Calling hours and the daily limit

- The **calling window** is evaluated in the timezone of the workspace's calling region, which is not necessarily the `timezone` shown on [`GET /workspace`](/api/workspace#retrieve-the-workspace), and never the contact's. A refusal's `details` gives the window and the timezone it was evaluated in, for example `{"window": "09:00-21:00", "timezone": "Asia/Kolkata"}`. If the window is switched on with no hours set, every call is refused (`details.window` is `null`).
- Campaigns: a start or resume outside the window is refused as a whole.
- The **per-number daily limit** counts calls to one number over the last 24 hours, from the console, the API and campaigns. It is separate from the workspace's daily limit of calls placed through the API ([`daily_call_limit_reached`](/errors/daily_call_limit_reached)).

## Refusals

A refusal is a normal API error with `retry: "no"`:

```json
{
  "error": {
    "code": "dnc_listed",
    "message": "This number is on your do-not-call list, so the call was not placed.",
    "details": {},
    "request_id": "req_k3m7q2x9w4z8b5n6c1v0h2j7",
    "retry": "no",
    "doc_url": "https://docs.relationsai.com/errors/dnc_listed"
  }
}
```

What each refusal means and who can change it:

- `dnc_listed`: The person asked not to be called. Do not call. Mark the record as do-not-call in your system. Who can change it: Nobody, unless the entry was a mistake.
- `consent_required`: Consent is required and this contact has none. Get consent through your own process first. Who can change it: Your team, with the person.
- `outside_calling_hours`: Outside the calling window. Retry inside the window in `details`. Who can change it: An admin can change the window in the console.
- `contact_daily_limit_reached`: Called too often in 24 hours. Try again after the window. Who can change it: An admin can review the limit.
- `campaign_recipients_suppressed`: Every campaign recipient is blocked. Choose callable recipients. Who can change it: Your team.
- `campaign_recipients_blocked`: A resume found newly blocked recipients. Create a new campaign with the still-pending recipients minus the blocked ones, then stop this one (never stop and start it: that calls everyone again). Who can change it: Your team.
- `campaign_dnc_override_enabled`: Do-not-call screening was switched off for this campaign in the console; the API will not start or resume it. Who can change it: A workspace admin, in the console.

> **Never route around a refusal:** Do not retry a refused call with a different number format, from a different workspace, as a campaign instead of a single call, or after removing a do-not-call entry or setting consent you did not receive. Record the refusal in your system as the outcome for that person. The checks are the same on every path, and working around them puts your business, not the API, in breach.

## What can go wrong

- **A number you added is still called by a running campaign**: Campaigns screen at start and at resume; calls already connected finish. Fix: Pause or stop the campaign; a resume is refused while the number is in its run.
- **[`consent_required`](/errors/consent_required) for someone who agreed**: Consent recorded in your system but not in RelationsAI. Fix: Upsert the contact with `consent_granted: true` from your record.
- **Consent was set but calls are still refused after a removal from the list**: Removing an entry does not restore consent. Fix: Record consent again only if the person gives it.
- **[`contact_limit_reached`](/errors/contact_limit_reached) on add**: Adding an unknown number creates a contact. Fix: Delete contacts you no longer need, or contact support.
- **[`insufficient_scope`](/errors/insufficient_scope)**: The key lacks `dnc:write`. Fix: Ask an admin for a key with it.

## Next steps

[Receiving webhooks](/guides/receiving-webhooks).
