# Import many consents at once (CSV upload, CRM export)

Source: https://docs.usetone.ai/reference/compliance/consent.recordMany

> Up to 1,000 per request.

`POST /v1/consent/bulk`
Up to 1,000 per request. Rows are accepted INDIVIDUALLY: a record that violates policy (an inferred consent missing `expiresAt`, a number inside its 90-day opt-out lockout) is refused with its index and reason while the rest import — fix and resubmit only the rejected rows. 🔴 Set `capturedAt` on every imported record: it defaults to now, which dates your whole back catalogue to the day of the import.

Requires the `write` scope.

## Request body

| Field | Type | | Meaning |
|---|---|---|---|
| `records` | object[] | required | The consents to import, 1-1000. Rows are accepted individually — the response lists exactly which were rejected and why, so you can fix and resubmit only those. |
| `records[].capturedAt` | string | optional | 🔴 When the RECIPIENT consented — not when you called this endpoint. Defaults to now, which is correct only for a live capture: importing a back catalogue without this dates every record to the day of the import and makes the 7-day transactional clock meaningless. |
| `records[].e164` | string | required | The number that consented, in E.164. |
| `records[].evidenceRef` | string | optional | Where the proof lives — a form submission id, a recording URL, a DCA reference. Tone stores the pointer, never the artefact. |
| `records[].expiresAt` | string | optional | When the consent lapses. Optional for `explicit` consent, where policy fills it — capped at seven days for a transactional purpose, otherwise until revoked. REQUIRED for `inferred` consent, which lasts exactly as long as the relationship, and only you know when that ends; omitting it there is a 422. |
| `records[].kind` | string | required | How the consent arose. `explicit` is a positive act by the recipient; `inferred` rests on an existing relationship and MUST carry its own `expiresAt`. Open set — handle an unrecognised value rather than throwing. Today: `explicit`, `inferred`. |
| `records[].purpose` | string | required | What they consented to be contacted about. Consent is purpose-scoped: a record for one purpose does not satisfy the gate for another. Open set — handle an unrecognised value rather than throwing. Today: `promotional`, `service`, `transactional`, `collections`. |
| `records[].scope` | string | optional | What the recipient actually agreed to, in their words or yours. |
| `records[].source` | string | required | Which channel captured it. Recorded verbatim and returned in the evidence pack. Open set — handle an unrecognised value rather than throwing. Today: `api`, `web_form`, `ivr`, `dtmf`, `otp`, `dca`, `import`. |

### Example request

```json
{
  "records": [
    {
      "capturedAt": "2026-08-20T11:02:00Z",
      "e164": "+919876543210",
      "evidenceRef": "form-sub-88213",
      "expiresAt": "2026-08-27T11:02:00Z",
      "kind": "explicit",
      "purpose": "promotional",
      "scope": "Order updates and delivery reminders for orders placed on acme.example.",
      "source": "web_form"
    }
  ]
}
```

## Response

| Field | Type | | Meaning |
|---|---|---|---|
| `imported` | integer | required | How many records were written. All accepted rows land in one transaction. |
| `rejected` | object[] | required | The refused rows. Fix these and resubmit only them — the accepted rows are already saved. |
| `rejected[].code` | string | required | Machine-readable refusal code — `conflict` for a number inside its opt-out lockout, `unprocessable` for a policy violation. |
| `rejected[].e164` | string | required | The number on the refused record. |
| `rejected[].index` | integer | required | Position of the refused record in your `records` array, zero-based. |
| `rejected[].message` | string | required | Why the record was refused, in words. |

### Example response

```json
{
  "data": {
    "imported": 0,
    "rejected": [
      {
        "code": "conflict",
        "e164": "+919876543210",
        "index": 0,
        "message": "This number opted out and is inside the 90-day re-consent lockout."
      }
    ]
  }
}
```
