logs:manage; the read routes need logs:read. The base URL, bearer authentication, and the error envelope are on the REST API overview; lua drains is the same surface from a terminal.
Log drains are switched on for a deployment as a whole, not per organization. While they are off, every drain route on this page answers
404 — the resource does not exist rather than being forbidden, which is why a 404 here never distinguishes a disabled feature from an unknown drain.Scopes
logs:manage is sensitive: a wildcard such as logs:* does not satisfy it, and a scoped key holds it only when it was granted by exact name. logs:read is an ordinary read scope — a key that already holds *:read has it and needs nothing re-minted.
A credential without the scope answers
403 with code: "INSUFFICIENT_SCOPE" and the requiredScope.
Every drain route resolves the drain by organization and id together, so a drain id from another organization answers 404 DRAIN_NOT_FOUND rather than 403. The two cases are deliberately indistinguishable.
Rate limits
The drain routes are limited per organization, not per credential — a drain is an organization resource, and every route carries the organization in its path:
Over either, the request answers
429 with code: "DRAIN_RATE_LIMITED" and a bare Retry-After header in seconds. Back off for what it says and retry.
Every response, throttled or not, also carries the bucket’s own budget headers, suffixed with the bucket that was evaluated — log-drains-read on a GET, log-drains-mutate on everything else:
Read the bare
Retry-After to back off and the suffixed X-RateLimit-Remaining-* to pace yourself; a client that looks for an unsuffixed X-RateLimit-Remaining will not find one.
POST …/verify carries a second, narrower limit on top of this one — 5 attempts per drain per hour, answered as 429 DRAIN_VERIFY_RATE_LIMITED. The log read routes are limited separately, and per credential.
Drains
GET /admin/orgs/:orgId/log-drains
Lists every drain in the organization, including health and quota. Soft-deleted drains are not returned. Response200 with { "drains": LogDrainRead[], "limits": { "maxDrains": 5 } }. limits.maxDrains is the organization’s ceiling — 5, or 10 on Enterprise — so a UI can say “3 of 5” without knowing the plan. Each drain carries its health and quota blocks.
headers[] carries each header’s name and last4 and never a value; last4 is an empty string when the stored value is shorter than 8 characters, rather than a shortened value. secret is never present.
POST /admin/orgs/:orgId/log-drains
Creates a drain. It starts inpending_verification and buffers rather than delivers until ownership is proved.
string
required
1–64 characters, unique in the organization.
string
required
http, otlp, datadog, or betterstack.string
The destination URL. Required for
http, otlp, and betterstack; refused for datadog, whose URL is derived from site. HTTPS only, and never a private, loopback, link-local, or cloud-metadata address.string
datadog only. One of datadoghq.com, datadoghq.eu, us3.datadoghq.com, us5.datadoghq.com, ap1.datadoghq.com, ddog-gov.com.string
default:"json"
json or ndjson. Honoured for http; the other types fix their own encoding.object[]
Up to 10
{ name, value } pairs. Write-only: values are stored encrypted and never returned. name matches ^[A-Za-z0-9-]{1,64}$.string
Write-only HMAC signing secret for an
http drain. Generated when omitted, and returned exactly once in this response.string
default:"info"
debug, info, warn, or error.string | string[]
default:"*"
"*" for every agent in the organization, now and later, or a list of agent ids that must belong to this organization.string[]
default:"[\"production\"]"
Any of
production, sandbox.object
{ "debug": 0.1 }. Deterministic head sampling by execution, 0 < n <= 1. Only debug is sampleable.boolean
default:"false"
Turns on the
user_message and agent_response bodies.boolean
Required
true when includeContent is true.string[]
Up to 32 attribute keys to drop before encoding, for example
["user.id"].object[]
Up to 20 organization masking rules,
{ id, pattern, flags? }. Each pattern is at most 256 characters, with no backreferences, nested quantifiers, or lookbehind.201 with the created drain. This is the only response that ever carries secret. Store it now: it cannot be read back, only rotated.
Errors
GET /admin/orgs/:orgId/log-drains/:drainId
Reads one drain, withhealth, quota, and the last verification outcome.
Response
200 with a LogDrainRead:
health.hourly is the same 24 hours as the *24h counters, broken into points to draw. There are always 24 of them, oldest first and zero-filled across quiet hours, so nothing has to handle a gap: h is the absolute UTC hour (floor(epochMilliseconds / 3600000)), and delivered, dropped, and rejected are that hour’s attempt counts. The window is the current, partial hour and the 23 before it, which is why the newest point’s numbers keep rising while you watch them.
Errors
PATCH /admin/orgs/:orgId/log-drains/:drainId
Updates a drain. Accepts everyPOST field except type, which cannot be changed. Sending null for sampling, redact, or scrubRules clears it.
Changing endpoint moves the drain back to pending_verification: delivery stops until it is re-verified, and records keep buffering meanwhile.
Response
200 with the updated LogDrainRead.
Errors — as for POST, plus 404 DRAIN_NOT_FOUND.
DELETE /admin/orgs/:orgId/log-drains/:drainId
Soft-deletes the drain, purges its stored header values and signing secret, and drops whatever it had queued. The name becomes free for reuse. Response204, no body.
Errors
Operations
POST /admin/orgs/:orgId/log-drains/:drainId/test
Enqueues onelua.drain.test record and sends it through the real delivery path. The outcome lands on the drain’s deliveries.
Response
202 with { "batchId", "enqueuedAt" }.
POST /admin/orgs/:orgId/log-drains/:drainId/verify
Runs ownership verification. For anhttp drain a single-use token valid for 10 minutes is sent as X-Lua-Verify and must be echoed back; for the vendor types any 2xx from the intake is accepted.
On success the drain moves to healthy, verifiedAt is set, and the buffered backlog starts draining.
Response
202 with { "verificationId", "expiresAt" }. The outcome appears on the drain’s verification object and in its deliveries.
Errors
POST /admin/orgs/:orgId/log-drains/:drainId/pause
Stops delivery. Records keep buffering, within the six-hour horizon and the buffer size.string
Recorded with the pause.
200 with the updated LogDrainRead, state: "paused" and health.pauseReason: "manual".
POST /admin/orgs/:orgId/log-drains/:drainId/resume
Resumes delivery. The drain returns todegraded, never straight to healthy — it has to prove itself again.
Response
200 with the updated LogDrainRead.
POST /admin/orgs/:orgId/log-drains/:drainId/rotate-secret
Mints a new HMAC signing secret and opens a 24-hour rotation window in which every delivery carries twov1 signatures. Pass ?finalize=true to close an open window instead.
Only a signed drain has a secret to rotate. The vendor presets — otlp, datadog, and betterstack — deliver unsigned and authenticate with the header you configured, so this route answers 400 on one of them rather than minting a secret that nothing would sign with.
boolean
default:"false"
Closes the current rotation window early. No new secret is minted.
finalize: 200 with { "secret", "rotation": { "startedAt", "expiresAt" } }. The secret is shown once.
With finalize=true: 200 with the updated LogDrainRead.
Errors
GET /admin/orgs/:orgId/log-drains/:drainId/deliveries
Recent delivery attempts, newest first. Kept for 7 days.number
default:"20"
At most 100.
string
The
nextCursor from a previous page.string
One of
batch, test, verify, heartbeat, dropped, or truncated. Anything else is a 400.200 with { "deliveries": LogDrainDeliveryRead[], "nextCursor": string | null }.
200 with partialSuccess.rejectedLogRecords, so the batch counts as delivered (ok: true) and is marked errorClass: "partial". It is never retried — the OTLP specification asks for that, and re-sending would duplicate the records the collector did accept. A partialSuccess that names zero refused records is a warning, not a rejection, and produces a plain ok row with no errorClass at all.
How many records were refused is counted on the drain rather than on the row: read health.rejectedCount24h, or the rejected figure in the hourly series, from GET …/log-drains/:drainId. The rejectedRecords field is part of the delivery schema and reserved for a per-row count, but nothing writes it in this phase — do not build a reconciliation on it.
A delivery record has no field that can hold a request body. responseExcerpt is at most 1 KB of the destination’s response, scrubbed. errorClass is one of the delivery error classes.
Audit trail
Every mutation on this page writes one entry to the organization’s audit log:logs.drain.created, logs.drain.updated, logs.drain.deleted, logs.drain.paused, logs.drain.resumed, logs.drain.verified, logs.drain.secret_rotated, logs.drain.test_sent, and logs.drain.quota_paused when the quota ladder pauses a drain by itself.
The entry is deliberately narrower than the drain it describes, because an audit log is read by more people than a drain configuration is:
Reducing the endpoint matters more than it looks: several vendors’ intakes carry a token in the query string, so the whole query is dropped rather than inspected.
name, type, site, format, state, version, and the selectors are recorded as they are.
Reading logs
These routes read the platform’s own copy of the same records. They needlogs:read.
GET /developer/agents/:agentId/logs
One agent’s execution logs, newest first.string
A log source, or
all. This is the parameter lua logs --type sets.string
Inclusive lower bound. ISO 8601, or a relative form such as
15m, 2h, 7d.string
Inclusive upper bound. Same formats.
string
production or sandbox.string
The
nextCursor from a previous page. Cannot be combined with page.number
default:"20"
Clamped to 100.
number
default:"1"
Deprecated. Use
cursor.200 with { "logs", "nextCursor", "pagination" }. pagination is kept for one release. pagination.totalCountExact is a boolean: above 10,000 matches totalCount is capped at 10000 and totalCountExact is false, so page through with cursor rather than trusting totalPages. hasNextPage is reliable either way.
A request that passes page explicitly also carries a Deprecation header and a Link: <…?cursor=…>; rel="next" pointing at the cursor form; the implicit first page does not.
GET /admin/orgs/:orgId/logs
Every agent in the organization, with the samelogSource, since, until, environment, cursor, and limit parameters. GET /admin/orgs/:orgId/logs/download exports the same selection.
These routes are rate limited to 120 requests per minute per credential.
Scope deprecation
The log read routes moved tologs:read. For two releases the scope each route used before is still accepted, so nothing breaks while keys are re-minted:
A request authorized by a route’s previous scope still succeeds and carries three response headers.
Warning names the scope that authorized that request, so a per-skill route answers automations:read and an organization route analytics:read:
Deprecation: true in your clients and re-mint those keys with logs:read before the date in the Sunset header. After it, only logs:read is accepted.
The
Sunset value above is the one in force as this is written, and is subject to change until the release is announced. Read the date from the header your own calls receive rather than pinning this literal.See also
- Ship logs to your stack — what a drain is and what it selects
- Event schema — the shape of what a drain delivers
- Delivery guarantees — retry, drops, and the health states behind
state lua drains— the same operations from a terminal- REST API overview — base URL, authentication, and the error envelope

