#Pagination
List endpoints return a page of results and an opaque cursor:
{
"data": [{ "id": "…" }],
"page": { "nextCursor": "8c1f…" }
}
Pass nextCursor back as cursor to fetch the next page. When nextCursor is null, you’ve reached the end. Keep every other parameter the same between pages.
| Parameter | Default | Maximum |
|---|---|---|
limit on metadata lists | 50 | 250 |
limit on payload lists (row values, submissions, step data, agent memory, feedback, eval runs, conversation events) | 50 | 100 |
Ordering is deterministic, so pages don’t skip or repeat items. Treat cursors as opaque: don’t build or edit them.
Lists return items in a stable ID order by default. Approvals, executions, and table and form row lists also accept sort=-createdAt for newest first, and approvals accept sort=-decidedAt for the most recent decisions first. Cursors work the same way in either order.
#Filters
Most lists accept ISO 8601 timestamp filters: createdAfter and createdBefore, plus updatedAfter and updatedBefore where the resource tracks updates. Lists also accept filters specific to that resource. For example, executions filter by workflowId, workflowVersionId, and status, and approvals by status, priority, source, workflowId, executionId, assignedTo, decisionKind, decidedAfter, and decidedBefore. The approval status filter also accepts decided, which matches approved, rejected, auto-approved, and committing requests. A committing request has a decision that the reviewer can still undo until its effectiveAt, so it is not final: act on an approval only once its status is approved, rejected, auto_approved, or expired. Approval workflowId, executionId, and decisionKind take up to 100 comma-separated values and match any of them. The OpenAPI document lists every filter for every endpoint.
Unknown or repeated query parameters return 400 VALIDATION_ERROR instead of being ignored, so a typo can’t silently widen your results.
#Expansions
Some endpoints can include related data in the same response with include:
include=payloadonGET /approvalsandGET /approvals/{id}adds each approval’s card aspayload: the proposed action, context, confidence reasoning, workflow name, and decision. It needspayloads:read, and pages hold at most 100 approvals.include=statsonGET /workflowsandGET /workflows/{id}addsstats: trigger type, pending approvals, runs per UTC day for the last 14 days, and success, approval, and confidence rates. It needs onlyworkflows:read. The numbers come from the same data as theworkflowExecutionsandapprovalsdatasets in/query.
#Filtering on values
Metadata row and submission lists filter only on envelope fields such as timestamps and source form. Filtering on your own column values requires payloads:read, through the payload lists or /query.
#Errors
Every error has the same shape:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Unknown or invalid query parameter",
"requestId": "5b0e…"
}
}
Branch on code, not message. Messages are for people and may change.
A form submission that fails validation adds fieldErrors, one entry per invalid field, where path is the field’s slug:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Please fix the highlighted fields.",
"requestId": "5b0e…",
"fieldErrors": [{ "path": "email", "message": "Invalid email address" }]
}
}
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Malformed body, unknown parameter, missing Idempotency-Key, or a body over its size limit. | Fix the request. |
| 400 | QUERY_TOO_EXPENSIVE | A /query shape outside the allowed rules, such as a missing time range or an unknown field. | Add a time range or narrow the query. See query rules. |
| 402 | QUOTA_EXCEEDED or SPENDING_CAP_EXCEEDED | A form submission would exceed the workspace’s storage. | Free up storage or raise the spending cap. |
| 401 | UNAUTHORIZED | Missing, invalid, expired, or revoked credential. | Replace the key or reconnect the client. |
| 403 | FORBIDDEN | Valid credential without a required scope. | Grant the scopes named in the WWW-Authenticate header. |
| 404 | NOT_FOUND | The resource doesn’t exist, is archived, or belongs to another workspace. | Check the ID and the credential’s workspace. |
| 409 | IDEMPOTENCY_CONFLICT | An Idempotency-Key was reused with a different body. | Use a new key for a new request. |
| 409 | DECISION_COMMITTED | An approval decision’s undo window has closed. | None: the workflow has received the decision. |
| 413 | RESPONSE_TOO_LARGE | The response would exceed 2 MiB. | Lower limit or narrow filters. |
| 429 | RATE_LIMITED | A request budget or concurrency limit was hit. | Wait for Retry-After, then slow down. |
| 503 | QUERY_TIMEOUT | A /query ran past its 5-second limit and was cancelled. | Wait Retry-After (30 seconds), then narrow the time range, filters, or grouping. |
| 503 | IDEMPOTENCY_IN_PROGRESS or IDEMPOTENCY_UNAVAILABLE | The first request with this key hasn’t finished yet. | Retry with the same key and body after Retry-After. |
| 500 | INTERNAL_ERROR | Something failed on our side. | Retry with backoff; contact support with the requestId if it persists. |
A 403 from a scope check includes the scopes it needed:
WWW-Authenticate: Bearer error="insufficient_scope", scope="tables:read payloads:read"
The API returns 404 rather than 403 for IDs in another workspace, so it never confirms that someone else’s resource exists.
#Rate limits
Limits keep shared infrastructure responsive for every workspace. They aren’t a pricing tier, and API reads never use action credits.
| Limit | Value |
|---|---|
| Requests per IP address, before authentication | 900 per minute |
| Request units per credential | 1,200 per minute |
| Request units per workspace, across all credentials | 6,000 per minute |
Concurrent /query requests per credential | 4 |
Concurrent /query requests per workspace | 20 |
Each request costs units:
| Request | Units |
|---|---|
| Metadata list or detail | 1 |
| Payload list page or payload detail | 5 |
POST /query | 10 |
| Any action in Actions and idempotency | 10 |
MCP aggregate tools draw from the same credential and workspace budgets as /query.
Successful responses report the credential’s budget:
| Header | Meaning |
|---|---|
RateLimit-Limit | Units available to this credential per minute. |
RateLimit-Remaining | Units left in the current minute. |
RateLimit-Reset | When the current window resets, as Unix time in seconds. |
A 429 adds Retry-After in seconds. Honor it; retrying sooner fails again and spends the next window’s headroom.
#Dashboard refresh
You choose your dashboard’s refresh interval, subject to these limits; there is no unlimited refresh rate. Each query panel costs 10 units per refresh. Ten panels refreshing every 10 seconds use 600 units per minute, half of one credential’s budget, which leaves room for retries and manual reloads.
Start at 30 seconds. For multi-panel dashboards, stay at 10 seconds or slower, and watch RateLimit-Remaining before going faster. Give separate dashboards separate keys so one busy screen can’t starve another.
#Actions and idempotency
These endpoints change state:
| Endpoint | Scope | Success |
|---|---|---|
POST /workflows/{workflowId}/runs | workflows:execute | 202 with { "data": { "executionId", "status": "pending" } } |
POST /approval-requests | approvals:request | 201 with { "data": { "approvalId", "status" } } |
POST /approvals/{approvalId}/decision | approvals:decide | 200 with { "data": { "approvalId", "decisionId", "status": "committing", "effectiveAt" } } |
POST /approvals/{approvalId}/decision/undo | approvals:decide | 200 with { "data": { "approvalId", "status": "pending" } } |
POST /forms/{formId}/submissions | forms:submit | 201 with { "data": { "rowId" } } |
POST /optimization-suggestions/{suggestionId}/decision | optimization:decide | 200 with { "data": { "suggestionId", "status" } } |
POST /optimization-suggestions/{suggestionId}/revert | optimization:decide | 200 with { "data": { "suggestionId", "reverted": true } } |
POST /optimization-suggestions/{suggestionId}/restore | optimization:decide | 200 with { "data": { "suggestionId", "status": "pending" } } |
An approval decision stays undoable until effectiveAt, 10 seconds after it is recorded. Only the reviewer who decided can undo it. After effectiveAt the workflow has the decision, and undo returns 409 DECISION_COMMITTED. Revert restores the configuration an accepted suggestion replaced, at any time. Restore returns a dismissed suggestion to pending.
Starting a run, requesting an approval, deciding an approval, submitting a form and deciding a suggestion require an Idempotency-Key header: 1 to 255 printable ASCII characters with no spaces. A UUID per logical operation works well. Rills keeps each key for 24 hours:
- Sending the same key and body again returns the original status and body with
Idempotency-Replayed: true. Nothing runs twice. - Sending the same key with a different body returns
409 IDEMPOTENCY_CONFLICT. - If the first request is still running, the retry waits briefly, then returns
503withRetry-Afterso you can try again with the same key.
Start a workflow, optionally choosing a trigger and passing trigger data:
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $RILLS_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen)" \
--data '{"triggerPayload": {"email": "jordan@example.com"}}' \
"https://rills.ai/api/v1/workflows/<workflow-id>/runs"
Submit a proposal for human review:
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $RILLS_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen)" \
--data '{"title": "Send renewal offer to Acme", "actionType": "send_email", "parameters": {"to": "buyer@acme.example"}, "priority": "high"}' \
"https://rills.ai/api/v1/approval-requests"
Approval requests take title (up to 200 characters), actionType (up to 80), optional parameters and context objects (128 KiB combined), priority (low, normal, high, or urgent; default normal), and expiresInSeconds (60 seconds to 30 days; default 7 days). Approvals are free. A started workflow’s actions are billed as usual.
Action bodies are capped at 256 KiB, 20 levels of nesting, 10,000 total values, 1,000 items per array, and 32 KiB per string. A request over any cap returns 400 VALIDATION_ERROR and creates nothing.