Auth API · apps
Install an app in the organization, or consent to it again
PUT /api/auth/organizations/{organizationId}/installations/{appId}
The grant is everything the app asks for — the installer never shapes an integration: one who lacks any scope it asks for is refused with 403 GrantExceedsYours naming what they lack (ask an owner or admin), and nothing is installed or narrowed. From then the app's token carries exactly that grant, and an embedded app is an entry under Apps in the admin. A re-consent restores the whole grant, and an app the organization removed is installed again by this call. consentVia records where the person consented: consent-card (the default — the admin's install screen) or onboarding, the setup walk's picker, where choosing the app is the consent; only a first-party app with a step in the walk is installed so (403 OnboardingConsentFirstPartyOnly, 409 AppHasNoOnboarding). A legacy platform may not be installed, by this call or any other, once the organization has gone live by decision (409 OrganizationAlreadyLive): it was retired then, one-way. 403 AdminRequired for a member who is not an owner or admin, 404 AppNotFound, 400 InvalidInstall for a body that is not this shape. Appends auth.app-installed. Idempotent by state: installing an app already installed with everything it asks for is acknowledged and appends nothing, wherever the consent was given.
Access
Requires a signed-in session: An owner or admin of the organization, checked against its membership by the route (the BetterAuth session or the person's tenant JWT for that organization)
How scopes come from roles: Scopes and roles. Getting a token: Authentication and app tokens.
Retries
Idempotent by state: installing an app already installed with everything it asks for is acknowledged and appends nothing, wherever the consent was given.
Why every write carries a key: Idempotency and retries.
Facts appended
auth.app-installed
What a fact is: Facts and the journal.
AI agent tool
No agent tool. Installing an app grants it the organization's data under its own identity: an owner's or admin's decision in Apps › Install, never an agent's.
Connecting an agent: AI agents.
Example
curl -X PUT 'https://auth.getmanifold.io/api/auth/organizations/org_01a156d2-8680-7738-ab2e-ff985e454887/installations/app_7c2e91d4-5a3b-4f60-8d17-3e9b0a6c4f21' \
-H "Authorization: Bearer $MANIFOLD_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"consentVia": "consent-card"
}'const response = await fetch("https://auth.getmanifold.io/api/auth/organizations/org_01a156d2-8680-7738-ab2e-ff985e454887/installations/app_7c2e91d4-5a3b-4f60-8d17-3e9b0a6c4f21", {
method: "PUT",
headers: { Authorization: `Bearer ${process.env.MANIFOLD_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"consentVia": "consent-card"
}),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const answer = await response.json();{
"consentVia": "consent-card"
}{
"id": "ins_01a16043-84a2-7c20-8701-12d6f6a127c6",
"appId": "app_7c2e91d4-5a3b-4f60-8d17-3e9b0a6c4f21",
"tenantId": "org_01a156d2-8680-7738-ab2e-ff985e454887",
"status": "installed",
"scopes": [
"orders:write",
"products:read",
"catalogs:read",
"stock:read",
"locations:write",
"integrations:write"
],
"installedBy": "usr_01a15709-7582-7646-b491-261a138f0650",
"consentVia": "consent-card",
"installedAt": "2026-10-21T21:04:10.000Z"
}The panel writes this call in other languages. It does not send it: Manifold's APIs do not take calls from a web
page on another site, so run the call from your own code or terminal. The whole API as OpenAPI: auth-service.openapi.json. Base URL https://auth.getmanifold.io.
Errors
Every refusal is a status and a JSON body whose _tag names it. A refused call changes nothing.
400
| Refusal | What it means | What to do |
|---|---|---|
InvalidInstall reason | The install request is not in the shape this call takes. | Fix the request body. |
401
| Refusal | What it means | What to do |
|---|---|---|
Unauthorized reason | The token is missing, expired or not one Manifold issued. | Get a fresh token and send the call again. |
TokenAlreadyUsed tokenId | A single-use app token was already spent. | Mint a new token for this call. |
402
| Refusal | What it means | What to do |
|---|---|---|
SubscriptionRequired status | The organization's subscription has lapsed, so writes are refused. | An owner settles billing. Reads still work meanwhile. |
403
| Refusal | What it means | What to do |
|---|---|---|
AdminRequired | Only an owner or admin of the organization may do this. | Ask an owner or admin. |
GrantExceedsYours scopes, views | The grant would give more than you hold yourself. | Ask an owner or admin to make it. |
OnboardingConsentFirstPartyOnly appId | Only an app Manifold runs records consent this way. | Install the app from the admin instead. |
TenantRequired userId | The token names no organization. | Choose the organization, then get a new token. |
ScopeRequired required | The token's roles do not grant the scope this call needs. | Ask an owner or admin for a role that grants it. |
404
| Refusal | What it means | What to do |
|---|---|---|
AppNotFound appId | No such record in this organization. | Check the id in the path or the body. |
409
| Refusal | What it means | What to do |
|---|---|---|
AppHasNoOnboarding appId | This app has no setup page to open. | Open the app from its own page instead. |
OrganizationAlreadyLive appId | The organization has gone live, so the app it ran beside is retired for good. | Nothing to do: the app is no longer needed. |
MirroredFromSource source | This area is fed by your source system until the organization goes live. | Make the change in the source system, or go live first. |
503
| Refusal | What it means | What to do |
|---|---|---|
RequestLogUnavailable tokenId, reason | The service could not record the request, so it did nothing. | Send the same request again shortly. |
How refusals work everywhere: Errors.