# Drains

## Drain Model

### Fields

- **`object`** `"drain"`

- **`id`** `string`

   Unique identifier, prefixed with `drn_`.

- **`project_id`** `string`

- **`team_id`** `string`

- **`name`** `string`

- **`collection_id`** `string | null`

   The collection whose records are forwarded. Null when the drain reads from a view.

- **`view_id`** `string | null`

   The view whose records are forwarded. Null when the drain reads from a collection.

- **`destination_url`** `string`

   The HTTPS URL records are delivered to.

- **`headers_configured`** `string[]`

   Names of the headers sent with every delivery. The values are stored encrypted and are never
   returned, so this shows what was configured without revealing the secrets.

- **`body_format`** [`DrainBodyFormat`](/api/drains#drain-body-format)

   The shape of each delivered request body.

- **`compression`** [`DrainCompression`](/api/drains#drain-compression)

   The compression applied to each delivered request body.

- **`backfill_enabled`** `boolean`

   Whether records that already existed when the drain first activated were delivered too. Decided
   once at first activation and cannot be changed afterwards.

- **`exclude_fields`** `string[]`

   Top-level field names removed from every record before delivery, for destinations that reject
   fields they do not expect. Empty means nothing is removed.

- **`status`** [`DrainStatus`](/api/drains#drain-status)

   Where the drain is in its lifecycle, from unverified through delivering or paused.

- **`schedule_cron`** `string`

   Cron expression setting how often undelivered records are looked for, evaluated in UTC. A drain
   that is behind keeps delivering without waiting for the next run, so this controls how quickly
   new records are picked up rather than how fast a backlog clears.

- **`next_send_at`** [`ISODateString | null`](/api/drains#iso-date-string)

   When the drain is next due to look for records. Null means no time is set rather than nothing
   scheduled: on an active drain it is due immediately, and on a stopped one it is ignored.
   Whether a drain runs at all is `status`.

- **`verified_at`** [`ISODateString | null`](/api/drains#iso-date-string)

   When the destination was proven reachable. Null until the drain is verified.

- **`last_success_at`** [`ISODateString | null`](/api/drains#iso-date-string)

   When the destination last accepted a delivery. Null if it never has.

- **`last_failure_at`** [`ISODateString | null`](/api/drains#iso-date-string)

   When a delivery last failed. Null if none ever has.

- **`consecutive_failure_count`** `number`

   How many deliveries have failed in a row. Resets to zero on the next success.

- **`last_error`** `string | null`

   What went wrong on the most recent failed delivery. Null if none ever has.

- **`last_batch_size`** `number`

   How many records were in the most recent successful delivery.

- **`records_sent_hourly`** `DrainHourlyRecords[]`

   The last 24 hours of delivery volume, oldest first. Always 24 entries, so an hour with no
   deliveries reads as a zero rather than being absent, and a drain that has never sent still
   returns a full window of zeros. Sum the counts for "records sent in the last 24 hours".

- **`created_at`** [`ISODateString`](/api/drains#iso-date-string)

- **`updated_at`** [`ISODateString`](/api/drains#iso-date-string)

### Referenced Types

#### ISODateString

`ISODateString`

An ISO 8601 date-time string returned at the JSON API boundary.

#### DrainBodyFormat

`"ndjson" | "json"`

#### DrainCompression

`"none" | "gzip"`

#### DrainStatus

`"draft" | "pending_verification" | "active" | "paused" | "disabled"`

## List Drains

### Endpoint

Retrieve a list of drains for a project.

```http
GET /v1/projects/:project_id/drains
```

**Scope:** `drains:read`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

### Query Parameters

- **`order_by`** `string`
  Field the results are ordered by. Optional. Defaults to `"created_at"`. Allowed values: `"name"`, `"created_at"`.

- **`status`** [`DrainStatus`](/api/drains#drain-status)
  Return only drains in this state. Optional.

- **`collection_id`** `string`
  Return only drains that read from this collection. Optional.

- **`view_id`** `string`
  Return only drains that read from this view. Optional.

- **`limit`** `number`
  Maximum number of items to return. Optional. Defaults to `25`. Minimum: `1`. Maximum: `200`.

- **`after`** `string`
  Cursor from `pagination.next_cursor` of a previous response. Returns the resources after that page. Optional.

- **`before`** `string`
  Cursor from `pagination.prev_cursor` of a previous response. Returns the resources before that page. Optional.

- **`sort`** `string`
  Sort direction for the result set. Optional. Defaults to `"desc"`. Allowed values: `"asc"`, `"desc"`.

### Response

```ts
{
  message: string;
  data: Drain[];
  status: 200;
  error: null;
  pagination: Pagination;
  endpoint: string;
}
```

### Comments

- `after` and `before` are mutually exclusive.

## Retrieve Drain

### Endpoint

Retrieve a single drain.

```http
GET /v1/projects/:project_id/drains/:drain_id
```

**Scope:** `drains:read`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Response

Drain retrieved

```ts
{
  message: string;
  data: Drain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

## Create Drain

### Endpoint

Create a drain that forwards records from one collection or view to an external destination.

```http
POST /v1/projects/:project_id/drains
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

### Request Body

- **`name`** `string` -- **Required**
  Name shown for this drain. Minimum length: `2`. Maximum length: `128`.

- **`collection_id`** `string | null`
  Collection whose records are forwarded. Set this or `view_id`, not both. Optional.

- **`view_id`** `string | null`
  View whose records are forwarded. Set this or `collection_id`, not both. Optional.

- **`destination_url`** `string` -- **Required**
  HTTPS URL records are delivered to. Must resolve to a public address. Minimum length: `1`. Maximum length: `2048`.

- **`headers`** `object | null`
  Headers sent with every delivery, for authenticating to the destination. Stored encrypted and never returned; responses list only the header names, as `headers_configured`. Optional.

- **`body_format`** [`DrainBodyFormat`](/api/drains#drain-body-format)
  Shape of each delivered request body. Optional.

- **`compression`** [`DrainCompression`](/api/drains#drain-compression)
  Compression applied to each delivered request body. Optional.

- **`backfill_enabled`** `boolean`
  Whether records that already existed when the drain first activates are delivered too. Decided once at first activation and cannot be changed afterwards. Optional.

- **`exclude_fields`** `string[]`
  Top-level field names stripped from every record before delivery, for destinations that reject fields they do not expect. Optional.

- **`schedule_cron`** `string`
  Cron expression setting how often undelivered records are looked for, evaluated in UTC. Defaults to hourly. A drain that is behind keeps delivering without waiting for the next run, so this sets how quickly new records are picked up, not how fast a backlog clears. Optional. Minimum length: `1`. Maximum length: `128`.

### Response

```ts
{
  message: string;
  data: Drain;
  status: 201;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- The destination must be reachable over HTTPS at a public address. Private, loopback and link-local addresses are rejected.
- A new drain starts unverified and sends nothing. Verify it before it delivers records.
- A destination that is Tailglow ingest or a domain this team has verified is trusted, so it skips verification and is activated on create.
- A trusted destination that would write back into the same collection the drain reads from is rejected, because it would loop.
- Exactly one of `collection_id` or `view_id` must be set. A drain reads from one container.
- `headers` accepts at most 20 entries.
- `exclude_fields` accepts at most 50 entries, each a top-level key of at most 256 characters. Dots, whitespace, wildcards and newlines are rejected. Entries are trimmed and duplicates removed.
- `schedule_cron` accepts a five-field expression evaluated in UTC. Second-level precision and hashed fields are rejected; the shortest supported gap is one minute.

## Update Drain

### Endpoint

Update a drain's name, payload format, compression, or excluded fields.

```http
POST /v1/projects/:project_id/drains/:drain_id
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Request Body

- **`name`** `string`
  Name shown for this drain. Optional. Minimum length: `2`. Maximum length: `128`.

- **`body_format`** [`DrainBodyFormat`](/api/drains#drain-body-format)
  Shape of each delivered request body. Optional.

- **`compression`** [`DrainCompression`](/api/drains#drain-compression)
  Compression applied to each delivered request body. Optional.

- **`exclude_fields`** `string[]`
  Top-level field names stripped from every record before delivery, for destinations that reject fields they do not expect. Optional.

- **`schedule_cron`** `string`
  Cron expression setting how often undelivered records are looked for, evaluated in UTC. Defaults to hourly. A drain that is behind keeps delivering without waiting for the next run, so this sets how quickly new records are picked up, not how fast a backlog clears. Optional. Minimum length: `1`. Maximum length: `128`.

### Response

Drain updated

```ts
{
  message: string;
  data: Drain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- `destination_url`, `headers` and `backfill_enabled` cannot be changed. Changing the URL or headers would invalidate the proof of access established at verification, and backfill is decided once when the drain first activates. Delete the drain and create a new one instead.
- At least one field must be provided.
- Any field not listed here is rejected, including `destination_url`, `headers` and `backfill_enabled`, which cannot be changed after create.
- `exclude_fields` accepts at most 50 entries, each a top-level key of at most 256 characters. Dots, whitespace, wildcards and newlines are rejected. Entries are trimmed and duplicates removed.
- `schedule_cron` accepts a five-field expression evaluated in UTC. Second-level precision and hashed fields are rejected; the shortest supported gap is one minute.

## Delete Drain

### Endpoint

Delete a drain and stop all delivery to its destination.

```http
DELETE /v1/projects/:project_id/drains/:drain_id
```

**Scope:** `drains:delete`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Response

Drain deleted

```ts
{
  message: string;
  data: null;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

## Verify Drain

### Endpoint

Send a verification marker to the drain's destination and return the destination's response.

```http
POST /v1/projects/:project_id/drains/:drain_id/verify
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Response 200 (1)

Drain auto-verified — destination is infrastructure you control.

```ts
{
  message: string;
  data: Drain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Response 200 (2)

Verification marker sent

```ts
{
  message: string;
  data: { drain: Drain; response: { status: number; status_text: string; ok: boolean; response_time_ms: number; body_preview: string; } };
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

#### Additional Response Fields

- **`drain`** [`Drain`](/api/drains#model) -- **Required**
  The drain as it stands after the verification attempt.

- **`response`** `{ status: number; status_text: string; ok: boolean; response_time_ms: number; body_preview: string; }` -- **Required**
  What the destination replied with, including a non-2xx status.

### Comments

- Proving control of the destination takes two steps. This one delivers a marker token to the destination; read that token from what the destination received and send it back to `verify_confirm`.
- The marker token is never included in this response. Reading it from the destination is what proves access, so echoing it here would defeat the check.
- A destination that has since become trusted infrastructure is activated immediately and returns the drain with no marker sent.
- The `response` field reports what the destination replied with, including non-2xx statuses, so a misconfigured endpoint can be diagnosed without reading logs.

## Confirm Drain Verification

### Endpoint

Confirm a drain by sending back the marker token the destination received.

```http
POST /v1/projects/:project_id/drains/:drain_id/verify_confirm
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Request Body

- **`token`** `string` -- **Required**
  The marker token the destination received from the verify request. Minimum length: `1`. Maximum length: `128`.

### Response

Drain verified

```ts
{
  message: string;
  data: Drain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- Send the token that arrived at the destination from `verify`. A correct token activates the drain and it begins delivering records.

## Pause Drain

### Endpoint

Pause a drain so it stops delivering records without losing its verification.

```http
POST /v1/projects/:project_id/drains/:drain_id/pause
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Response

Drain paused

```ts
{
  message: string;
  data: Drain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

### Comments

- A paused drain keeps its position, so resuming continues from where it stopped rather than re-sending or skipping records.

## Resume Drain

### Endpoint

Resume a paused drain and continue delivering records from where it stopped.

```http
POST /v1/projects/:project_id/drains/:drain_id/resume
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Response

Drain resumed

```ts
{
  message: string;
  data: Drain;
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

## Send Drain Sample

### Endpoint

Send a sample payload to the drain's destination and return the destination's response.

```http
POST /v1/projects/:project_id/drains/:drain_id/sample
```

**Scope:** `drains:write`

### Path Parameters

- **`project_id`** `string` -- **Required**
  Unique identifier of the project.

- **`drain_id`** `string` -- **Required**
  Unique identifier of the drain.

### Response

```ts
{
  message: string;
  data: { status: number; status_text: string; ok: boolean; response_time_ms: number; body_preview: string };
  status: 200;
  error: null;
  pagination: null;
  endpoint: string;
}
```

#### Additional Response Fields

- **`status`** `number` -- **Required**
  The HTTP status the destination replied with.

- **`status_text`** `string` -- **Required**
  The status text that came with it.

- **`ok`** `boolean` -- **Required**
  Whether the destination accepted the request.

- **`response_time_ms`** `number` -- **Required**
  How long the destination took to reply, in milliseconds.

- **`body_preview`** `string` -- **Required**
  The start of the destination's response body, for diagnosing a rejection.

### Comments

- Use this to check the destination accepts the payload shape before or after activating the drain. The sample is not part of the record stream and does not move the drain's position.
- A non-2xx reply is returned rather than raised, so the status and body preview can be read directly from the response.

