# Apps tools

> MCP tools for Apps: apps_create, apps_disable, apps_get, apps_installations, apps_list, apps_listing_get, apps_listing_submit, apps_listing_withdraw, apps_rotate_secret, apps_update.

- `apps_create`: Register an app
- `apps_disable`: Disable a registered app
- `apps_get`: Get a registered app
- `apps_installations`: List an app's installations
- `apps_list`: List the organization's registered apps
- `apps_listing_get`: Get an app's listing
- `apps_listing_submit`: Submit an app for listing
- `apps_listing_withdraw`: Withdraw an app's listing application
- `apps_rotate_secret`: Rotate a registered app's client secret
- `apps_update`: Update a registered app

## `apps_create`

**Register an app.** Operation [`apps.create`](https://ownrecord.io/docs/reference/apps-create.md) · Organization level · permission `agents:admin` · risk tier `security` · since M10

Registers an OAuth app (a partner integration) that organizations install through Ownrecord's consent screen.
It uses the authorization code flow with PKCE at `/oauth/auth` and `/oauth/token`, with rotating refresh
tokens, and acts in each organization that installs it, with at most the installing admin's access.

- `type: "confidential"` (default): a server-side app. The response contains its client secret in
  `credentials.clientSecret`, **shown only once**.
- `type: "public"`: a native or browser app without a secret (PKCE only).

`visibility: "private"` apps install only in your own organization. `"listed"` apps can be installed by any
organization once platform review approved them: apply with `apps.listing.submit` (publisher identity, privacy
policy, security questionnaire, scope justification); until then the listing is `not_submitted` and other
organizations can't install the app. `scopes` are the most the app may ask for.
`dpopBoundAccessTokens: true` requires a `DPoP` proof on every token request (RFC 9449): access and refresh
tokens are then always bound to the app's key.

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument                | Type                                                                 | Required | Description                                                                                                                                                                                              |
| ----------------------- | -------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | string                                                               | yes      | (1–100 characters)                                                                                                                                                                                       |
| `publisher`             | string                                                               | yes      | (1–100 characters)                                                                                                                                                                                       |
| `logoUrl`               | string                                                               | no       | (at most 2000 characters)                                                                                                                                                                                |
| `websiteUrl`            | string                                                               | no       | (at most 2000 characters)                                                                                                                                                                                |
| `privacyPolicyUrl`      | string                                                               | no       | (at most 2000 characters)                                                                                                                                                                                |
| `redirectUris`          | array of string                                                      | yes      | (1–10 items)                                                                                                                                                                                             |
| `type`                  | one of `confidential`, `public`                                      | no       | (default `"confidential"`)                                                                                                                                                                               |
| `visibility`            | one of `private`, `listed`                                           | no       | (default `"private"`)                                                                                                                                                                                    |
| `scopes`                | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` | yes      | (1–200 items)                                                                                                                                                                                            |
| `dpopBoundAccessTokens` | boolean                                                              | no       | Require DPoP for every token: a token request without a DPoP proof is refused (RFC 9449). (default `false`)                                                                                              |
| `idempotency_key`       | string                                                               | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`               | boolean                                                              | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is invalid, e.g. a redirect URI that isn't https or loopback.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't manage the organization's apps.
- [`fresh_auth_required`](https://ownrecord.io/docs/reference/errors.md#fresh_auth_required): Your last sign-in is older than 15 minutes.

### Example

**A listed accounting app**

Arguments:

```json
{
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "websiteUrl": "https://ledgerly.example",
  "privacyPolicyUrl": "https://ledgerly.example/privacy",
  "redirectUris": ["https://ledgerly.example/oauth/callback"],
  "visibility": "listed",
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "app": {
    "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
    "object": "app",
    "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
    "name": "Ledgerly AI",
    "publisher": "Ledgerly AI B.V.",
    "logoUrl": null,
    "websiteUrl": "https://ledgerly.example",
    "privacyPolicyUrl": "https://ledgerly.example/privacy",
    "redirectUris": ["https://ledgerly.example/oauth/callback"],
    "type": "confidential",
    "visibility": "listed",
    "review": {
      "status": "not_submitted",
      "at": null
    },
    "verified": false,
    "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
    "secretPrefix": "or_cs_Exampl",
    "dpopBoundAccessTokens": false,
    "status": "active",
    "suspension": null,
    "createdAt": "2026-10-25T09:00:00.000Z",
    "updatedAt": "2026-10-25T09:00:00.000Z"
  },
  "credentials": {
    "clientId": "ocl_01m4ttwd3veh9apbewxheq5xha",
    "clientSecret": "or_cs_ExampleOnlyNotARealSecret0000000000000000",
    "tokenEndpoint": "https://ownrecord.io/oauth/token",
    "authMethod": "client_secret_basic"
  }
}
```

## `apps_disable`

**Disable a registered app.** Operation [`apps.disable`](https://ownrecord.io/docs/reference/apps-disable.md) · Organization level · permission `agents:admin` · risk tier `security` · since M10

Disables an app for good, in every organization at once, in one transaction:

- every access and refresh token, authorization code and grant it holds is deleted, and so are the tokens of
  the agents it owns;
- every installation that isn't uninstalled is paused (`pausedReason: app_disabled`, whatever it was before);
  it never resumes (`app_installations.resume` answers `oauth_client_disabled`), and it can still be uninstalled;
- from then on the OAuth server issues the app and its agents nothing and refuses their tokens, and nothing
  installs it.

Your organization and every organization with the app installed get the `APP_DISABLED` alert (high: their
owners and admins are emailed); other organizations aren't told who disabled it. Nothing re-enables an app:
register a new one instead. Disabling a disabled app changes nothing.

Annotations: `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | ID (`ocl_…`) | yes      | –                                                                                                                                                                                                        |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean      | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The app or one of its installations changed at the same moment; retry.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't manage the organization's apps.

### Example

**Disable an app**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "logoUrl": null,
  "websiteUrl": "https://ledgerly.example",
  "privacyPolicyUrl": "https://ledgerly.example/privacy",
  "redirectUris": ["https://ledgerly.example/oauth/callback"],
  "type": "confidential",
  "visibility": "listed",
  "review": {
    "status": "approved",
    "at": "2026-10-25T09:00:00.000Z"
  },
  "verified": true,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "secretPrefix": "or_cs_Exampl",
  "dpopBoundAccessTokens": false,
  "status": "disabled",
  "suspension": null,
  "createdAt": "2026-10-25T09:00:00.000Z",
  "updatedAt": "2026-10-26T14:30:00.000Z"
}
```

## `apps_get`

**Get a registered app.** Operation [`apps.get`](https://ownrecord.io/docs/reference/apps-get.md) · Organization level · permission `agents:read` · risk tier `read` · since M10

Returns one of your organization's registered OAuth apps. Never the secret.

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument | Type         | Required | Description |
| -------- | ------------ | -------- | ----------- |
| `id`     | ID (`ocl_…`) | yes      | –           |

- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't see the organization's apps.

### Example

**An app**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha"
}
```

Result `output`:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "logoUrl": null,
  "websiteUrl": "https://ledgerly.example",
  "privacyPolicyUrl": "https://ledgerly.example/privacy",
  "redirectUris": ["https://ledgerly.example/oauth/callback"],
  "type": "confidential",
  "visibility": "listed",
  "review": {
    "status": "approved",
    "at": "2026-10-25T09:00:00.000Z"
  },
  "verified": true,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "secretPrefix": "or_cs_Exampl",
  "dpopBoundAccessTokens": false,
  "status": "active",
  "suspension": null,
  "createdAt": "2026-10-25T09:00:00.000Z",
  "updatedAt": "2026-10-25T09:00:00.000Z"
}
```

## `apps_installations`

**List an app's installations.** Operation [`apps.installations`](https://ownrecord.io/docs/reference/apps-installations.md) · Organization level · permission `agents:read` · risk tier `read` · since M10

For the publisher of a registered app: the organizations that installed it, newest first (at most 100), with
the scopes they granted and the installation's status. Each API call the app makes names one of them with
`Ownrecord-Org` when its token covers several.

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument | Type         | Required | Description |
| -------- | ------------ | -------- | ----------- |
| `id`     | ID (`ocl_…`) | yes      | –           |

- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't see the organization's apps.

### Example

**Where an app is installed**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha"
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "ains_01m4ttwd3veh9apbewxheq5xhc",
      "orgId": "org_01m4ttwd3veh9apbewxheq5xh3",
      "orgName": "Acme",
      "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write"],
      "status": "active",
      "createdAt": "2026-10-25T09:00:00.000Z",
      "updatedAt": "2026-10-25T09:00:00.000Z"
    }
  ]
}
```

## `apps_list`

**List the organization's registered apps.** Operation [`apps.list`](https://ownrecord.io/docs/reference/apps-list.md) · Organization level · permission `agents:read` · risk tier `read` · since M10

Lists the OAuth apps your organization registered for partners (Developers → Apps), newest first, with their review status. Never returns secrets. Filter by `status`; paginate with `limit` and `starting_after`.

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument         | Type                        | Required | Description |
| ---------------- | --------------------------- | -------- | ----------- |
| `limit`          | integer                     | no       | (1–100)     |
| `starting_after` | ID (`ocl_…`)                | no       | –           |
| `status`         | one of `active`, `disabled` | no       | –           |

- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A filter or pagination value is invalid.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't see the organization's apps.

### Examples

**Registered apps**

Arguments:

```json
{}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
      "object": "app",
      "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
      "name": "Ledgerly AI",
      "publisher": "Ledgerly AI B.V.",
      "logoUrl": null,
      "websiteUrl": "https://ledgerly.example",
      "privacyPolicyUrl": "https://ledgerly.example/privacy",
      "redirectUris": ["https://ledgerly.example/oauth/callback"],
      "type": "confidential",
      "visibility": "listed",
      "review": {
        "status": "approved",
        "at": "2026-10-25T09:00:00.000Z"
      },
      "verified": true,
      "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
      "secretPrefix": "or_cs_Exampl",
      "dpopBoundAccessTokens": false,
      "status": "active",
      "suspension": null,
      "createdAt": "2026-10-25T09:00:00.000Z",
      "updatedAt": "2026-10-25T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

