Integrations
Integration modules (e.g. SAP/ERP connectors) are enabled per org and gated by scoped permission grants. Base path: /api/org/{slug}/integrations (authenticated org member). Routes marked Owner/Admin refuse other members with 403.
Enable / disconnect a module
Section titled “Enable / disconnect a module”| Method · Route | Purpose |
|---|---|
GET /enabled | List enabled modules: [{ moduleKey, enabledAt, enabledByEmail }] |
POST /{moduleKey}/enable | Enable a module (Owner/Admin, passkey seal). Body: { assertionResponseJson }. Returns { moduleKey, enabledAt, enabledByEmail }; enabling a module that is already on returns the existing row. |
DELETE /{moduleKey}/enable | Disconnect a module (Owner/Admin, passkey seal). Body: { assertionResponseJson }. 204 on success, 404 when the module is not enabled. |
The seal is a passkey assertion bound to this organization, this module and the direction (enable or disconnect), so a seal made for one cannot be replayed for the other. A missing assertion answers 400; one that fails verification answers 401 with the reason in error.
| Status | error | When |
|---|---|---|
| 402 | ADDON_REQUIRED | The connector is a paid add-on the organization does not own. addon names the add-on key. |
| 409 | ROOST_DERIVED | roost turns on by itself once a box is bound to the organization; it cannot be enabled or disconnected here. |
Paid connector add-ons are bought through the one add-on door, POST /api/org/{slug}/settings/addons/{addonKey}. There is no separate per-module purchase route.
The same two routes install and remove a marketplace app, with moduleKey = app:{groupKey} — see Selling on the marketplace for the codes specific to apps (APP_NOT_ON_MARKETPLACE, OWN_APP, PAYMENT_REQUIRED, 202 with endsAt on uninstall).
Reading what a connector sends (setup wizard)
Section titled “Reading what a connector sends (setup wizard)”| Method · Route | Purpose |
|---|---|
GET /verified-connectors | { verified } — the connectors that have delivered a signed event (the “connected” mark) |
GET /{moduleKey}/connection-status | { verified, eventCount, lastEventAt } |
GET /{moduleKey}/inbound-preview?orgPath=&userPath=&emailPath= | Values found at those paths in the most recent inbound payloads (up to 300 events): { eventCount, org: [{ value, count }], users: [{ id, email, count, match }] }, where match is existing, new or unknown. Needs org.inbound_event_log.read. |
GET /{moduleKey}/fields | The field paths seen in recent payloads, for mapping without typing |
GET /{moduleKey}/hierarchy-preview?levels= | Org paths built from the given comma-separated level fields: { eventCount, levels, paths: [{ path, count }] } |
GET /{moduleKey}/org-structure | The connector’s org tree: { available, source, discoveredFrom, receivedAt, nodes: [{ id, parentId, name, kind, memberCount }] } |
GET /{moduleKey}/org-structure/members?unitId= | Members of one unit: [{ id, name, email }]. Needs org.inbound_event_log.read. |
POST /{moduleKey}/org-structure/materialize | Create the whole tree as Axowl units in one call (Owner/Admin). Body: { renames? } (unit id → new name). Returns { created, skipped, total, tooLarge }; 400 when the connector has sent no org structure. |
GET /{moduleKey}/event-types | Event types actually received: [{ type, count }] |
GET /{moduleKey}/notarize-scopes | { scopes } — the inbound events that are sealed into the audit chain (separate from RBAC). Owner/Admin. |
POST /{moduleKey}/notarize-scopes | Add scopes to that list. Body: { scopes: [...] }. Returns { added, reactivated, already, total }. Owner/Admin. |
Inbound data itself arrives at POST /api/org/{slug}/inbound/{system}. Org structure is never auto-synced; the tree is materialized only on an explicit call.
Access requests
Section titled “Access requests”Some integrations are opened by Axowl on request. An organization asks; Axowl reviews the request.
| Method · Route | Purpose |
|---|---|
POST /{moduleKey}/request | Ask for access. Needs org.integration_access_request.create. Returns { id, status }; if a request for this module is already Pending or Approved, that one is returned and no new request is created. |
GET /{moduleKey}/request | The latest request for this module: { status, defaultRoleId, reviewedAt }. status is None, Pending, Approved or Denied. |
Permission grants
Section titled “Permission grants”A grant creates (or reuses) one role per connector group and adds the scopes to it.
| Method · Route | Purpose |
|---|---|
POST /{moduleKey}/permissions/analyze | Check a set of scopes for conflicts before granting (Owner/Admin). Body: { groupName, scopes, isAllGroups }. Returns { ok, error, conflicts: [{ severity, title, detail, scopes }] }. 502 when the analysis could not be run. |
POST /{moduleKey}/permissions/grant | Grant scopes to a group (Owner/Admin, passkey seal bound to this organization, module and group). Body: { groupName, scopes, riskLevel, webhookUrl?, assertionResponseJson }. Returns { ok, error, roleId, addedScopes, failedScopes: [{ scope, error }] } — a scope that fails does not stop the others, and ok is false when any failed. A scope fails when it is malformed, is outside the integration’s namespace, carries uses, ttl or exp, or covers an Owner-only scope the caller does not hold. 400 without scopes or assertion, 401 when the seal fails. |
POST /{moduleKey}/ai/translate-scope | Turn a plain-language description into scopes (Owner/Admin, Pro plan or higher). Body: { prompt } (up to 2,000 characters). Returns { ok, error, scopes, notes: [{ scope, reason }], recommendedRiskLevel }. 403 UPGRADE_REQUIRED below Pro, 502 when the translation could not be run. |
Granted permissions flow through the same RBAC model — see Permission scopes.