# Record consent

Stores publisher consent state for Script Tag and custom consent flows.

Optional session/user-level consent sync. It is separate from request-level placement consent and is not required before /v1/placements when that request already carries consent flags.

| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| POST | /v1/consent | Secret key, or activation token for browser flows | beta |

## Request fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| client_id | string | Yes | Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_.... |
| session_id | string | Yes | Publisher session identifier. |
| decision | string | Yes | Consent decision: personalized, basic, or custom. |
| source | string | Yes | Consent source: publisher_custom, server_sync, or wavebird_dialog. Compatibility aliases publisher and custom_dialog normalize to publisher_custom but are not recommended for new integrations. |
| purposes | object | No | Purpose-level consent flags: semantic_targeting, session_persistence, cross_session_persistence, and prompt_shared. Compatibility purposes.ads maps to semantic_targeting and purposes.measurement maps to session_persistence. |

## Response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| consent_id | string | Yes | Stored consent record identifier. |
| session_id | string | Yes | Publisher session identifier for the stored consent state. |
| expires_at | string | Yes | ISO timestamp when the stored consent state expires. |
| normalized.source | string | Yes | Canonical source after compatibility alias mapping. |
| normalized.purposes | object | No | Canonical purpose flags after compatibility ads/measurement mapping. |

## Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing auth. |
| 403 | forbidden | Publishable key origin is not allowed, or raw publishable-key auth was used instead of a browser activation token. |
| 400 | validation_error | Consent purposes are missing or malformed. |

## Response example

```json
{
  "ok": true,
  "consent_id": "consent_demo_123",
  "session_id": "sess_demo_123",
  "expires_at": "2026-08-02T08:31:00.000Z",
  "normalized": {
    "source": "publisher_custom",
    "purposes": {
      "semantic_targeting": false,
      "session_persistence": true,
      "cross_session_persistence": false,
      "prompt_shared": false
    }
  }
}
```

## Placement consent vs /v1/consent sync
Request-level placement consent is sent inside /v1/placements and controls per-request privacy and targeting behavior. It is not required to call /v1/consent first when the placement request already carries request-level consent.
/v1/consent is optional session/user-level consent sync for external CMP or publisher consent state.

| Consent path | Location | Purpose | Required before placement |
| --- | --- | --- | --- |
| Request-level placement consent | inside /v1/placements | per-request privacy and targeting signal | No |
| Consent sync | /v1/consent | session/user-level CMP or publisher consent state | Optional |

## Canonical /v1/consent sync body and compatibility aliases
New integrations should use this canonical /v1/consent sync body when syncing session/user-level CMP or publisher consent state.
Compatibility aliases remain supported after the canonical form: source: "publisher" normalizes to publisher_custom, purposes.ads maps to semantic_targeting, and purposes.measurement maps to session_persistence.

```json
{
  "client_id": "wbproj_...",
  "session_id": "sess_...",
  "decision": "custom",
  "source": "publisher_custom",
  "purposes": {
    "semantic_targeting": false,
    "session_persistence": true,
    "cross_session_persistence": false,
    "prompt_shared": false
  }
}
```

## cURL

```bash
curl -X POST https://api.wavebird.ai/v1/consent \
  -H "Authorization: Bearer sk_test_wavebird_demo_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "wbproj_demo_8jK42",
    "session_id": "sess_demo_123",
    "decision": "custom",
    "source": "publisher_custom",
    "purposes": {
      "semantic_targeting": false,
      "session_persistence": true,
      "cross_session_persistence": false,
      "prompt_shared": false
    }
  }'
```

## Node

```javascript
const response = await fetch("https://api.wavebird.ai/v1/consent", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_wavebird_demo_secret",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
  client_id: "wbproj_demo_8jK42",
  session_id: "sess_demo_123",
  decision: "custom",
  source: "publisher_custom",
  purposes: {
    semantic_targeting: false,
    session_persistence: true,
    cross_session_persistence: false,
    prompt_shared: false
  }
})
});

const data = await response.json();
console.log(data);
```

## Python

```python
import json
import urllib.request

request = urllib.request.Request(
    "https://api.wavebird.ai/v1/consent",
    data=json.dumps({"client_id": "wbproj_demo_8jK42", "session_id": "sess_demo_123", "decision": "basic", "source": "publisher_custom", "purposes": {"semantic_targeting": False, "session_persistence": True, "cross_session_persistence": False, "prompt_shared": False}}).encode("utf-8"),
    headers={
        "Authorization": "Bearer sk_test_wavebird_demo_secret",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    print(response.read().decode("utf-8"))
```