**One per page**

Arguments:

```json
{
  "limit": 1
}
```

Result `output`:

```json
{
  "object": "list",
  "data": [
    {
      "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
      "object": "app",
      "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
      "name": "Ledgerly AI",
      "publisher": "Ledgerly AI B.V.",
      "logoUrl": null,
      "websiteUrl": "https://ledgerly.example",
      "privacyPolicyUrl": "https://ledgerly.example/privacy",
      "redirectUris": ["https://ledgerly.example/oauth/callback"],
      "type": "confidential",
      "visibility": "listed",
      "review": {
        "status": "approved",
        "at": "2026-10-25T09:00:00.000Z"
      },
      "verified": true,
      "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
      "secretPrefix": "or_cs_Exampl",
      "dpopBoundAccessTokens": false,
      "status": "active",
      "suspension": null,
      "createdAt": "2026-10-25T09:00:00.000Z",
      "updatedAt": "2026-10-25T09:00:00.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "ocl_01m4ttwd3veh9apbewxheq5xha"
}
```

## `apps_listing_get`

**Get an app's listing.** Operation [`apps.listing.get`](https://ownrecord.io/docs/reference/apps-listing-get.md) · Organization level · permission `agents:read` · risk tier `read` · since M13

Returns the listing of one of your organization's registered apps: the review `status`, whether it is `listed`
for every organization (a verified publisher) and for which scopes (`approvedScopes`; `pendingScopes` wait for
review), the last decision's `reason`, your application, its history, and the current security `questionnaire`
to answer with `apps.listing.submit`.

