# Record a preference

`POST /v1/preferences`

Records one statement, a grant or an opt-out, for a handle on one channel. Writing is an upsert: the key is the channel, handle, and optional sender scope, and a new statement replaces the key's current one.

Statements are ordered by when they were made, not when they arrive. A statement older than the key's current one is refused and returned with `applied: false` alongside the statement that survived; refusals are recorded on the key's history. Granting over a stored opt-out needs `consented_at` later than the opt-out, and a person's own opt-out (an unsubscribe, a stop keyword) cannot be overridden by a grant asserted on their behalf.

A `201` means this key had no record and one was created; a `200` returns the key's surviving record, whether this statement replaced it, repeated it, or was refused.

## Code samples

### TypeScript

```ts
const result = await bird.preferences.create({
  channel: "sms",
  handle: "+15550001234",
  status: "revoked",
});
console.log(result.applied, result.preference?.id);
```

### Python

```py
result = client.preferences.create(
    channel="email",
    handle="jane@acme.com",
    status="granted",
    consented_at="2026-08-20T14:03:10Z",
    source="signup-form-v2",
)
if result.preference:
    print(result.applied, result.preference.id)
```

### Go

```go
result, err := client.Preferences.Create(context.Background(), bird.PreferencesCreateParams{
	Channel:     bird.PreferenceChannelEmail,
	Handle:      "recipient@example.com",
	Status:      bird.PreferenceStatusGranted,
	Source:      "signup-form-v2",
	ConsentedAt: time.Now(),
})
if err != nil {
	log.Fatal(err)
}
// A newer statement already on file answers Applied false instead of an
// error, with the surviving statement in Preference.
if result.Applied != nil && *result.Applied {
	fmt.Println("grant recorded")
}
```

### PHP

```php
$result = $bird->preferences->create(
    channel: 'email',
    handle: 'jane@acme.com',
    status: 'granted',
    source: 'signup-form-v2',
    consentedAt: new DateTimeImmutable('2026-08-20T14:03:10Z'),
);
echo var_export($result->getApplied(), true);
```

### CLI

```sh
bird preferences create \
  --channel sms \
  --coverage non_transactional \
  --handle +15550001234 \
  --status revoked
```

### cURL

```sh
curl -X POST "https://us1.platform.bird.com/v1/preferences" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "handle": "+15550001234",
    "status": "revoked",
    "coverage": "non_transactional"
  }'
```

## Example response `200`

```json
{
  "transition_id": "prt_01krdgeqcxet5s7t44vh8rt9mg",
  "preference": {
    "id": "prf_01krdgeqcxet5s7t44vh8rt9mg",
    "channel": "sms",
    "handle": "+15550001234",
    "sender_scope": "+15557654321",
    "topic_id": null,
    "status": "revoked",
    "coverage": "non_transactional",
    "origin": "api_key",
    "source": "signup-form-v2",
    "contact_id": "con_01krdgeqcxet5s7t44vh8rt9mg",
    "created_at": "2026-05-20T09:14:52Z",
    "updated_at": "2026-05-25T16:42:01Z"
  }
}
```

## Request body

- `handle` (string, required): Who the statement is about: an email address on the email channel, a phone number in E.164 format on SMS and WhatsApp.
- `channel` (string, required)

  The channel a preference statement applies to. A preference addresses one channel: the handle that identifies the person differs per channel, so opting out of one channel says nothing about the others. New channels can be added over time, so a value outside this list can be returned.

  Possible values (may grow over time): `email`, `sms`, `whatsapp`
- `status` (string, required)

  What the statement says: `granted` records consent to receive messages, `revoked` records an opt-out. There is no third state: a person who never stated anything simply has no preference on record.

  Possible values: `granted`, `revoked`
- `coverage` (string): How much traffic the statement covers. Defaults to `non_transactional`, which keeps transactional messages such as receipts and verification codes flowing.
- `sender_scope` (string): Limit the statement to one sender instead of the whole channel. On SMS this is the originator; on WhatsApp it identifies the business account. Not supported on email, where preferences are always channel-wide.
- `source` (string): Free-form note on where the statement came from: a form name, an import batch, a campaign. Stored verbatim and returned on the preference.
- `consented_at` (string): When the person consented, on a `granted` statement. Required evidence when granting over a stored opt-out: the grant applies only if this is later than the opt-out it reverses. May not be in the future.

## Response body

- `applied` (boolean, required): Whether the request took effect. False only when it was refused as out of order; the surviving, newer statement is returned in `preference`.
- `transition_id` (nullable string, required): Identifies this write on the key's record, for applied and refused requests alike. Null when the write was a repeat of the current statement and recorded nothing new.
- `preference` (nullable object, required): The key's surviving statement. Null after an applied delete, when the key is back to having no record.
- `preference.id` (string, required)
- `preference.channel` (string, required)
- `preference.handle` (string, required): Who the statement is about: an email address on the email channel, a phone number in E.164 format on SMS and WhatsApp.
- `preference.sender_scope` (nullable string, required): The sender the statement is limited to, or null when it covers the whole channel. On SMS this is the originator the person replied to; on WhatsApp it identifies the business account that messaged them. Email preferences are always channel-wide, so it is always null there.
- `preference.topic_id` (nullable string, required): The topic the statement is limited to, or null when it covers every topic. Part of the key that identifies a statement, alongside `sender_scope`.
- `preference.status` (string, required)
- `preference.coverage` (string, required)
- `preference.effective_at` (string, required): When the statement was made, as reported by whoever made it. This is what orders one key's statements: a write dated before this moment is refused rather than applied.
- `preference.origin` (string, required)
- `preference.source` (nullable string): Free-form note on where the statement came from, as supplied when it was recorded: a form name, an import batch, a campaign. Null when none was given.
- `preference.consented_at` (nullable string): When the person consented, as evidenced by whoever asserted the grant. Null on statements that carry no consent evidence, including every opt-out.
- `preference.contact_id` (nullable string): The contact whose handle matched when the statement was recorded. Null when no contact matched at that moment; it is not updated when contacts change later.
- `preference.created_at` (string, required)
- `preference.updated_at` (string, required)