# Create a polling subscription over the change feed

`POST /api/v1/data/subscriptions` - [human page](https://aicdapi.com/api-reference#createFeedSubscription)

**Release status.** Limited availability

Limited, machine-only release. Creates one feed bound to the authenticated customer and to the exact API key that made the request. No caller-supplied account, customer, credential, or key identifier is accepted, and the binding and the resource filter are immutable once created. Many feeds may share one key. The name is unique per account and key among live feeds: posting the same live name with the same resource returns the existing feed instead of creating a second one, and the same name with a different resource is refused. The response carries the start cursor captured from the feed high-water position at creation, so a client that stores it can poll from that point on. If the feed state cannot be read, the request fails and creates nothing.

**Authentication.** Customer API key: `Authorization: Bearer aicd_...`. A browser session does not authenticate this endpoint. Required scope: `civic:read`.

### Request body



| Field | Type | Required | Bounds | Description |
| --- | --- | --- | --- | --- |
| `name` | string | yes | length 1..80 | A caller-chosen name, unique per account and key among live feeds, of 1 to 80 characters after trimming. |
| `resource` | any | no |  | One resource name from the civic data catalog (`CivicResource`), or null to cover every resource. Omitting the field means null. The value is pinned for the feed's life. |

Example request body:

```json
{
  "name": "county-meetings",
  "resource": "meetings"
}
```

### Example request

```bash
curl -X POST 'https://aicdapi.com/api/v1/data/subscriptions' \
  -H "Authorization: Bearer $AICD_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"name":"county-meetings","resource":"meetings"}'
```

### Response 200

This name and resource already had a live feed for this account and key, so the existing feed is returned rather than a second one being created.

| Field | Type | Required | Bounds | Description |
| --- | --- | --- | --- | --- |
| `data` | object | yes |  |  |
| `data.id` | string (uuid) | yes | pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\|00000000-0000-0000-0000-000000000000\|ffffffff-ffff-ffff-ffff-ffffffffffff)$ | Server-assigned subscription id. Stable for the feed's life. |
| `data.name` | string | yes | length 1..80 | The caller-chosen name, unique per account and key among live feeds. |
| `data.resource` | any | yes |  | The pinned resource filter, which the poll request cannot override. One of the civic resource names the `CivicResource` component lists, or null for every resource. |
| `data.keyId` | string (uuid) | yes | pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\|00000000-0000-0000-0000-000000000000\|ffffffff-ffff-ffff-ffff-ffffffffffff)$ | The credential this feed is bound to, fixed for the feed's life. Only this exact credential can poll it: a replacement key must register a new feed, and may carry a retained cursor position from the old feed onto it while that position is still replayable. |
| `data.startCursor` | string | yes | length 1..512 | The change-feed position captured when the feed was created. Polling without a cursor starts here, and it is subject to the same replay retention as every other position: once it is older than the retained floor, a poll is refused with 410 and the feed must be reconciled from a fresh snapshot boundary. |
| `data.createdAt` | string (date-time) | yes | pattern ^(?:(?:\\d\\d[2468][048]\|\\d\\d[13579][26]\|\\d\\d0[48]\|[02468][048]00\|[13579][26]00)-02-29\|\\d{4}-(?:(?:0[13578]\|1[02])-(?:0[1-9]\|[12]\\d\|3[01])\|(?:0[469]\|11)-(?:0[1-9]\|[12]\\d\|30)\|(?:02)-(?:0[1-9]\|1\\d\|2[0-8])))T(?:(?:[01]\\d\|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z\|([+-](?:[01]\\d\|2[0-3]):[0-5]\\d)))$ | UTC timestamp ending in Z. |

Example response:

```json
{
  "data": {
    "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
    "name": "county-meetings",
    "resource": "meetings",
    "keyId": "9c858901-8a57-4791-81fe-4c455b099bc9",
    "startCursor": "v1.eyJzZXF1ZW5jZSI6MTI4fQ",
    "createdAt": "2026-09-28T12:00:00Z"
  }
}
```

### Response 201

Feed created, with the start position captured from the feed high-water mark.

