Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
description: 'The customer-facing HTTP/JSON API for Composer.
Describe the feed you want in prose, answer the questions the builder asks, and
approve the finished schema. Every route is under `/v1` and every route under
`/v1` requires an API key.
## Authentication
`Authorization: Bearer sk_fb_live_…`. The scheme is matched
case-insensitively. The tenant comes from the key AND ONLY FROM THE KEY: no
request body may carry an organization or user id, and one that does is refused
rather than silently overwritten.
The two documentation routes — `/openapi.yaml` and `/docs` — take no key.
## Errors
Every refusal has the same body: `{"error": {"type", "code", "message", "param?", "request_id?"}}`.
Switch on `type` to decide whether to retry, fix the request, or escalate;
`code` is the specific reason within the family. `request_id` matches the
`x-string-request-id` response header and our log line, so quote it when you ask
us about a request.
| Upstream condition | HTTP |
| --- | --- |
| invalid argument, out of range | 400 |
| bad or missing key | 401 |
| permission denied | 403 |
| unknown build | 404 |
| already exists, failed precondition, aborted | 409 |
| body over 1 MiB | 413 |
| rate limited | 429 + `Retry-After` |
| the request was cancelled upstream | 499 |
| not implemented upstream | 501 |
| unavailable | 503 |
| upstream timeout | 504 |
| anything else | 500, with a generic message |
The table is grpc-gateway''s canonical gRPC-to-HTTP mapping with ONE deliberate
change: a failed precondition answers **409, not 400**. On this API that
condition is a state conflict — input sent to a build that has finished, a gate
answered after it stopped being pending — which you resolve by changing state,
not by editing your request, and a 400 would send you to re-read JSON that is
already correct.
499 is listed for completeness and is not documented on individual operations:
it is written when the request was already cancelled, so in practice there is
nobody left to read it.
**403 is never about a particular build or feed, and that is deliberate.** It
answers only for the CALLER: an organization that is not entitled to use
Composer (`feed_builder_disabled`), or, on `POST /v1/builds` alone, a key with no
user recorded against it (`key_not_attributed`). A build owned by another
organization answers a BYTE-IDENTICAL 404 to an unknown id — same code, same
message. Nothing inspects the id, the organization, or the message to choose
between them, because a distinguishable answer is an oracle: a caller could walk
build ids and learn which ones are real. Do not write client code that tries to
tell the two apart.
## Scope: this API serves supervised builds only
Every build starts supervised: it may stop and ask you things, and it publishes
by way of a publish-review gate you answer. There is no unattended mode and no
way to pre-authorize publishing, so `POST /v1/builds` takes no lever for
either — the body is the query and nothing else.
## Streaming
`GET /v1/builds/{id}/events` is server-sent events. OpenAPI cannot describe a
stream of framed events, so **generated clients will not handle that endpoint** —
use an SSE client for it and a generated client for the other ten operations.
The frame shape and event names are on the operation itself.'
title: Composer Feeds API
version: v1
servers:
- description: The origin this document was served from.
url: /
security:
- bearerAuth: []
tags:
- description: Published feeds you are subscribed to, and the data they collect.
name: Feeds
paths:
/v1/feeds:
get:
description: 'The published feeds your organization currently holds an active subscription to, by name. The name is the handle every other feed route takes.
A feed whose subscription has lapsed is not listed, and is indistinguishable from one that does not exist — see the 404 on the data operation.
`data_available: false` means the feed is not delivered to object storage at all (it goes to Snowflake, or to your own bucket), so its data operation will refuse rather than return an empty list. That is NOT the same as "no data collected yet".'
operationId: listFeeds
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/FeedList'
description: Your feeds.
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: The API key is missing, malformed, unknown, or its secret does not match. All four collapse to ONE code and message so the key format cannot be probed one refusal at a time.
headers:
WWW-Authenticate:
description: The authentication scheme. Names the scheme only — never why the presented key failed.
schema:
type: string
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'Your ORGANIZATION is not entitled to use Composer. Code `feed_builder_disabled`.
The key itself is genuine — do not rotate it. It starts working again the moment the entitlement is restored, and until then your organization''s browser sessions are refused for the same reason. Every operation on this API answers this identically, so it tells you nothing about whether any particular build or feed exists.'
'429':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: Rate limited by the core.
headers:
Retry-After:
description: Seconds to wait. Always present and never zero; the core's own backoff when it supplies one, otherwise a one-second floor.
schema:
type: string
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'An unmapped failure. The message is generic on purpose: the underlying text may name our infrastructure.'
'501':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: This deployment has no feed catalog. Code `feed_data_unavailable`. Ours to fix.
'503':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'OUR outage, not your key: the key store or the core is unreachable. Retriable. Note that an unreachable key store answers 503 rather than 401 precisely so you do not rotate a working key during our incident.'
summary: List your feeds
tags:
- Feeds
/v1/feeds/{feed}/data:
get:
description: 'What the feed has collected, each object with a signed URL you can fetch directly. This is the ongoing run data — the same artifacts the delivery subscription mails links to, reached by a program instead of by a person. `GET /v1/builds/{id}/data` is a different thing: the one-shot dataset a *build* harvested before the feed started running.
LINKS ARE SHORT-LIVED AND MINTED PER REQUEST. `expires_in` defaults to 3600 seconds, cannot be less than 60, and cannot exceed 604800 (7 days) — an out-of-range value is a 400, never silently clamped, because a pipeline built on a link it believes lasts 30 days fails a week later for no reason its author changed. `url` is a bearer link: anyone holding it can fetch that object until `expires_at`. Fetch and discard rather than storing them.
`path` is relative to the feed''s own subtree and is stable across calls, so it is what you compare to tell an object you already have from a new one. It does not name a bucket.
PAGING. `page_size` defaults to 100 and caps at 1000. When `truncated` is true, pass the returned `next_page_token` as `page_token` to continue from the first object not included in this page. The token follows the object store''s lexicographic order; it does not freeze a snapshot while new objects are collected. `page_size` also bounds raw objects examined, so metadata filtering in a shared prefix can produce a short or empty page with `truncated` still true. Continue until it is false.
FILTERING BY TIME. `updated_after` (inclusive) and `updated_before` (exclusive) keep only objects whose `updated_at` falls in that window. The filter applies to each page after it is listed, because the object store lists by name and not by time, so it narrows what is returned without reducing how many pages there are. Expect short or empty pages with `truncated` still true, and keep paging until it is false.
DOWNLOADING THROUGH THIS HOST. If your network cannot reach the object store a `url` names, pass the object''s `path` to `GET /v1/feeds/{feed}/download` instead, which streams the same bytes from this host.
DELIVERY FORMATS. A subscription can carry a delivery format: renamed and dropped columns, a row window, a file-naming template, and an encoding and container such as a zip of one JSON array. For such a subscription this operation lists the format''s files instead of the raw export, one entry per file each run yields (on a quarter-end run, a full-history file as well). Their `path` is `/`, their `url` is this API''s own download path for that file (fetch it with the same key; it does not expire, so there is no `expires_at`), and `size` is absent because the file is built when it is downloaded. `expires_in` has no effect on them.'
operationId: getFeedData
parameters:
- description: The feed name, as returned by GET /v1/feeds. Feeds are addressed by name and never by a numeric id.
in: path
name: feed
required: true
schema:
type: string
- description: Link lifetime in seconds. Default 3600, minimum 60, maximum 604800. Out-of-range values are refused with 400.
in: query
name: expires_in
schema:
default: 3600
maximum: 604800
minimum: 60
type: integer
- description: Maximum objects to examine and return. Default 100, maximum 1000. Shared-prefix filtering can return fewer.
in: query
name: page_size
schema:
default: 100
maximum: 1000
minimum: 1
type: integer
- description: The `next_page_token` from the previous page. Absent means the first page.
in: query
name: page_token
schema:
type: string
- description: Keep only objects with `updated_at` at or after this RFC 3339 timestamp.
in: query
name: updated_after
schema:
format: date-time
type: string
- description: Keep only objects with `updated_at` strictly before this RFC 3339 timestamp. Must be later than `updated_after` when both are given.
in: query
name: updated_before
schema:
format: date-time
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/DataList'
description: The feed's objects, with a link for each.
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: The request is malformed. `param` names the offending field where there is one. Note that a body carrying an organization or user id (`org_id`, `organization_id`, `user_id`, `submitted_by` and their casing variants, at any nesting depth) is refused here rather than silently overwritten — the tenant comes from the API key and only from the API key. `expires_in` and `page_size` must be within their documented bounds; `page_token` must be a token returned by this operation; `updated_after` and `updated_before` must be RFC 3339 timestamps, the second later than the first.
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: The API key is missing, malformed, unknown, or its secret does not match. All four collapse to ONE code and message so the key format cannot be probed one refusal at a time.
headers:
WWW-Authenticate:
description: The authentication scheme. Names the scheme only — never why the presented key failed.
schema:
type: string
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'Your ORGANIZATION is not entitled to use Composer. Code `feed_builder_disabled`.
The key itself is genuine — do not rotate it. It starts working again the moment the entitlement is restored, and until then your organization''s browser sessions are refused for the same reason. Every operation on this API answers this identically, so it tells you nothing about whether any particular build or feed exists.'
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: No such feed. A feed your organization is NOT subscribed to, and one whose subscription has lapsed, both answer this identically — there is no 403, because a distinguishable answer would let a caller walk feed names and learn which of them are real. Do not write client code that tries to tell the cases apart.
'409':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: This feed is not delivered to object storage, so it has no object listing and never will. Code `feed_data_not_delivered`. Distinct from an empty list, which means "nothing collected yet".
'429':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: Rate limited by the core.
headers:
Retry-After:
description: Seconds to wait. Always present and never zero; the core's own backoff when it supplies one, otherwise a one-second floor.
schema:
type: string
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'An unmapped failure. The message is generic on purpose: the underlying text may name our infrastructure.'
'501':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: This deployment has no feed catalog or object store. Code `feed_data_unavailable`. Ours to fix.
'503':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'OUR outage, not your key: the key store or the core is unreachable. Retriable. Note that an unreachable key store answers 503 rather than 401 precisely so you do not rotate a working key during our incident.'
summary: List a feed's collected data
tags:
- Feeds
/v1/feeds/{feed}/download:
get:
description: 'Streams one collected object from this host. It is the same object the data listing''s `url` points at, for callers whose network allows this API''s host but not the object store''s.
`path` is the object''s `path` exactly as the data listing returns it. It is resolved inside the feed''s own data, so it can only name an object the listing would show you.
The response is the object''s bytes with its stored `Content-Type` (`application/octet-stream` when none is recorded), `Content-Length`, and `Content-Disposition: attachment`. Headers are sent before the body, so a failure part-way through cannot change the status: it shows up as a body shorter than `Content-Length`, which HTTP clients report as an error. Retry the download when that happens.
Each pod streams a bounded number of downloads at once, and each organization gets a share of them (8 per pod). Over either, the answer is 429 with `Retry-After`, so fetch a backlog a few files at a time. A download whose caller stops reading for 30 seconds is dropped.
For a subscription with a delivery format, the file is built from its run when you ask for it, with the format''s own `Content-Type` (for example `application/zip`). The same file downloaded twice is byte-identical.'
operationId: downloadFeedObject
parameters:
- description: The feed name, as returned by GET /v1/feeds. Feeds are addressed by name and never by a numeric id.
in: path
name: feed
required: true
schema:
type: string
- description: The object's `path`, as returned by GET /v1/feeds/{feed}/data.
in: query
name: path
required: true
schema:
maxLength: 1024
type: string
responses:
'200':
content:
application/octet-stream: {}
description: The object's bytes.
headers:
Content-Disposition:
description: '`attachment; filename="<last segment of path>"`.'
schema:
type: string
Last-Modified:
description: When the object was last written.
schema:
type: string
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: The request is malformed. `param` names the offending field where there is one. Note that a body carrying an organization or user id (`org_id`, `organization_id`, `user_id`, `submitted_by` and their casing variants, at any nesting depth) is refused here rather than silently overwritten — the tenant comes from the API key and only from the API key. `path` is required, at most 1024 characters, and must name a file rather than end in `/`.
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: The API key is missing, malformed, unknown, or its secret does not match. All four collapse to ONE code and message so the key format cannot be probed one refusal at a time.
headers:
WWW-Authenticate:
description: The authentication scheme. Names the scheme only — never why the presented key failed.
schema:
type: string
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'Your ORGANIZATION is not entitled to use Composer. Code `feed_builder_disabled`.
The key itself is genuine — do not rotate it. It starts working again the moment the entitlement is restored, and until then your organization''s browser sessions are refused for the same reason. Every operation on this API answers this identically, so it tells you nothing about whether any particular build or feed exists.'
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: No such feed, or no such object in it. Code `not_found` for the feed, answered exactly as on the data operation, and `object_not_found` for a path that names nothing in a feed you hold.
'409':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: This feed is not delivered to object storage (code `feed_data_not_delivered`); or, for a delivery format, the run is too large to format on request (`run_too_large`) or a full-history file was asked of a run that did not collect the full history (`history_incomplete`).
'429':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: Too many downloads in progress on this pod. Code `too_many_downloads`.
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: string
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'An unmapped failure. The message is generic on purpose: the underlying text may name our infrastructure.'
'501':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: This deployment has no feed catalog or object store. Code `feed_data_unavailable`. Ours to fix.
'503':
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
description: 'OUR outage, not your key: the key store or the core is unreachable. Retriable. Note that an unreachable key store answers 503 rather than 401 precisely so you do not rotate a working key during our incident.'
summary: Download one of a feed's objects
tags:
- Feeds
components:
schemas:
ErrorResponse:
description: The one error envelope every refusal on this API uses.
properties:
error:
$ref: '#/components/schemas/ErrorDetail'
required:
- error
type: object
FeedList:
properties:
data:
items:
$ref: '#/components/schemas/FeedSummary'
type: array
object:
type: string
required:
- object
- data
type: object
DataObject:
properties:
expires_at:
format: date-time
type: string
object:
type: string
path:
type: string
size:
format: int64
type: integer
updated_at:
format: date-time
type: string
url:
type: string
required:
- path
- object
- updated_at
- url
type: object
DataList:
properties:
data:
items:
$ref: '#/components/schemas/DataObject'
type: array
feed:
type: string
next_page_token:
type: string
object:
type: string
truncated:
type: boolean
required:
- object
- feed
- data
- truncated
type: object
ErrorDetail:
description: The reason a request was refused. `type` is the family a client switches on; `code` is the specific reason within it; `param` names the offending request field when there is one and is omitted rather than sent empty.
properties:
code:
type: string
message:
type: string
param:
type: string
request_id:
type: string
type:
type: string
required:
- type
- code
- message
type: object
FeedSummary:
properties:
data_available:
type: boolean
name:
type: string
object:
type: string
required:
- name
- object
- data_available
type: object
securitySchemes:
bearerAuth:
description: 'A Composer API key: `Authorization: Bearer sk_fb_live_…`.'
scheme: bearer
type: http