# Approve, reject or ask for changes to a listed app

> The platform.app_reviews.decide operation: POST /api/v1/platform/app_reviews/{id}/decide in the REST API and the platform_app_reviews_decide tool in the MCP server.

Records a platform admin's decision on a listed app waiting for review (spec §41.3), with a `reason` the
publisher reads and the review `checklist` as ticked (stored with the decision):

- `approve`: the app is listed for every organization, with a verified publisher, for every scope it asks for
  now. Every required checklist item must be ticked, the application complete, and the organization's
  acceptance of the Partner and App Terms current;
- `request_changes`: the publisher edits the application and submits it again; an app approved before stays
  listed for its approved scopes;
- `reject`: the listing ends: other organizations can no longer install the app (existing installations keep
  working until each organization removes them).

`expectedVersion` is the app's `version` you reviewed: a decision on an application that changed since fails
with `version_conflict`. The owning organization gets an `app.listing_*` event and the app's contact an email
with the reason; the reviewer isn't named. Platform admins only.

## At a glance

- **REST:** `POST /api/v1/platform/app_reviews/{id}/decide`
- **MCP tool:** `platform_app_reviews_decide`
- **Operation ID:** `platform.app_reviews.decide`
- **Scope:** Platform
- **Permission:** `platform:write`
- **Risk tier:** `write`
- **Dry run:** Supported (send `Ownrecord-Dry-Run: true`)
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Authentication:** API key with the `platform:write` scope
- **Since:** M13

## Request

### Path parameters

| Field | Type       | Description |
| ----- | ---------- | ----------- |
| `id`  | `ocl_…` ID |             |

### Body

A JSON object (`Content-Type: application/json`):

| Field                       | Type                                          | Required | Description                                                                         |
| --------------------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------------- |
| `decision`                  | one of `approve`, `request_changes`, `reject` | Yes      | `approve`, `request_changes` or `reject`.                                           |
| `reason`                    | string                                        | Yes      | Why: what you checked, what to change or why it's rejected. The publisher reads it. |
| `checklist`                 | object                                        | Yes      | The checklist items you ticked.                                                     |
| `checklist.version`         | integer (at least 1)                          | Yes      | The review checklist's current version (`platform.app_reviews.get`).                |
| `checklist.items`           | array of object                               | Yes      |                                                                                     |
| `checklist.items[].id`      | string                                        | Yes      |                                                                                     |
| `checklist.items[].checked` | boolean                                       | Yes      |                                                                                     |
| `expectedVersion`           | integer (at least 1)                          | Yes      | The app's `version` you reviewed.                                                   |

## Response

`200 OK` with a JSON object:

| Field               | Type                                                                                                                         | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `id`                | `ocl_…` ID                                                                                                                   |             |
| `object`            | `"app_listing"`                                                                                                              |             |
| `ownerOrgId`        | `org_…` ID                                                                                                                   |             |
| `name`              | string                                                                                                                       |             |
| `publisher`         | string                                                                                                                       |             |
| `visibility`        | one of `private`, `listed`                                                                                                   |             |
| `appStatus`         | one of `active`, `disabled`                                                                                                  |             |
| `status`            | one of `not_submitted`, `pending`, `changes_requested`, `approved`, `rejected`                                               |             |
| `listed`            | boolean                                                                                                                      |             |
| `scopes`            | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` (all in the OpenAPI document)                           |             |
| `approvedScopes`    | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` (all in the OpenAPI document) or null                   |             |
| `pendingScopes`     | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` (all in the OpenAPI document)                           |             |
| `submittedAt`       | date-time or null                                                                                                            |             |
| `decidedAt`         | date-time or null                                                                                                            |             |
| `reason`            | string or null                                                                                                               |             |
| `application`       | object or null                                                                                                               |             |
| `history`           | array of object                                                                                                              |             |
| `history[].action`  | one of `submitted`, `withdrawn`, `approved`, `rejected`, `changes_requested`, `scopes_added`, `identity_changed`, `unlisted` |             |
| `history[].at`      | date-time                                                                                                                    |             |
| `history[].by`      | string or null                                                                                                               |             |
| `history[].reason`  | string or null                                                                                                               |             |
| `history[].scopes`  | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` (all in the OpenAPI document)                           |             |
| `version`           | integer                                                                                                                      |             |
| `org`               | object                                                                                                                       |             |
| `org.id`            | `org_…` ID                                                                                                                   |             |
| `org.slug`          | string                                                                                                                       |             |
| `org.name`          | string                                                                                                                       |             |
| `checklist`         | object or null                                                                                                               |             |
| `redirectUris`      | array of string                                                                                                              |             |
| `websiteUrl`        | string or null                                                                                                               |             |
| `logoUrl`           | string or null                                                                                                               |             |
| `unjustifiedScopes` | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` (all in the OpenAPI document)                           |             |

## Examples

### Approve after checking the application

```http
POST /api/v1/platform/app_reviews/ocl_01m4ttwd3veh9apbewxheq5xha/decide
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "decision": "approve",
  "reason": "Identity, privacy policy and questionnaire checked; scopes match the filing service.",
  "checklist": {
    "version": 1,
    "items": [
      {
        "id": "publisher_identity",
        "checked": true
      },
      {
        "id": "contact_confirmed",
        "checked": true
      },
      {
        "id": "privacy_policy",
        "checked": true
      },
      {
        "id": "questionnaire_acceptable",
        "checked": true
      },
      {
        "id": "scopes_justified",
        "checked": true
      },
      {
        "id": "redirect_uris",
        "checked": true
      },
      {
        "id": "branding",
        "checked": true
      }
    ]
  },
  "expectedVersion": 3
}
```

Response (`200`):

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app_listing",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "visibility": "listed",
  "appStatus": "active",
  "status": "approved",
  "listed": true,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "approvedScopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "pendingScopes": [],
  "submittedAt": "2026-10-30T09:00:00.000Z",
  "decidedAt": "2026-10-31T10:15:00.000Z",
  "reason": "Identity, privacy policy and questionnaire checked; scopes match the filing service.",
  "application": {
    "publisher": {
      "legalName": "Ledgerly AI B.V.",
      "country": "NL",
      "websiteUrl": "https://ledgerly.example",
      "contact": {
        "name": "Mira de Vries",
        "email": "partners@ledgerly.example"
      }
    },
    "privacyPolicyUrl": "https://ledgerly.example/privacy",
    "questionnaire": {
      "version": 1,
      "answers": [
        {
          "questionId": "data_location",
          "answer": null,
          "details": "AWS eu-central-1 (Frankfurt), Germany"
        },
        {
          "questionId": "encryption_in_transit",
          "answer": "yes",
          "details": null
        },
        {
          "questionId": "encryption_at_rest",
          "answer": "yes",
          "details": null
        },
        {
          "questionId": "credential_storage",
          "answer": "yes",
          "details": null
        },
        {
          "questionId": "staff_access",
          "answer": "yes",
          "details": null
        },
        {
          "questionId": "incident_response",
          "answer": "yes",
          "details": null
        },
        {
          "questionId": "subprocessors",
          "answer": "yes",
          "details": "AWS (hosting), Postmark (email)"
        },
        {
          "questionId": "model_training",
          "answer": "no",
          "details": null
        },
        {
          "questionId": "data_deletion",
          "answer": "yes",
          "details": "Within 30 days of uninstalling"
        },
        {
          "questionId": "vulnerability_management",
          "answer": "yes",
          "details": null
        }
      ]
    },
    "scopeJustifications": [
      {
        "scope": "filings:read",
        "justification": "Reads filing periods to prepare each return."
      },
      {
        "scope": "filings:write",
        "justification": "Records the prepared return and its submission."
      },
      {
        "scope": "tasks:read",
        "justification": "Finds the filing tasks assigned to the app."
      },
      {
        "scope": "tasks:write",
        "justification": "Claims and completes filing tasks."
      },
      {
        "scope": "ledger:read",
        "justification": "Reconciles the return with the tax payable accounts."
      }
    ],
    "partnerTermsVersion": "1.0",
    "submittedBy": "usr_01m4ttwd3veh9apbewxheq5xhf"
  },
  "history": [
    {
      "action": "submitted",
      "at": "2026-10-30T09:00:00.000Z",
      "by": "usr_01m4ttwd3veh9apbewxheq5xhf",
      "reason": null,
      "scopes": []
    },
    {
      "action": "approved",
      "at": "2026-10-31T10:15:00.000Z",
      "by": "usr_01m4ttwd3veh9apbewxheq5xhg",
      "reason": "Identity, privacy policy and questionnaire checked; scopes match the filing service.",
      "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"]
    }
  ],
  "version": 4,
  "org": {
    "id": "org_01m4ttwd3veh9apbewxheq5xhb",
    "slug": "ledgerly",
    "name": "Ledgerly AI"
  },
  "checklist": {
    "version": 1,
    "items": [
      {
        "id": "publisher_identity",
        "checked": true
      },
      {
        "id": "contact_confirmed",
        "checked": true
      },
      {
        "id": "privacy_policy",
        "checked": true
      },
      {
        "id": "questionnaire_acceptable",
        "checked": true
      },
      {
        "id": "scopes_justified",
        "checked": true
      },
      {
        "id": "redirect_uris",
        "checked": true
      },
      {
        "id": "branding",
        "checked": true
      }
    ]
  },
  "redirectUris": ["https://ledgerly.example/oauth/callback"],
  "websiteUrl": "https://ledgerly.example",
  "logoUrl": null,
  "unjustifiedScopes": []
}
```