Statuses: `not_submitted` (no application yet, or withdrawn), `pending` (Ownrecord reviews it),
`changes_requested` (edit the application and submit it again), `approved`, `rejected`. An approved app stays
listed for its approved scopes while scopes it added are reviewed again.

Annotations: `readOnlyHint: true` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument | Type         | Required | Description |
| -------- | ------------ | -------- | ----------- |
| `id`     | ID (`ocl_…`) | yes      | –           |

- **Read-only:** changes nothing; `idempotency_key` is ignored.

### Errors

- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't see the organization's apps.

### Example

**An application waiting for review**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha"
}
```

Result `output`:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app_listing",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "visibility": "listed",
  "appStatus": "active",
  "status": "pending",
  "listed": false,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "approvedScopes": null,
  "pendingScopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "submittedAt": "2026-10-30T09:00:00.000Z",
  "decidedAt": null,
  "reason": null,
  "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": []
    }
  ],
  "version": 3,
  "questionnaire": {
    "version": 1,
    "status": "proposed",
    "questions": [
      {
        "id": "data_location",
        "type": "text",
        "required": true,
        "text": "Where do you store and process organizations' data? Name the countries and hosting providers.",
        "detailsRequiredWhen": null,
        "hint": null
      }
    ]
  }
}
```

