Read and decide the requests for approval

List the requests for approval that executions wait on, and approve or reject one.

An approval is a request for a person's decision that an execution waits on. A Require Approval node raises one, and so does an AI Model node whose overrun policy asks a person. The operations of this page list the requests and decide one. The console shows the same requests on Approvals.

Who reads and who decides

Reading the requests takes management:read. Deciding one takes management:write, because a decision lets an execution spend money. The audit log records every decision.

A key reads and decides the requests of its own project. A request of another project, or one that does not exist, answers 404 with the code approval_not_found.

The list is the newest first and is read page by page, as Requests describes. Send status=pending to read only the requests that still wait.

What a decision does

  • The body is optional. Its reason, of up to 500 characters, is kept with the approval and shows in the trace of the execution.

  • The answer is the approval with its new status and decided_at. A decision made with an API key names the key in decided_by. A decision made in the console names the person in decided_by_user_id and decided_by_name.

  • The request does not move the execution. The execution goes on within a minute: through the Approved port after an approval, and through Rejected after a rejection.

  • A request from a model node is decided the same way. An approval lets the model call go on with a new hold, and a rejection refuses it with overrun_stopped — Approvals says what follows.

  • Slen sends the webhook event approval.decided with the verdict.

A request that can no longer be decided

A request waits until expires_at. The author of a Require Approval node sets the time on the node, and a request from a model node waits 24 hours. When nobody decides in time, the request becomes expired within a minute, and approval.decided says expired. From then on it goes as after a rejection: the execution leaves a Require Approval node through Rejected, and the call of a model node is refused with overrun_stopped.

  • A request that is no longer pending answers 409 with the code approval_already_decided: somebody decided it, or it has expired.

  • A request that is still pending when its time has passed answers 409 with the code approval_expired. It is closed as expired at once, as if nobody had decided, and the execution goes on as after a rejection.

  • A request whose execution has ended and no longer waits answers the same approval_expired. It is closed as expired, nothing goes on, and no webhook event is sent.

Operations

Decisions parked executions are waiting for

GET /v1/approvals

  • Scope: management:read

Parameters

ParameterDescription
limit optional

queryinteger

Page size (default 30).

  • Minimum: 1
  • Maximum: 100
cursor optional

querystring

  • Maximum length: 32
status optional

queryApprovalStatus

  • One of: pending, approved, rejected, expired
project optional

querystring

current narrows the list to the active project — an API key's own, a person's as switched on POST /api/auth/me/project — plus the organization's rows where the resource has them. A literal, never an id: anything else is 422. Without it the list is everything within the caller's reach.

  • One of: current

Responses

200

One cursor page of approvals

