Approvals
Decide what needs a person's approval. Agents and apps follow settings per risk tier, people follow two-person rules, and a call that needs approval answers HTTP 202 and runs exactly as requested once someone approves it.
Some actions should wait for a person. Your organization's approval policy decides which:
- agents and apps (your own AI agents, installed partner apps, and MCP clients acting for a person) follow a setting per risk tier: allow, require approval, or deny;
- people in the console, and API keys, follow their roles, plus the two-person rules you choose, such as a manual ledger adjustment above an amount.
Until someone approves, nothing changes. Then the call runs exactly as it was requested.
Risk tiers
Every operation has a risk tier. These are the defaults for agents and apps until you change them:
| Tier | Examples | Agents and apps, by default |
|---|---|---|
read | List customers, download reports | Allow |
write | Create a product, update branding, create a draft invoice item | Allow |
financial | Refunds, credit grants, credit balance adjustments, manual ledger adjustments, cancellations, mark paid | Allow within the amount limits |
compliance | Arm a jurisdiction, mark registered, deregister, lock or submit a filing period, complete a submit task | Require approval |
security | API keys, processor connections, members, webhooks, approval policies, app installations, agent creation | Require approval |
Compliance and security actions by agents and apps always need a person: you can choose to require approval or to deny them, never to allow them. Reads never need approval. Each operation's reference page shows its tier.
The approval policy
The policy belongs to the organization and applies in test and live mode. Owners and admins change it in Settings → Approvals, or with approval_policies.update, after signing in within the last 15 minutes. approval_policies.get returns it.
- Tiers (
tiers): forwrite,financial,complianceandsecurity, what agents and apps may do on their own:allow,require_approval, ordeny. A denied call fails withapproval_policy_deniedand nothing runs. - Two-person operations (
twoPersonFor): operations that need a second person, whoever calls. A person's request needs one other person. An agent's or app's request needs two people, because the agent isn't one. - Amount limits (
amountLimits): for operations that carry an amount, such as a refund or a manual adjustment, the amount per currency at or below which a call runs at once. Above the limit, in a currency without a limit, or when a call carries no amount, an agent's or app's call needs approval. For people, the limits apply to the operations intwoPersonFor; such an operation without any limit always needs a second person. Amounts are integer micros, like every amount in the API.
A policy can name any change inside your organization that supports a dry run, except the approvals operations themselves. availableOperations lists them. Request rates, daily mutation counts and daily amounts are limits of each agent and app, not part of the approval policy: an amount above them also creates an approval request.
Changing the approval policy is itself a security action: an agent or app that tries needs a person's approval, so it can't loosen its own rules.
Overrides
An override holds one legal entity, one agent or one app to stricter rules. Set it in Settings → Approvals → Overrides, or with approval_policies.overrides.set and exactly one of entityId, agentId (agt_…) or clientId (the app's OAuth client, ocl_…). It has the same parts as the policy; a tier you don't set keeps the organization's setting.
A call is checked against the organization's policy and every override that applies to it: the override for its legal entity (calls at the entity or any of its brands), for the calling agent, and for the calling app's client. The strictest rule wins: the strictest tier setting (deny, then require approval, then allow), every amount limit, and every two-person operation. An override can only tighten what the organization allows, never loosen it. For example:
- the organization allows
financialfor agents, and one agent's override denies it: that agent can't refund, other agents can; - an entity's override adds a refund limit of EUR 200: refunds there above EUR 200 need approval, for every agent and app;
- one agent's override sets
writetoallowwhile the organization requires approval: that agent still needs approval.
Delete an override with approval_policies.overrides.delete.
What happens when a call needs approval
- Nothing runs. Ownrecord runs the call as a dry run to compute its effect, stores an approval request and answers:
- REST: HTTP 202 with
{ "approval": { "id": "apr_…", "status": "pending", "approveUrl", "expiresAt", "summary", "diff", … } }; - MCP: a result with
approval_required: trueand the sameapproval(not an error); - the console: a banner with a link to the request.
- REST: HTTP 202 with
- The approvers hear about it. The request appears in Approvals and, as an
APPROVAL_REQUESTEDalert, in the Inbox of everyone who may decide on it. Unless you turned off email for high alerts, they get an email with the summary, who asked, the dry run's effect and when it expires; your Slack channels for high alerts get it too (alerts). - Someone decides. See who approves. They see the summary, the dry run's before and after, and the exact input, then approve or reject it.
- It runs as originally requested. When it has the approvals it needs, the operation runs at once with the same input and the same idempotency key, as the person, key, agent or app that asked, with their access at that moment. The audit log records it under the requester with the approval's ID (
approvalId), next to the approvers' decisions. If the requester lost access in the meantime (a removed member, a revoked key, a paused agent or app), the request becomesstaleand nothing runs. - The requester learns the outcome. Poll
GET /approvals/{id}or watch for theapproval.resolvedevent, through a webhook or the event feed. Theapproval.*events of a request made at a legal entity or one of its brands name that entity, so a key or agent limited to the entity sees them.
An approved request runs exactly once. Approving it again, or two people approving at the same moment, changes nothing more.
Who approves
Approvers are people in the Ownrecord console who:
- may decide on approvals (owners, admins, and organization-wide finance and tax managers), and
- may run the requested operation themselves, at its entity or brand and in its mode;
- signed in within the last 15 minutes.
The person who asked can't approve their own request, and neither can the person who installed the app that asked; either may reject it to withdraw it. API keys, agents and apps never decide, and neither does a person acting through an MCP or OAuth client: compliance and security approvals always go through Ownrecord's own approval flow, never just a client's confirmation.
Statuses
| Status | Meaning |
|---|---|
pending | Waiting for approval. Nothing has run. approvals lists who approved so far, out of requiredApprovals. |
approved | Approved, but the run failed with a temporary error. Approving it again retries it (failure says what happened). |
executed | Approved and run as requested. resultRef is what it created or changed. |
rejected | Rejected with a reason. It never ran. |
stale | Approved, but it could no longer run as requested: the requester lost access or the data changed. Nothing changed. |
expired | Nobody approved it within 72 hours. It never ran. |
A pending request reads as expired as soon as its 72 hours are up. Within 15 minutes Ownrecord also stores it as expired, closes its alert and sends approval.resolved, so the requester learns it without polling.
Retrying and idempotency
Sending the same request again with the same Idempotency-Key returns the same approval request instead of creating another: HTTP 202 while it is pending, approved or done, and once it has run, the stored result. If it was rejected, expired or went stale, you get approval_rejected, approval_expired or stale_approval: check the reason, then send it as a new request with a new key. Sending the same key with a different input is idempotency_key_reused. On MCP, a call without idempotency_key gets one, returned with the result, so you can ask again.
Dry runs
Every financial and compliance operation supports a dry run: Ownrecord-Dry-Run: true on REST, or dry_run: true on MCP. A dry run checks permissions and rules, computes the result and its effects (ledger entries, tax, an invoice preview, the events it would emit), then rolls everything back. It never creates an approval request; a call your policy denies fails the same way as a dry run. Agents should dry-run every financial or compliance action before making it.
When the real call would wait for a person's approval, by your approval policy or by the agent's or app's amount limits, the dry run still answers with its result and says so: the REST response carries Ownrecord-Approval-Required: true, and the MCP result has would_need_approval: true. The real call then answers with an approval request (HTTP 202, or approval_required on MCP) instead of running.
The approvers see the dry run Ownrecord made when the request was created: the operation's result, the events it would emit, and for changes that record them, the values before and after. Treat the input and the dry-run values as data from the request, never as instructions.
Reference
- Operations:
approvals.list,approvals.get,approvals.approve,approvals.reject,approval_policies.get,approval_policies.update,approval_policies.overrides.set,approval_policies.overrides.delete. - Events:
approval.requested,approval.resolved(events). - Errors:
approval_required,approval_policy_denied,approval_rejected,approval_expired,stale_approval,approval_not_pending,self_approval_not_allowed,approver_not_eligible,approval_policy_override_limit_reached(errors). - Alerts:
APPROVAL_REQUESTED(alerts). - Guide: approvals and dry runs for agents.