## `apps_listing_submit`

**Submit an app for listing.** Operation [`apps.listing.submit`](https://ownrecord.io/docs/reference/apps-listing-submit.md) · Organization level · permission `agents:admin` · risk tier `write` · since M13

Applies to list one of your organization's apps for every organization on Ownrecord, or submits a changed
application. Ownrecord reviews it (`pending`) and approves it, asks for changes or rejects it, with a reason sent
to the contact you give. Once approved, the app is listed: any organization can install it, and its consent
screen shows a verified publisher.

The application names the publisher (legal name, country, website and a contact), links the app's privacy
policy, answers the current security questionnaire (`apps.listing.get` returns its questions and version) and
justifies every scope the app asks for. Your organization must have accepted the current Partner and App Terms
(`partner_terms.accept`). Submitting makes the app `listed`.

You can edit and submit again while the application waits for review or for your changes. Submitting again for
an approved app keeps it listed for its approved scopes during the review, unless the publisher's legal name,
country or website changed: then it is listed again only once approved.

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument              | Type            | Required | Description                                                                                                                                                                                              |
| --------------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | ID (`ocl_…`)    | yes      | –                                                                                                                                                                                                        |
| `publisher`           | object          | yes      | –                                                                                                                                                                                                        |
| `privacyPolicyUrl`    | string          | yes      | The app's privacy policy (it becomes the app's `privacyPolicyUrl`). (at most 2000 characters)                                                                                                            |
| `questionnaire`       | object          | yes      | –                                                                                                                                                                                                        |
| `scopeJustifications` | array of object | yes      | (at most 200 items)                                                                                                                                                                                      |
| `idempotency_key`     | string          | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`             | boolean         | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`app_listing_incomplete`](https://ownrecord.io/docs/reference/errors.md#app_listing_incomplete): A required question isn't answered, a scope isn't justified, or the questionnaire version isn't the current one.
- [`partner_terms_not_accepted`](https://ownrecord.io/docs/reference/errors.md#partner_terms_not_accepted): Your organization hasn't accepted the current Partner and App Terms.
- [`oauth_client_disabled`](https://ownrecord.io/docs/reference/errors.md#oauth_client_disabled): The app is disabled.
- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is invalid, e.g. a URL that isn't https.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The app changed at the same moment; retry.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't manage the organization's apps.

### Example

**Apply to list an accounting app**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "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",
        "details": "AWS eu-central-1 (Frankfurt), Germany"
      },
      {
        "questionId": "encryption_in_transit",
        "answer": "yes"
      },
      {
        "questionId": "encryption_at_rest",
        "answer": "yes"
      },
      {
        "questionId": "credential_storage",
        "answer": "yes"
      },
      {
        "questionId": "staff_access",
        "answer": "yes"
      },
      {
        "questionId": "incident_response",
        "answer": "yes"
      },
      {
        "questionId": "subprocessors",
        "answer": "yes",
        "details": "AWS (hosting), Postmark (email)"
      },
      {
        "questionId": "model_training",
        "answer": "no"
      },
      {
        "questionId": "data_deletion",
        "answer": "yes",
        "details": "Within 30 days of uninstalling"
      },
      {
        "questionId": "vulnerability_management",
        "answer": "yes"
      }
    ]
  },
  "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."
    }
  ],
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app_listing",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "visibility": "listed",
  "appStatus": "active",
  "status": "pending",
  "listed": false,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "approvedScopes": null,
  "pendingScopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "submittedAt": "2026-10-30T09:00:00.000Z",
  "decidedAt": null,
  "reason": null,
  "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": []
    }
  ],
  "version": 3
}
```

## `apps_listing_withdraw`

