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.
/api/v1/platform/app_reviews/{id}/decideRecords 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-Keyrequired - Modes: Test and live
- Authentication: API key with the
platform:writescope - 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
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):
{
"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 | 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 | 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 | 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 | 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 | 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 | 409 | The app changed since you reviewed it (expectedVersion); reload it. | Fetch the object again and retry the change. |
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 | 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: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, rate_limited, internal_error.