# Approval

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /approval/ask

**Ask for access, or for a guarded operation**

`operationId: ApprovalController_ask`

Two kinds of ask:

- **A screen or menu item** — `target: { kind: "screen" | "menu", path, app? }`. Opens an `access_request` and fires the access workflow; the approvers get it in their tray and mail, and the asker is told it was sent. The same open ask for the same path returns `already-pending` instead of a second request.
- **An operation on a record** — `target: { kind: "record", operation: "publish", datatype, id }`, for publishing a page, post or product. When no enabled workflow takes that datatype there is nothing to wait for and the answer is `{ decision: "allowed" }` — the caller simply goes ahead.

`request.reason` is shown to the approvers; `request.label` names the screen in messages (defaults to `path`).

#### Signature

```http
POST /approval/ask (body) -> The decision state
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | SIGN_IN_TO_ASK | Sign in to ask | The caller has no email on their session. | — |
| `400` | PATH_REQUIRED | target.path is required | A screen or menu ask without a path. | — |
| `404` | RECORD_NOT_FOUND | page 66f1c0ffee12ab34cd56ef78 not found | The record to publish does not exist. | — |
| `422` | ACCESS_REQUESTS_OFF | Access requests are switched off in this org: no enabled workflow takes access_request (check the collection's Enable Workflow and the workflow's Enabled switch) | A screen/menu ask when the access workflow is disabled. | — |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `GET /approval/tray`
- `POST /approval/request/{id}/cancel`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What is asked for.

```json
{
  "target": {
    "kind": "screen",
    "path": "/crm/leads",
    "app": "appmint"
  },
  "request": {
    "reason": "I cover the front desk on Fridays",
    "label": "Leads"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The decision state |
| `400` | target.path is required — A screen or menu ask without a path. |
| `401` | Sign in to ask — The caller has no email on their session. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | page 66f1c0ffee12ab34cd56ef78 not found — The record to publish does not exist. |
| `422` | Access requests are switched off in this org: no enabled workflow takes access_request (check the collection's Enable Workflow and the workflow's Enabled switch) — A screen/menu ask when the access workflow is disabled. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "decision": "pending",
  "requestId": "66f1c0ffee12ab34cd56ef70",
  "taskId": "66f1c0ffee12ab34cd56ef78",
  "approvers": [
    "owner@acme.com"
  ],
  "escalatesAt": "2026-09-30T14:00:00.000Z"
}
```

## GET /approval/tray

**Get my approval tray**

`operationId: ApprovalController_tray`

Everything that needs the caller: decisions waiting on them (`waitingOnMe` — only tasks whose workflow is a decision, not work tasks or workspace cards), their own open access requests (`myRequests`), and their notices, newest first, with the unread count. Reads up to 200 waiting tasks, 100 requests and 300 notices.

#### Signature

```http
GET /approval/tray () -> { waitingOnMe, myRequests, notices, unread, counts }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /approval/history`
- `POST /approval/notices/read`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { waitingOnMe, myRequests, notices, unread, counts } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /approval/history

**Get my decided approvals**

`operationId: ApprovalController_history`

What the caller decided (`decided` — tasks they moved that are now approved, rejected, done or cancelled) and the answers to their own access requests (`myRequests`), newest first, up to 100 of each.

#### Signature

```http
GET /approval/history () -> { decided, myRequests }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /approval/tray`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { decided, myRequests } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /approval/task/{taskId}/approve

**Approve**

`operationId: ApprovalController_approve`

Moves the task to its workflow's "yes" stage (Approved, or Published for a publishing flow) and tells the person who asked.

- **Access request**: `grant` is required and says how — `{ via: "role" | "group", name }`. The requester gets that role or group; access is never granted one-off.
- **Publish request**: the page or post is published (a product is marked published).

Only an assignee of the task — or an Owner, ConfigAdmin or RootAdmin — may decide. Checks run before anything is granted, so a task that cannot be decided never half-happens.

#### Signature

```http
POST /approval/task/{taskId}/approve (taskId: string, body) -> { taskId, decision, granted?, task }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | TASK_NOT_FOUND | Task not found | No task with that id. | — |
| `409` | ALREADY_DECIDED | Already approved | The task is no longer open (the sentence names its status). | — |
| `403` | NOT_WAITING_ON_YOU | This is not waiting on you | The caller is not one of the task's assignees and not an Owner, ConfigAdmin or RootAdmin. | — |
| `422` | NOT_A_DECISION | This is a prep-pipeline task, not an approval — move it on its own board | The task's workflow has no Approved/Published and Rejected stages. The body also carries `reason: "not-a-decision"`. | — |
| `400` | GRANT_REQUIRED | Say how: a role or a group to grant | Approving an access request without `grant.via` and `grant.name`. The body also carries `reason: "grant-required"`. | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /approval/task/{taskId}/reject`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `taskId` | path | string | yes | The decision task `sk` (from the tray). |

### Request body

The note, and for access requests the grant.

```json
{
  "grant": {
    "via": "group",
    "name": "front-desk"
  },
  "note": "Fridays only for now"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { taskId, decision, granted?, task } |
| `400` | Say how: a role or a group to grant — Approving an access request without `grant.via` and `grant.name`. The body also carries `reason: "grant-required"`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | This is not waiting on you — The caller is not one of the task's assignees and not an Owner, ConfigAdmin or RootAdmin. |
| `404` | Task not found — No task with that id. |
| `409` | Already approved — The task is no longer open (the sentence names its status). |
| `422` | This is a prep-pipeline task, not an approval — move it on its own board — The task's workflow has no Approved/Published and Rejected stages. The body also carries `reason: "not-a-decision"`. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "taskId": "66f1c0ffee12ab34cd56ef78",
  "decision": "approve",
  "granted": "group front-desk",
  "task": {
    "id": "66f1c0ffee12ab34cd56ef78",
    "status": "approved"
  }
}
```

## POST /approval/task/{taskId}/reject

**Reject**

`operationId: ApprovalController_reject`

Moves the task to its workflow's Rejected stage, records the note, and tells the person who asked. Same permission rule as approving.

#### Signature

```http
POST /approval/task/{taskId}/reject (taskId: string, body) -> { taskId, decision, task }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | TASK_NOT_FOUND | Task not found | No task with that id. | — |
| `409` | ALREADY_DECIDED | Already approved | The task is no longer open (the sentence names its status). | — |
| `403` | NOT_WAITING_ON_YOU | This is not waiting on you | The caller is not one of the task's assignees and not an Owner, ConfigAdmin or RootAdmin. | — |
| `422` | NOT_A_DECISION | This is a prep-pipeline task, not an approval — move it on its own board | The task's workflow has no Approved/Published and Rejected stages. The body also carries `reason: "not-a-decision"`. | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /approval/task/{taskId}/approve`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `taskId` | path | string | yes | The decision task `sk` (from the tray). |

### Request body

Why.

```json
{
  "note": "Ask your manager first"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { taskId, decision, task } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | This is not waiting on you — The caller is not one of the task's assignees and not an Owner, ConfigAdmin or RootAdmin. |
| `404` | Task not found — No task with that id. |
| `409` | Already approved — The task is no longer open (the sentence names its status). |
| `422` | This is a prep-pipeline task, not an approval — move it on its own board — The task's workflow has no Approved/Published and Rejected stages. The body also carries `reason: "not-a-decision"`. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /approval/request/{id}/cancel

**Withdraw my access request**

`operationId: ApprovalController_cancel`

The person who asked takes it back while it is still pending. The workflow task is cancelled and the approvers are told.

#### Signature

```http
POST /approval/request/{id}/cancel (id: string, body) -> { requestId, status: "cancelled" }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | REQUEST_NOT_FOUND | Request not found | No request with that id. | — |
| `403` | NOT_YOUR_REQUEST | Not your request | Someone else asked. | — |
| `409` | ALREADY_DECIDED | Already approved | The request is no longer pending (the sentence names its status). | — |

Plus the standard platform errors: `401`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | The `access_request` `sk` (`requestId` from the ask). |

### Request body

Why.

```json
{
  "reason": "No longer needed"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { requestId, status: "cancelled" } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Not your request — Someone else asked. |
| `404` | Request not found — No request with that id. |
| `409` | Already approved — The request is no longer pending (the sentence names its status). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /approval/notices/read

**Mark all my notices read**

`operationId: ApprovalController_readAll`

Marks every unread tray notice addressed to the caller as read (up to 200 at a time).

#### Signature

```http
POST /approval/notices/read () -> { read }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /approval/notices/{id}/read`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { read } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "read": 4
}
```

## POST /approval/notices/{id}/read

**Mark one notice read**

`operationId: ApprovalController_readOne`

Marks one tray notice read. It must be addressed to the caller.

#### Signature

```http
POST /approval/notices/{id}/read (id: string) -> { read: 1 }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | NOT_YOUR_NOTICE | Not your notice | The notice does not exist or is not addressed to the caller. | — |

Plus the standard platform errors: `401`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Notice id from the tray. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { read: 1 } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Not your notice — The notice does not exist or is not addressed to the caller. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /approval/setup

**Set up access requests**

`operationId: ApprovalController_setup`

Makes sure the org has an access workflow: when a workflow already lists `access_request` it is left alone (`seeded: false`); otherwise the shipped `access-approval` is created. Safe to repeat — asking runs it too.

#### Signature

```http
POST /approval/setup () -> { workflow, seeded }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { workflow, seeded } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "workflow": "access-approval",
  "seeded": false
}
```