**Withdraw an app's listing application.** Operation [`apps.listing.withdraw`](https://ownrecord.io/docs/reference/apps-listing-withdraw.md) · Organization level · permission `agents:admin` · risk tier `write` · since M13

Withdraws an application that waits for review (`pending`) or for your changes (`changes_requested`). The
application stays saved for a later `apps.listing.submit`. A first application goes back to `not_submitted`; a
re-review of an approved app goes back to `approved`, still listed for its approved scopes only.

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | ID (`ocl_…`) | yes      | –                                                                                                                                                                                                        |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean      | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`app_listing_not_withdrawable`](https://ownrecord.io/docs/reference/errors.md#app_listing_not_withdrawable): The app has no application waiting for review or for changes.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The app changed at the same moment; retry.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't manage the organization's apps.

### Example

**Withdraw a first application**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app_listing",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "visibility": "listed",
  "appStatus": "active",
  "status": "not_submitted",
  "listed": false,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "approvedScopes": null,
  "pendingScopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "submittedAt": "2026-10-30T09:00:00.000Z",
  "decidedAt": null,
  "reason": null,
  "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": "withdrawn",
      "at": "2026-10-30T11:00:00.000Z",
      "by": "usr_01m4ttwd3veh9apbewxheq5xhf",
      "reason": null,
      "scopes": []
    }
  ],
  "version": 4
}
```

## `apps_rotate_secret`

**Rotate a registered app's client secret.** Operation [`apps.rotate_secret`](https://ownrecord.io/docs/reference/apps-rotate-secret.md) · Organization level · permission `agents:admin` · risk tier `security` · since M10

Replaces a confidential app's client secret. The new secret is in `credentials.clientSecret`, **shown only
once**; the old one stops working at once, so update your server first. Tokens already issued keep working.

Annotations: `readOnlyHint: false` · `destructiveHint: true` · `idempotentHint: false` · `openWorldHint: false`

### Arguments

| Argument          | Type         | Required | Description                                                                                                                                                                                              |
| ----------------- | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | ID (`ocl_…`) | yes      | –                                                                                                                                                                                                        |
| `idempotency_key` | string       | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`         | boolean      | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): The app is public and has no secret.
- [`oauth_client_disabled`](https://ownrecord.io/docs/reference/errors.md#oauth_client_disabled): The app is disabled.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The app changed at the same moment; retry.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't manage the organization's apps.
- [`fresh_auth_required`](https://ownrecord.io/docs/reference/errors.md#fresh_auth_required): Your last sign-in is older than 15 minutes.

### Example

**Rotate the secret**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "app": {
    "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
    "object": "app",
    "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
    "name": "Ledgerly AI",
    "publisher": "Ledgerly AI B.V.",
    "logoUrl": null,
    "websiteUrl": "https://ledgerly.example",
    "privacyPolicyUrl": "https://ledgerly.example/privacy",
    "redirectUris": ["https://ledgerly.example/oauth/callback"],
    "type": "confidential",
    "visibility": "listed",
    "review": {
      "status": "approved",
      "at": "2026-10-25T09:00:00.000Z"
    },
    "verified": true,
    "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
    "secretPrefix": "or_cs_Exampl",
    "dpopBoundAccessTokens": false,
    "status": "active",
    "suspension": null,
    "createdAt": "2026-10-25T09:00:00.000Z",
    "updatedAt": "2026-10-26T14:30:00.000Z"
  },
  "credentials": {
    "clientId": "ocl_01m4ttwd3veh9apbewxheq5xha",
    "clientSecret": "or_cs_ExampleOnlyNotARealSecret0000000000000000",
    "tokenEndpoint": "https://ownrecord.io/oauth/token",
    "authMethod": "client_secret_basic"
  }
}
```

## `apps_update`

**Update a registered app.** Operation [`apps.update`](https://ownrecord.io/docs/reference/apps-update.md) · Organization level · permission `agents:admin` · risk tier `security` · since M10

Changes a registered app's name, publisher, links, redirect URIs, visibility, scopes or whether every token must
be DPoP-bound (`dpopBoundAccessTokens`). Send only what changes; `null` removes a link. Narrower scopes apply to
the app's tokens at once in every installation; wider scopes need the organizations' consent again.

For listed apps (see `apps.listing.get`): `visibility: "listed"` waits for an application
(`apps.listing.submit`); `"private"` ends the listing. On an approved app, scopes beyond the approved ones go to
review again (`pending`): the app stays listed for its approved scopes, other organizations can consent only to
those, and installations keep the scopes they consented to until the new ones are approved and consented. A new
name or publisher of an approved app is reviewed again before the app is listed again.

Annotations: `readOnlyHint: false` · `destructiveHint: false` · `idempotentHint: true` · `openWorldHint: false`

### Arguments

| Argument                | Type                                                                 | Required | Description                                                                                                                                                                                              |
| ----------------------- | -------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                    | ID (`ocl_…`)                                                         | yes      | –                                                                                                                                                                                                        |
| `name`                  | string                                                               | no       | (1–100 characters)                                                                                                                                                                                       |
| `publisher`             | string                                                               | no       | (1–100 characters)                                                                                                                                                                                       |
| `logoUrl`               | string or null                                                       | no       | –                                                                                                                                                                                                        |
| `websiteUrl`            | string or null                                                       | no       | –                                                                                                                                                                                                        |
| `privacyPolicyUrl`      | string or null                                                       | no       | –                                                                                                                                                                                                        |
| `redirectUris`          | array of string                                                      | no       | (1–10 items)                                                                                                                                                                                             |
| `visibility`            | one of `private`, `listed`                                           | no       | –                                                                                                                                                                                                        |
| `scopes`                | array of one of 84 values, e.g. `org:read`, `org:write`, `org:admin` | no       | (1–200 items)                                                                                                                                                                                            |
| `dpopBoundAccessTokens` | boolean                                                              | no       | Require DPoP for every token: a token request without a DPoP proof is refused (RFC 9449).                                                                                                                |
| `idempotency_key`       | string                                                               | no       | A unique string for this change. Retrying with the same key and arguments never acts twice. When left out, the server generates one and returns it in the result's `idempotency_key`. (1–255 characters) |
| `dry_run`               | boolean                                                              | no       | true: check permissions and rules and compute the result, then roll everything back. Nothing is saved. (default `false`)                                                                                 |

- **Idempotency:** the result returns the `idempotency_key` used, yours or one the server generated. Retry with it: a call with the same key and arguments never acts twice (keys are kept for 24 hours).
- **Dry run:** supported. `dry_run: true` checks permissions and rules and computes the result, then rolls everything back.

### Errors

- [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed): A field is invalid.
- [`oauth_client_disabled`](https://ownrecord.io/docs/reference/errors.md#oauth_client_disabled): The app is disabled.
- [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found): Your organization registered no app with this ID.
- [`version_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict): The app changed at the same moment; retry.
- [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied): You can't manage the organization's apps.

