Log inTalk to us
Getting started

Apps: registry, installs and consent

An app extends the Ivora dashboard for one team and can call the API on a project's behalf. A team registers its apps; each project of the team decides whether to install one and exactly which scopes it may hold. Apps are private: only the owning team's projects can install them. Ivora's own apps are the exception: they are first-party, and every project can install them.

Hosting the app on a serverless platform? See Building serverless apps.

All routes below are for a signed-in person. API keys and connected MCP apps are refused (403 forbidden), so an automation never grants itself an app.

Note

The Ivora dashboard does not show apps in production yet. The API routes on this page are available in production.

Register an app

POST /v1/teams/{team_id}/apps (team owners, admins and developers, and Ivora administrators):

{
  "name": "Fleet ops",
  "ui_origins": ["https://fleet.ivoraapps.com"],
  "extensions": [
    {"point": "project.tab", "path": "/", "title": "Fleet ops"},
    {"point": "station.panel", "path": "/station", "title": "Warranty"},
    {"point": "overview.widget", "path": "/widget", "size": "1x1"},
    {"point": "action", "target": "station", "path": "/action", "title": "Open ticket"}
  ],
  "scopes": ["stations:read", "stations:control"],
  "webhook_url": "https://fleet.example.com/ivora/webhooks"
}
  • ui_origins are https origins with no path and no port, and each is one label (1-63 characters) under the environment's apps domain: https://<app>.ivoraapps.com in production, https://<app>.ivoraapps.dev in preproduction. Any other host, an explicit port (even :443) or a longer label is refused with 422 app_origin_outside_apps_domain, on registering and on updating. Only a test environment may set IVORA_APPS_DOMAIN to another domain; production and previews refuse to start otherwise. Hosts on Ivora's own domains are refused (app_origin_refused): the dashboard's sign-in cookie covers them, so the app would receive it. Host the app on its own domain.
  • Apps registered before this rule keep working (the check runs on register and update only), but their next PATCH of ui_origins must move to the apps domain. Ivora contacts each team whose app still uses another origin; re-host the UI and PATCH the origin (a new origin needs a new consent).
  • extensions say where the dashboard may show the app: project.tab, station.panel, session.panel, overview.widget or action.
  • The response carries client_secret once. Ivora stores only its digest; rotate it with POST /v1/teams/{team_id}/apps/{app_id}/secret.
  • A team registers at most 50 apps (app_limit).
  • Changing, archiving or rotating the secret of an app, and listing its installs, reaches every project it is installed in. A developer limited to some of the team's projects can do it only while every active install is in one of their projects; otherwise a team owner or admin does it.

Scopes an app may request

An app may request any project scope except billing:refund and stations:admin (app_scope_forbidden). It never reaches /v1/admin, API-key management, members or payment methods. Four scopes can act on chargers, money or prices and are marked sensitive on the consent screen:

ScopeLets the app
stations:controlSend real commands to chargers
billing:writeCreate bills and start or stop paid sessions
tariffs:writeSet prices: create, change and archive tariffs and choose the tariff of a connector, location or project
settlement:writeReport externally settled payments

An app sets prices only with tariffs:write: billing:write never creates, changes or assigns a tariff; a bill may still name another of the project's tariffs, reported as tariff_differs_from_connector.

Each install has its own request budget, 120 requests a minute by default. Ivora administrators can change it.

  1. GET /v1/tenants/{tenant_id}/apps lists the team's apps, and Ivora's first-party apps, with this project's active install, if any.
  2. GET /v1/tenants/{tenant_id}/apps/{app_id}/consent is the consent screen: the requested scopes with descriptions and sensitive flags, the rate limit and a revision.
  3. POST /v1/tenants/{tenant_id}/apps/{app_id}/install with the scopes you accept and that revision as consent_revision.

Only a team owner, admin or developer of the project (or an Ivora administrator, with a reason) installs. You can grant only scopes you hold yourself. An install never holds more than the app requests, and staff roles are never carried into it: it has exactly the scopes that were granted.

consent_revision ties the consent to what was shown. When the app's scopes, origins or extensions change, its revision rises and an old consent answers 409 consent_outdated (details.revision holds the current one). Narrowing the app's requested scopes removes them from every install at once; widening them grants nothing until an installer consents again.

