Register an app
The apps.create operation: POST /api/v1/apps in the REST API and the apps_create tool in the MCP server.
/api/v1/appsRegisters 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 incredentials.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.
At a glance
- REST:
POST /api/v1/apps - MCP tool:
apps_create - Operation ID:
apps.create - Scope: Organization
- Permission:
agents:admin - Risk tier:
security - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: API key with the
agents:adminscope - Fresh sign-in: In the console, a sign-in within the last 15 minutes
- Since: M10
Request
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | |
publisher | string | Yes | |
logoUrl | string | No | |
websiteUrl | string | No | |
privacyPolicyUrl | string | No | |
redirectUris | array of string | Yes | |
type | one of confidential, public | No | |
visibility | one of private, listed | No | |
scopes | array of one of 84 values, e.g. org:read, org:write, org:admin (all in the OpenAPI document) | Yes | |
dpopBoundAccessTokens | boolean | No | Require DPoP for every token: a token request without a DPoP proof is refused (RFC 9449). |
Response
201 Created with a JSON object:
| Field | Type | Description |
|---|---|---|
app | object | |
app.id | ocl_… ID | |
app.object | "app" | |
app.ownerOrgId | org_… ID | |
app.name | string | |
app.publisher | string | |
app.logoUrl | string or null | |
app.websiteUrl | string or null | |
app.privacyPolicyUrl | string or null | |
app.redirectUris | array of string | |
app.type | one of confidential, public | |
app.visibility | one of private, listed | |
app.review | object | |
app.review.status | one of not_submitted, pending, changes_requested, approved, rejected | |
app.review.at | string or null | |
app.verified | boolean | |
app.scopes | array of one of 84 values, e.g. org:read, org:write, org:admin (all in the OpenAPI document) | |
app.secretPrefix | string or null | |
app.dpopBoundAccessTokens | boolean | |
app.status | one of active, disabled | |
app.suspension | object or null | |
app.createdAt | string | |
app.updatedAt | string | |
credentials | object | |
credentials.clientId | ocl_… ID | |
credentials.clientSecret | string or null | |
credentials.tokenEndpoint | string | |
credentials.authMethod | one of none, client_secret_basic, client_secret_post, private_key_jwt |
Examples
A listed accounting app
POST /api/v1/apps
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/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"
]
}
Response (201):
{
"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"
}
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | A field is invalid, e.g. a redirect URI that isn't https or loopback. | Fix the fields listed in details.issues and send the request again. |
permission_denied | 403 | You can't manage the organization's apps. | Ask an organization owner or admin for a role or scope that allows this action. |
fresh_auth_required | 403 | Your last sign-in is older than 15 minutes. | Sign in again, then repeat the action within 15 minutes. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, resource_not_found, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.