### Example

**Add a redirect URI**

Arguments:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "redirectUris": ["https://ledgerly.example/oauth/callback", "https://eu.ledgerly.example/oauth/callback"],
  "idempotency_key": "8d0c5f8e-4b7a-4e43-9b0f-2f6a0c1d9e21"
}
```

Result `output`:

```json
{
  "id": "ocl_01m4ttwd3veh9apbewxheq5xha",
  "object": "app",
  "ownerOrgId": "org_01m4ttwd3veh9apbewxheq5xhb",
  "name": "Ledgerly AI",
  "publisher": "Ledgerly AI B.V.",
  "logoUrl": null,
  "websiteUrl": "https://ledgerly.example",
  "privacyPolicyUrl": "https://ledgerly.example/privacy",
  "redirectUris": ["https://ledgerly.example/oauth/callback", "https://eu.ledgerly.example/oauth/callback"],
  "type": "confidential",
  "visibility": "listed",
  "review": {
    "status": "approved",
    "at": "2026-10-25T09:00:00.000Z"
  },
  "verified": true,
  "scopes": ["filings:read", "filings:write", "tasks:read", "tasks:write", "ledger:read"],
  "secretPrefix": "or_cs_Exampl",
  "dpopBoundAccessTokens": false,
  "status": "active",
  "suspension": null,
  "createdAt": "2026-10-25T09:00:00.000Z",
  "updatedAt": "2026-10-26T14:30:00.000Z"
}
```