application/json
{
  "data": [
    {
      "approval_id": "74438200973594624",
      "project_id": "74438200973594624",
      "execution_id": "74438200973594624",
      "flow_id": "74438200973594624",
      "node_key": "string",
      "operation_code": "generate_email",
      "note": "string",
      "status": "pending",
      "decided_by": "string",
      "decided_by_user_id": "74438200973594624",
      "decided_by_name": "string",
      "reason": "string",
      "expires_at": "2026-10-02T09:30:00Z",
      "created_at": "2026-10-02T09:30:00Z",
      "decided_at": "2026-10-02T09:30:00Z"
    }
  ],
  "meta": {
    "limit": 1,
    "next_cursor": "string"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Let a parked execution spend

POST /v1/approvals/{id}/approve

  • Scope: management:write

A person's verdict names them (decided_by_user_id, decided_by_name); a key's names the key in decided_by, as before.

409 approval_already_decided — the request has left pending: somebody decided it, its deadline passed, or its execution ended. 409 approval_expired — this very call found the request past its expires_at, or found that the execution it belongs to is no longer waiting (it failed, was lost or ran into its parked ceiling); the request is closed as expired and nothing is continued.

A verdict that is accepted is announced to webhook subscribers as approval.decided (see createWebhookEndpoint), and the node that asked leaves {status, reason, approval_id, decided_at} in its output: a refusal and a deadline nobody met both leave through rejected, and nodes.<key>.output.status (rejected / expired) tells them apart.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body optional

application/json
{
  "reason": "string"
}

Responses

200

The decided approval

application/json
{
  "data": {
    "approval_id": "74438200973594624",
    "project_id": "74438200973594624",
    "execution_id": "74438200973594624",
    "flow_id": "74438200973594624",
    "node_key": "string",
    "operation_code": "generate_email",
    "note": "string",
    "status": "pending",
    "decided_by": "string",
    "decided_by_user_id": "74438200973594624",
    "decided_by_name": "string",
    "reason": "string",
    "expires_at": "2026-10-02T09:30:00Z",
    "created_at": "2026-10-02T09:30:00Z",
    "decided_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

404

No such resource for this tenant. Somebody else's resource — and a resource of a project the caller does not reach — answers the same way on purpose: the two must not be distinguishable.

409

State conflict: idempotency clash, stale draft, illegal transition; the last owner of an organization (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Refuse a parked execution

POST /v1/approvals/{id}/reject

  • Scope: management:write

A person's verdict names them (decided_by_user_id, decided_by_name); a key's names the key in decided_by, as before.

409 approval_already_decided — the request has left pending: somebody decided it, its deadline passed, or its execution ended. 409 approval_expired — this very call found the request past its expires_at, or found that the execution it belongs to is no longer waiting (it failed, was lost or ran into its parked ceiling); the request is closed as expired and nothing is continued.

A verdict that is accepted is announced to webhook subscribers as approval.decided (see createWebhookEndpoint), and the node that asked leaves {status, reason, approval_id, decided_at} in its output: a refusal and a deadline nobody met both leave through rejected, and nodes.<key>.output.status (rejected / expired) tells them apart.

Parameters

ParameterDescription
id required

pathId

Snowflake identifier, as a string.

  • Pattern: ^\d+$
  • Example: 74438200973594624

Request body optional

application/json
{
  "reason": "string"
}

Responses

200

The decided approval

application/json
{
  "data": {
    "approval_id": "74438200973594624",
    "project_id": "74438200973594624",
    "execution_id": "74438200973594624",
    "flow_id": "74438200973594624",
    "node_key": "string",
    "operation_code": "generate_email",
    "note": "string",
    "status": "pending",
    "decided_by": "string",
    "decided_by_user_id": "74438200973594624",
    "decided_by_name": "string",
    "reason": "string",
    "expires_at": "2026-10-02T09:30:00Z",
    "created_at": "2026-10-02T09:30:00Z",
    "decided_at": "2026-10-02T09:30:00Z"
  }
}

Errors

StatusCodesWhen
401

No credential, or one that is not valid

403

Authenticated, but not allowed: scope (insufficient_scope), policy or tenant (not_a_member); a blocked end customer (customer_blocked); an act only a signed-in person may take (session_required); the role rule on members and invitations (member_forbidden, invite_forbidden); a write to the organization's own rows, or a read of the audit journal, by a caller that does not reach every project (project_access_restricted)

404

No such resource for this tenant. Somebody else's resource — and a resource of a project the caller does not reach — answers the same way on purpose: the two must not be distinguishable.

409

State conflict: idempotency clash, stale draft, illegal transition; the last owner of an organization (last_owner); the owned-organization limit (organization_limit_reached); an end customer with credit history that cannot be deleted (customer_has_history)

422

Validation failure or a semantically impossible request. Field-level validation from the framework answers {"message", "errors"}; domain refusals answer the standard error envelope. An Idempotency-Key longer than 190 characters is invalid_request in that envelope.

Headers

  • X-Request-Id

The body of every error is the error envelope.

Examples

Read the requests that wait

One execution waits for a decision on its node review. The note is the one the author of the flow left on the node.

Terminal
curl "https://api.slen.ai/api/v1/approvals?status=pending" \
  -H "Authorization: Bearer $SLEN_API_KEY"
200 OK
{
  "data": [
    {
      "approval_id": "74443031076798464",
      "project_id": "74438200973594624",
      "execution_id": "74443028719599616",
      "flow_id": "74438623450128384",
      "node_key": "review",
      "operation_code": "generate_email",
      "note": "Check the tone before the email goes out.",
      "status": "pending",
      "decided_by": null,
      "decided_by_user_id": null,
      "decided_by_name": null,
      "reason": null,
      "expires_at": "2026-10-03T09:20:11+00:00",
      "created_at": "2026-10-02T09:20:11+00:00",
      "decided_at": null
    }
  ],
  "meta": {
    "limit": 30,
    "next_cursor": null
  }
}

Approve a request

The answer is the approval with the verdict and the key that gave it.

Terminal
curl -X POST https://api.slen.ai/api/v1/approvals/74443031076798464/approve \
  -H "Authorization: Bearer $SLEN_API_KEY"
200 OK
{
  "data": {
    "approval_id": "74443031076798464",
    "project_id": "74438200973594624",
    "execution_id": "74443028719599616",
    "flow_id": "74438623450128384",
    "node_key": "review",
    "operation_code": "generate_email",
    "note": "Check the tone before the email goes out.",
    "status": "approved",
    "decided_by": "74438344587730944",
    "decided_by_user_id": null,
    "decided_by_name": null,
    "reason": null,
    "expires_at": "2026-10-03T09:20:11+00:00",
    "created_at": "2026-10-02T09:20:11+00:00",
    "decided_at": "2026-10-02T09:31:40+00:00"
  }
}

Reject a request

The reason is kept with the approval. The execution leaves the node through Rejected.

Terminal
curl https://api.slen.ai/api/v1/approvals/74443031076798464/reject \
  -H "Authorization: Bearer $SLEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "The customer asked us not to write again."
  }'
200 OK
{
  "data": {
    "approval_id": "74443031076798464",
    "project_id": "74438200973594624",
    "execution_id": "74443028719599616",
    "flow_id": "74438623450128384",
    "node_key": "review",
    "operation_code": "generate_email",
    "note": "Check the tone before the email goes out.",
    "status": "rejected",
    "decided_by": "74438344587730944",
    "decided_by_user_id": null,
    "decided_by_name": null,
    "reason": "The customer asked us not to write again.",
    "expires_at": "2026-10-03T09:20:11+00:00",
    "created_at": "2026-10-02T09:20:11+00:00",
    "decided_at": "2026-10-02T09:31:40+00:00"
  }
}

Where to go next

  • Approvals — the same requests in the console.

  • Async runs and waiting — how an execution that waits for a decision answers.

  • Webhooks — approval.requested and approval.decided, and how they arrive.

  • Error codes — what each code of this page means and what to do about it.

Built from openapi b070e1f, sdk contract 5cc7055, php ae4a53b, node 84bc62d