String API
Composer API

Builds

Start, list, watch, steer and finish a feed build.

Starting a build

curl -X POST "https://feedbuilder.usestring.ai/v1/builds" \
  -H "Authorization: Bearer $STRING_FEED_BUILDER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "a feed of laptop listings from example.com"
  }'
FieldRequiredNotes
queryyesWhat to build, in prose. At most 4000 characters, counted in runes.

201 returns the id and nothing else:

{ "object": "build", "id": "4c1d8a76-0f2b-4e93-9a5d-6b7c8e0f1a23" }

Lengths are counted in runes

query 4000, message text 2000, cancel reason 1000 — all counted in runes, not bytes, so a request written in a non-Latin script is not refused at a third of the length of the same request in English.

A request body over 1 MiB is a 413.

Keep the id — a create is not repeatable

Each POST /v1/builds starts a new build and bills for it. The id in the 201 is the only handle you get, so store it before you do anything else; a request that times out after the server accepted it leaves a build running that you can still find in GET /v1/builds.

Listing builds

curl "https://feedbuilder.usestring.ai/v1/builds?page_size=25" \
  -H "Authorization: Bearer $STRING_FEED_BUILDER_KEY"

Newest first, and only your organization's builds. page_size is at most 100; a larger value is refused, not clamped. Omitting it — or sending 0 — takes the default. Pass the response's next_page_token back as page_token for the next page.

{
  "object": "list",
  "data": [{ "object": "build", "id": "7b2e5f10-3c94-4a8d-8e61-2f0a9c3d5b74", "status": "awaiting_input", "query": "…", "pending_gate_id": "e0a4d9c2-8b31-4f57-a6d0-3c9e1b7f2a85", "pending_gate_kind": "schema_review" }],
  "total_size": 1,
  "next_page_token": "…"
}

Getting a build

GET /v1/builds/{id} returns the full build: status, progress, pending_gate, gate_history, messages, activity, report and a monotonic version.

status is one of running, completed, errored, cancelled, awaiting_input, unknown. awaiting_input means the build is parked on a gate waiting for you — nothing moves until you answer it.

`awaiting_input` and `pending_gate` are the same fact

They are guaranteed to agree, so either one answers "is this waiting on me?" and you never have to check both:

  • status: "awaiting_input"pending_gate is present (and pending_gate_id on a list row).
  • A terminal build — completed, errored, cancelled — never reports a pending gate, even if it was holding one when it ended. There is nothing left to answer, and answering would only earn a 409.

A run that ended because we had to step in reports errored, not awaiting_input. If a build is waiting on us rather than on you, it will never say it is waiting on you.

error on a build is customer-facing copy: it is the same string the run-finished email carries, not an operator diagnostic.

No response contains a JSON `null` — but a collection can be absent

No response from this API contains a JSON null, at any depth, so nothing you read needs a null check.

Presence is a separate question. candidates, questions, a choice gate's choices, and the fields and sample_rows of GET /v1/builds/{id}/sample are always written, empty or not. Others are omitted when empty rather than sent as []gate_history, messages and activity on the build, a question's choices, and examples and edits. Treat an absent collection as an empty one; do not read .length off it unguarded.

Auto-proceed

PATCH /v1/builds/{id} toggles whether the build advances past gates it can answer for itself.

{ "auto_proceed": true }

`auto_proceed` must be sent explicitly

Omitting the key is a 400 (auto_proceed_required), never a silent false. A silent false would park the build at its next gate, which looks like a hang rather than a setting you changed. null earns the same 400.

Sending a message

POST /v1/builds/{id}/messages puts free text into a running build — a correction, a hint, an extra constraint.

{ "text": "only listings under $2000", "interrupt": false }

interrupt: true aborts Composer's turn in flight instead of queueing behind it. Omitted means queue, which is the non-destructive default. Text is at most 2000 runes.

Sending a message to a build that has already finished is a 409, not a 400: nothing is wrong with your request, the build is in the wrong state for it.

Cancelling a build

POST /v1/builds/{id}/cancel, optionally with {"reason": "…"} (at most 1000 runes).

Deleting a build

DELETE /v1/builds/{id} soft-deletes it: it stops appearing in listings. 204 on success.

Sample rows

GET /v1/builds/{id}/sample returns the published field schema and real sample rows for a build that produced them.

{
  "object": "sample",
  "build_id": "a8d3f612-5e07-4b9c-81fa-72d4e6035c9b",
  "feed_name": "example_laptops",
  "fields": [{ "key": "title", "type": "string", "required": true, "repeated": false }],
  "sample_rows": [["ThinkPad X1", "1899.00", "https://example.com/…"]],
  "rows_written": 4821
}

Rows are arrays, and a sample is not the dataset

sample_rows are arrays positionally aligned with fields, never objects keyed by column name.

And this endpoint is a sample, not an export. There is no bulk data download on this API; the built feed is delivered through the subscription the publish-review gate registers. See Limits.