PATCH /v1/tenants/{tenant_id}/installs/{install_id} replaces an install's scopes with a fresh consent.

First-party apps

An app with visibility: first_party is Ivora's own, such as Ivora Checkout. Only Ivora administrators set it, when registering the app or with PATCH /v1/teams/{team_id}/apps/{app_id} (403 forbidden for anyone else); each change is in the team's audit trail (app.visibility_changed).

  • Every project lists it in GET /v1/tenants/{tenant_id}/apps and can install it, with the same consent as any app. In a project of another team, only that team's owners and admins install it (403 forbidden for a developer); in the registering team's own projects, owners, admins and developers do, as for any app. It carries publisher: "Ivora", which the dashboard shows as made by Ivora.
  • Read from another team's project, it has no secret_prefix, secret_rotated_at, created_by or webhook_url (null). Its registry, secrets, deliveries and installs list stay with the team that registered it.
  • Only Ivora administrators change its webhook_url, its ui_origins, its client secret (POST .../secret) or its webhook signing key (POST .../webhook-secret): anyone else, including the registering team's owners, admins and developers, gets 403 app_first_party_staff_only. Reads stay as they are, and an unchanged value in a PATCH is accepted. Each staff change is in the team's audit trail (app.first_party_changed) with the actor and, for the webhook URL and origins, the values before and after; a rotation of either secret records only that it happened, never the secret.
  • It becomes private again only once no project outside its team has it installed (409 app_installed_outside_team).

Installs by Ivora staff

A project's own owners, admins and developers normally install apps. When a project asks Ivora to do it, an Ivora administrator who is not a member of the project's team installs with a reason:

{"scopes": ["stations:read"], "consent_revision": 3, "reason": "Requested by the project owner, ticket 4521"}

Without one the install answers 422 staff_reason_required; the consent view's reason_required says when it is needed. The install shows the reason as staff_reason, and the project's audit trail records it with the installer (app.installed, with staff: true). Every other install is recorded there too, with staff: false.

Revoke

DELETE /v1/tenants/{tenant_id}/installs/{install_id} uninstalls the app and revokes everything it held at once. DELETE /v1/teams/{team_id}/apps/{app_id} archives the app and revokes every install in every project (revoked_reason: app_archived). A revoked install stays in GET /v1/tenants/{tenant_id}/installs for the record; install the app again to restore access.

Lifecycle webhooks

When an app has a webhook_url, Ivora sends it a signed POST for each change to one of its installs:

typeWhendata
app.installedA project installs the appscopes, consent_revision, rate_limit_per_minute, installed_by
app.scopes_changedThe install's scopes change: a new consent (reason: consent) or the app stopped requesting some (reason: app_narrowed)scopes (now), previous_scopes, reason, consent_revision, changed_by
app.uninstalledThe install is revokedreason (uninstalled or app_archived), revoked_by

The body is {"id", "type", "app_id", "install_id", "tenant_id", "created_at", "data"}; it never carries a token or a secret. Ivora-Signature: t=<unix>,v1=<hex> is the HMAC-SHA256 of <t>.<body> with the app's webhook secret, as for project webhooks; Ivora-Event-Id and Ivora-App-Id repeat the event and the app. Answer with a 2xx. Deliveries retry with backoff for about a day and are at least once: deduplicate by event id. Ivora calls public https addresses only and never follows redirects. Each attempt goes to the app's webhook_url and is signed with its webhook secret as they are at that moment: after you change the URL or replace the secret, events still pending go to the new URL with the new signature. Delivered events are listed for 30 days.

  • The webhook secret (whsec_...) is returned once, with the client secret, when the app is registered. POST /v1/teams/{team_id}/apps/{app_id}/webhook-secret replaces it (and issues the first one for an app registered before lifecycle webhooks). Rotating the client secret leaves it alone. For a first-party app only Ivora administrators replace it (403 app_first_party_staff_only).
  • An app without a webhook_url, or without a webhook secret, receives nothing.
  • GET /v1/teams/{team_id}/apps/{app_id}/deliveries?after=<sequence>&limit= lists the events with their delivery status (pending, delivered, failed), attempts, last_status and last_error. A developer limited to some projects reads their projects' events only, and only while the app is active in no project outside them.
  • Changing the app's ui_origins, scopes or extensions raises its revision; installs consented to an earlier revision keep working but are not told. Narrowing the scopes is reported as app.scopes_changed per install.