## Errors

| Code                                                                                                               | HTTP status | When                                                                                                       | What to do                                                                                                                                                                          |
| ------------------------------------------------------------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`app_review_not_pending`](https://ownrecord.io/docs/reference/errors.md#app_review_not_pending)                   | 409         | The app isn't waiting for review.                                                                          | Reload the review queue: the application was already decided or withdrawn, or the partner hasn't submitted its changes yet.                                                         |
| [`app_review_checklist_incomplete`](https://ownrecord.io/docs/reference/errors.md#app_review_checklist_incomplete) | 400         | An approval without every required checklist item ticked.                                                  | Check each required item of the review checklist and tick it, or request changes or reject with a reason instead. `details.missing` lists the items.                                |
| [`app_listing_incomplete`](https://ownrecord.io/docs/reference/errors.md#app_listing_incomplete)                   | 400         | An approval of an application that is missing or incomplete (e.g. an added scope without a justification). | Answer every required question of the current security questionnaire, justify every scope the app asks for, and give a privacy policy URL. `details.problems` lists what's missing. |
| [`partner_terms_not_accepted`](https://ownrecord.io/docs/reference/errors.md#partner_terms_not_accepted)           | 409         | An approval while the organization's Partner and App Terms acceptance isn't current.                       | An owner or admin reads the Partner and App Terms at /legal/partners and accepts them under Developers → Apps (or with `partner_terms.accept`), then submits the application again. |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)                             | 400         | No reason, a checklist version that isn't the current one, or an unknown checklist item.                   | Fix the fields listed in `details.issues` and send the request again.                                                                                                               |
| [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict)                               | 409         | The app changed since you reviewed it (`expectedVersion`); reload it.                                      | Fetch the object again and retry the change.                                                                                                                                        |
| [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)                           | 404         | No registered app has this ID.                                                                             | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                                                                                |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)                             | 403         | You aren't a platform admin.                                                                               | Ask an organization owner or admin for a role or scope that allows this action.                                                                                                     |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