| Field | Type | Required | Bounds | Description |
| --- | --- | --- | --- | --- |
| `data` | object | yes |  |  |
| `data.id` | string (uuid) | yes | pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\|00000000-0000-0000-0000-000000000000\|ffffffff-ffff-ffff-ffff-ffffffffffff)$ | Server-assigned subscription id. Stable for the feed's life. |
| `data.name` | string | yes | length 1..80 | The caller-chosen name, unique per account and key among live feeds. |
| `data.resource` | any | yes |  | The pinned resource filter, which the poll request cannot override. One of the civic resource names the `CivicResource` component lists, or null for every resource. |
| `data.keyId` | string (uuid) | yes | pattern ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\|00000000-0000-0000-0000-000000000000\|ffffffff-ffff-ffff-ffff-ffffffffffff)$ | The credential this feed is bound to, fixed for the feed's life. Only this exact credential can poll it: a replacement key must register a new feed, and may carry a retained cursor position from the old feed onto it while that position is still replayable. |
| `data.startCursor` | string | yes | length 1..512 | The change-feed position captured when the feed was created. Polling without a cursor starts here, and it is subject to the same replay retention as every other position: once it is older than the retained floor, a poll is refused with 410 and the feed must be reconciled from a fresh snapshot boundary. |
| `data.createdAt` | string (date-time) | yes | pattern ^(?:(?:\\d\\d[2468][048]\|\\d\\d[13579][26]\|\\d\\d0[48]\|[02468][048]00\|[13579][26]00)-02-29\|\\d{4}-(?:(?:0[13578]\|1[02])-(?:0[1-9]\|[12]\\d\|3[01])\|(?:0[469]\|11)-(?:0[1-9]\|[12]\\d\|30)\|(?:02)-(?:0[1-9]\|1\\d\|2[0-8])))T(?:(?:[01]\\d\|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z\|([+-](?:[01]\\d\|2[0-3]):[0-5]\\d)))$ | UTC timestamp ending in Z. |

Example response:

```json
{
  "data": {
    "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
    "name": "county-meetings",
    "resource": "meetings",
    "keyId": "9c858901-8a57-4791-81fe-4c455b099bc9",
    "startCursor": "v1.eyJzZXF1ZW5jZSI6MTI4fQ",
    "createdAt": "2026-09-28T12:00:00Z"
  }
}
```

### Errors

| Status | `error.code` | Description |
| --- | --- | --- |
| **400** | `invalid_parameter` | The body is not valid JSON, contains an unknown key, carries a name that is empty or longer than 80 characters, or names a resource that does not exist. |
| **401** | `unauthorized` | The API key is missing, malformed, unknown, revoked, expired, or belongs to a customer that is not active. |
| **403** | `billing_channel_unset`, `billing_entitlement_required`, `feed_subscription_plan_required`, `forbidden` | The key lacks the required grant, the account has no billing channel while billing enforcement is on, is not paid through, or is not on a current Pro or Ultra paid tier. |
| **409** | `feed_subscription_conflict`, `feed_subscription_limit_reached` | A live feed already uses this name for this account and key with a different resource, or the account already holds the maximum live subscriptions for its paid tier. |
| **413** | `request_too_large`, `response_too_large` | The request body or the serialized response exceeds its size cap. |
| **429** | `rate_limited` | Either a per-account allowance was exhausted or the account-wide abuse ceiling was reached. The abuse ceiling is account-wide, is not charged against the paid quota, and is charged after the identity lookup and before a revoked, expired, unpaid, or scope-denied rejection is decided. |
| **500** | `internal_error` | Unexpected failure. No private database detail is returned. |
| **503** | `authentication_unavailable`, `feed_unavailable` | Authentication, limit store, or feed state is unavailable. |

### Behaviour

- **Limits.** Counted against the per-account minute, burst, and response-byte allowances shared by every key of the account. A subscription carries no budget of its own: subscribing does not add, reserve, or partition paid usage, and the account's admitted-request and byte allowances are what a poll consumes. The account-wide abuse ceiling applies here as it does to the rest of the machine API: it is account-wide, is not charged against the paid quota, and is charged before a revoked, expired, unpaid, or scope-denied rejection is decided.
- **Retry-After.** Sent on 429 only, as whole seconds.
- **Caching.** Customer feed responses must not be cached.