App tokens

An installed app acts on its project with a token Ivora signs (ES256). Verify one with the public keys at GET /v1/apps/jwks.json (no credential; cache for a few minutes, refetch on an unknown kid). Its issuer is the API origin and its audience is the app's id.

TokenHowLifetimeWho it acts as
Session tokenPOST /v1/apps/session-tokens with {"install_id": ...}, by a signed-in project member (the dashboard does this for the app's UI)5 minutesthe member
Access tokenPOST /v1/oauth/token, grant_type=urn:ietf:params:oauth:grant-type:token-exchange with the session token as subject_token10 minutesthe same member
Client credentialsPOST /v1/oauth/token, grant_type=client_credentials with install_id10 minutesthe install itself, no user

The token endpoint takes application/x-www-form-urlencoded. The app's backend authenticates with client_id (the app's id) and client_secret, in the body or by HTTP Basic. A scope parameter narrows the token. Errors use the RFC 6749 body (error, error_description): invalid_client (401), invalid_grant, invalid_scope, invalid_request, unsupported_grant_type.

Scopes are the install's consented scopes, within what the app still requests. For a token that names a member they are also within that member's current team role in the project, so a viewer's session token reads and nothing more. An Ivora staff role is never part of it: a platform administrator who is not a member of the project's team gets 403 from the session endpoint.

Every request re-reads the install and the member's role, so uninstalling, archiving the app, narrowing its scopes or removing the member takes effect on the next call, and a token never outlives them. An app token is refused on /v1/admin, API-key, member, app-registry and payment-method routes, and cannot ask for another token. A session token stays valid only while you can trust the UI that holds it: when an update adds a UI origin, installs consented earlier get 409 consent_outdated from the session endpoint until an owner, admin or developer consents again (PATCH the install). The app's backend tokens are not affected, and removing an origin needs no new consent.

CORS reads the app's current registered origins (cached for 15 seconds), not the install's consent. An origin you add, but that an install has not yet consented to again, can therefore already call /v1 with the app tokens that exist until those tokens expire (5 minutes for a session token, 10 for an access token). No new session token is issued for that install until it consents.

Minted tokens are written to the audit log, one entry per install, kind (app.token.session, app.token.exchange, app.token.client_credentials) and hour, with the number minted. The actor is app:<install_id>. A request made with an app token that names a user is recorded as app:<install_id>/user:<user_id>, which the audit actor filter accepts.

Each install has its own request budget (rate_limit_per_minute, 120 by default): every token of the install counts in it, whoever holds them. Browsers may call /v1 from the app's registered UI origins with that app's tokens (CORS); no cookies or credentials are ever allowed, and no other origin is admitted. Never put an API key or the client secret in browser code.

An app that creates payments for anonymous drivers from its server, such as a driver checkout, can have each driver's IP address count in the per-address payment limit (rate limits). Ask Ivora to mark the app checkout_service: only Ivora administrators set it (PATCH the app with {"checkout_service": true}; 403 for anyone else, 422 for an app that does not request billing:write), every change is recorded in the audit log as app.checkout_service, and the app's record shows it. The app's backend then sends X-Ivora-Driver-Address on POST /payment/{service}/v1/tenants/{tenant_id}/payments with its client-credentials token for that project's install, which must hold billing:write. The header is ignored, and logged, on session and exchanged tokens (they act for a dashboard member, not for drivers) and on any app not marked. A change takes effect within a minute.

Operators set IVORA_APP_TOKEN_KEY (or _FILE), an EC P-256 private key in PEM form. To rotate it, set IVORA_APP_TOKEN_KEY_PREVIOUS to the old key for 10 minutes. Without a key a shared ledger answers 503.

Errors

CodeStatusMeaning
app_scope_forbidden422A scope an app cannot hold
app_origin_outside_apps_domain422A UI origin that is not https://<label>.<apps domain> of this environment
app_origin_refused422A UI origin on an Ivora domain
app_limit409The team registers 50 apps already
app_archived409The app is archived
app_already_installed409Already installed in this project
consent_outdated409The app changed after the consent screen
install_revoked409The install was uninstalled or its app archived