Skip to content

Changelog

What changed on the surface you integrate against: the public API, token format, standards support, and SDKs. Internal dashboard work is not listed here unless it changes something you call.

Dates are the date the change went live on the development environment (testapi.axowl.com).


Schema changes go to staging first. A database connection now declares what it is — PATCH /api/org/{slug}/app-groups/{groupId}/data-sources/{id} takes purpose (production or staging), and the console’s Database tab shows it. axowl_request_migration takes target, and defaults to staging when the app group has one. Before applying to production, Axowl checks that the exact same SQL already ran on staging (by hash, in axowl_applied_migration) and refuses if it did not — a stamp does not override that. If staging cannot be reached, the change still applies and the receipt says the check could not be made.

Guards: the primary connection cannot be marked staging, and an app group has at most one staging database. Discarding a story’s work (Redo / Back to backlog) does not wipe staging — Axowl notes on the card what that attempt left behind, and reversing it is its own approval.

A backend staging workflow template. backend-staging builds a .NET backend and pushes it to the repository’s own GitHub Packages as a staging image — no extra secrets (it uses GITHUB_TOKEN), one staging deployment that stories take turns using. Set the repository variable STAGING_URL to the staging address. The deploy job never runs the repository’s own Dockerfile.


Deploys go out on the customer’s own Cloudflare account. A person connects it per client — GET/POST/DELETE /api/org/{slug}/applications/{appId}/cloudflare (account id + a Pages:Edit API token, checked against Cloudflare before it is saved, stored encrypted, never returned by any endpoint). Until that connection exists, publishing, project verification and project creation are refused: Axowl has no fallback to its own Cloudflare account, and the deploy service no longer reads a server token at all.

Agents can ask for the Pages project itself with axowl_request_pages_project(projectName, reason) — an approval (pages.project.create) carrying the name only. Which account it is created on comes from the client’s connection, never from the agent or the approval document. The console’s repository tab now shows both cards: Cloudflare connection, then deployment target.


A story in testing carries the address of the thing it built. When a story moves to Testing, Axowl reads the GitHub Deployment your deploy workflow made for that branch (environment: preview) and attaches its address to the card, so the person testing opens the running result instead of a diff. Agents cannot write it — axowl_pm_link refuses a preview kind — because the point of the address is that it is the thing that was actually deployed. Repositories without an Axowl deploy workflow are unaffected and are tested from the pull request.

Previews do not outlive their story: Redo, Back to backlog and a completed merge delete the agent branch (axowl/agent/…), which ends its branch deployment. Request revision does not — that story continues on the same branch and pull request. See One story at a time.


Projects have a documents tab, and agents can read it. GET/POST /api/org/{slug}/projects/{projectId}/docs, GET …/docs/{fileId}/download, DELETE …/docs/{fileId} — files live in the file box’s project scope, gated by project assignment. Agents read them with axowl_pm_files / axowl_pm_file (text only, 512KB) and cannot write or delete. See Project documents.

Agents can ask for a schema change, and see which repository secrets are set. axowl_request_migration(sql, reason) files an approval carrying the SQL in full; once stamped, Axowl applies it to the app group’s database in one transaction and records it in axowl_applied_migration (approval op db.migration.apply). axowl_repo_secrets and GET …/applications/{appId}/repository/secrets report which secrets the deployment workflow needs and which are set — names only: values are entered by the customer in their own GitHub and never pass through Axowl. See Schema changes.

Agents can ask for a deployment workflow, and read why CI failed. axowl_request_workflow files an approval carrying the complete workflow file; once a person stamps it, Axowl commits it to the repository’s default branch (approval op workflow.install). Templates only — the approval carries a template id and its values, never YAML, and .github/ remains closed to agents. axowl_repo_ci_log returns the trailing lines of the failed job’s log for one pull request. See Deployment workflows.

Project stories go one at a time: write, test, deploy, done. Story state gains Deploying (between InReview and Done; the board shows InReview as Testing). Two new routes: POST /api/org/{slug}/projects/{projectId}/stories/{storyId}/start opens a ToDo story for its agent, and POST …/stories/{storyId}/review takes decision (pass · revise · redo · hold) and note. Agents move a story to InReview only with a testGuide, an attached pull request and green CI; they can no longer move stories to Done, which now follows the merge. agent.wake gains the reasons story_start, story_passed, story_revise, story_redo, story_hold and setup_done. axowl_pm_create_story takes forPerson for setup a person must do. See One story at a time.

M2M tokens can outlive 5 minutes — per key, for devices. POST /api/m2m/token accepts lifetimeSeconds; the issued lifetime is min(request, key ceiling, 7 days) and expiresIn reports the real value. The ceiling is set per key at issue time (maxTokenLifetimeSeconds on POST /api/org/{slug}/settings/keys/api-keys, or the new lifetime picker in Settings → Keys). Anything above the default requires the organization owner and their personal seal; omitting lifetimeSeconds still yields 300 on every key, so existing integrations are unchanged. Revoking a key keeps refusing its tokens for the key’s whole ceiling. See Longer-lived tokens for devices.

Strix credit gate accepts a Korean recurring-billing card. POST …/data-sources/strix, POST …/data-sources/{id}/reconnect and POST …/data-sources/{id}/tier no longer return 402 CREDIT_REQUIRED when the paying organization has an active Korean billing card registered (POST /api/org/{slug}/billing/krw/billing-key/issue → confirm). A registered card counts the same as auto top-up: month-end usage is charged to it. The 402 body is unchanged (code, balance, required, reserve). See Database management.

Korean Package: AlimTalk from your own KakaoTalk channel. An app group on an organization with the Korean Package can link its own Kakao business channel; verification codes for Korean phone numbers then go out as AlimTalk under that channel, with SMS as the automatic fallback. Setup is in the app group’s new Korean Package tab, and the underlying routes are documented in KakaoTalk AlimTalk from your own channel.

  • GET | PUT /api/org/{slug}/app-groups/{groupId}/kakao, POST …/kakao/member, POST …/kakao/popup/{channel|template|sender} are new (dashboard session, app-group scopes).
  • GET /api/org/{slug}/billing/business-lookup?number= is new: verifies a Korean business registration number (checksum, then Popbill business-info lookup) and returns the company name, representative, address and business type for autofill. 30 lookups per organization per day.
  • The app group’s Korean Package tab has a two-step setup wizard (channel → template). Business verification and the Popbill link moved to the organization-level Korean Package hub (2026-09-07), under Applications → Overview, which is also where the package is turned on or off and where each app’s Korean sign-in providers and settlement currency are switched.
  • GET /api/org/{slug}/korean-package (2026-09-07) is new: one read of the package state, the organization’s business registration and Popbill link, and every app group’s Korean switches. POST …/korean-package/popbill-member links the organization with Popbill (billing scope).
  • GET /api/org/{slug}/billing/address-search?q= is new (2026-09-07): Korean road-name address search via the Ministry of the Interior and Safety API, server-side, 300 per organization per day. The wizard’s business-address field is now search → pick → detail.
  • Audit: appgroup.kakao.channel_linked / channel_changed / channel_unlinked / member_joined.

Board quorum now counts who attended, not who was invited. Creating a board meeting puts every director on the participant list automatically, and quorum was counting that list — so “present” meant “on the board”, one absent director left an unfillable gap, and items that a real board had carried were recorded as NotVoted. Quorum is now the directors marked present.

  • POST /api/org/{slug}/meetings/{meetingId}/participants/{participantId}/attendance is new. Body { present: true } (send false to undo). The organizer or the chairman may call it; it emits business.meeting.attendance_marked. Participant payloads already carried checkedInAt — it is now the value quorum is computed from.
  • Casting a vote marks that director present automatically, so an existing meeting keeps the attendance its votes already prove. Votes cast before this change were backfilled the same way.
  • POST /{meetingId}/agenda-votes now requires the director to be a participant of that meeting — 403 { error: "NOT_A_PARTICIPANT" }. Only the votes of currently seated, present directors are counted, so a vote left behind by a removed director no longer inflates a tally.
  • close-preview and complete report the same numbers as before; what changed is the definition behind attendingCount.

A meeting can no longer end up with nobody able to close it. POST /api/org/{slug}/meetings always gives the organizer a participant row now (previously only if they listed themselves), and the organizer-only actions — compile, close-preview, complete, delete — also accept the meeting’s own OrganizerId, which unblocks meetings already in that state.

The agenda is no longer frozen at creation. Items could only be created with the meeting, so a typo could not be fixed and an item raised in the room had nowhere to go.

  • POST /api/org/{slug}/meetings/{meetingId}/agenda-items adds one. Body takes title and an optional memo. It is numbered after the highest number the meeting has ever used, deleted ones included — numbers never move, because votes point at them.
  • PUT .../agenda-items/{index} now also accepts Title, and DELETE .../agenda-items/{index} drops an item (soft).
  • Both refuse an item that already has votes — 400 { error: "AGENDA_HAS_VOTES" }. Each vote stores the wording it was cast on, so rewriting or removing the item would break what a director agreed to. Deleting leaves a gap in the numbering, and that gap is intentional: renumbering would move existing votes onto a different item. There is no reorder endpoint.
  • Any of the three rebuilds the meeting’s Agenda summary and emits business.meeting.updated.

A meeting that never happened can be called off. Nothing could set Cancelled, and delete only accepts Completed or Cancelled — so a meeting scheduled by mistake stayed on the list forever. POST /api/org/{slug}/meetings/{meetingId}/cancel takes { "reason": "..." } (required), sets Cancelled, and emits business.meeting.cancelled. It refuses a meeting that has votes — 400 { error: "MEETING_HAS_VOTES" }: a vote means the meeting sat and decided, which is a close, not a cancellation.

Follow-up meetings go through the same doors as any meeting. POST /{meetingId}/complete with CreateFollowUp used to build the meeting directly, bypassing the plan gate, the board add-on quota, and the board/chairman requirements, and emitting no business.meeting.created. It now runs the ordinary create path — the follow-up is a new meeting and counts as one. When a gate blocks it, the close still succeeds and the response carries followUpError instead of failing.


End-user MFA no longer reports a code as sent when it wasn’t. POST /apps/{key}/auth/mfa/enroll/send swallowed every SMS failure and returned { sent: true } regardless, and a login that returned mode: "challenge" did the same — so a user whose code never left the carrier saw a code-entry screen forever, with no way back. Three changes:

  • POST /apps/{key}/auth/mfa/enroll/send now returns the failure: 502 { error } where error is invalid_number, region_not_supported, or send_failed (503 { error: "send_failed" } when no SMS provider is configured). Same error vocabulary as the organization OTP endpoints. 200 { sent: true } still means the message was accepted by the carrier.
  • A login that needs MFA returns two new fields alongside the existing { mfaRequired, mode, mfaToken }: smsDelivered (true/false, and null in enroll mode where nothing is sent yet) and smsError (the same error code, or null). Existing fields are unchanged — if you ignore the new ones, nothing about your integration changes.
  • POST /apps/{key}/auth/mfa/challenge/resend is new. Body { mfaToken }. It issues a fresh code to the phone already on the session and returns { sent: true }, or the same error shape as above. Use it when smsDelivered is false, or when the user asks to resend. One send per 60 seconds per session — 429 { error: "cooldown" } in between; a failed send does not start the cooldown. mfaToken is only issued after the first factor has already passed, so this endpoint cannot be used to send messages to an arbitrary number.

Projects can be deleted without archiving first, and a roadmap is one call instead of N.

  • DELETE /api/org/{slug}/projects/{id} takes a new ?purge=true. Without it the route behaves as before — it discards an empty shell (Planning, no stories, no epics) and refuses anything else with 409 PROJECT_NOT_EMPTY_SHELL. With it, the project’s rows in the axowl_pm_* tables of your database are deleted too (seven tables, children first, one transaction) and the response reports { discarded, purged, purgedRows, purgedFiles }. It fails closed: if your database cannot be reached nothing is deleted at all (409 PM_DATASOURCE_UNREACHABLE), because removing our row while yours survives leaves orphaned stories in your database forever. Attachment files are not erased — the ledger rows are soft-deleted and the objects go to the retention worker, as everywhere else in Axowl. An archived project is refused (409 PROJECT_ARCHIVED); restore it first.
  • ?purge=true now requires a personal seal and the project’s name. The scope (org.project.create) is unchanged, but on its own it made whoever may start a project also the person who may burn one — a weaker door than archiving, which keeps a copy. The purge branch now takes a body: confirmName must match the project name exactly (400 NAME_MISMATCH) and assertionResponseJson must be a passkey assertion bound to that project and the purge action (401 SEAL_REQUIRED), the same ceremony as deleting a data source for good. The empty-shell branch is unchanged — no body, no seal, because there is no work to lose.
  • GET /api/org/{slug}/projects/roadmap is new: every visible project with its epics, story counts and a server-computed progress, in one response. Epics are read once per app group rather than once per project. Archived projects are excluded, scoped guests get only their invited projects, and a database that cannot be read fails per project — that row carries an error (PM_DATASOURCE_REQUIRED / PM_SCHEMA_FAILED / PM_DATASOURCE_UNREACHABLE) and an empty epics array while the rest still return theirs.

See Projects.

A suspended workspace now says why — by email, to its owners. When Axowl suspends a workspace, every sign-in through your apps returns 403 with errorCode: "ORG_SUSPENDED", unchanged. What is new is that the owners of that workspace now receive an email at the moment of the suspension naming the reason (unpaid balance, terms of service, a security investigation, or their own request) and what clears it. Until now nothing was sent, so the first sign of a suspension was your users failing to log in. When the suspension is for an unpaid balance and the bill is paid by another organization, that payer’s owners are written to as well — they are the ones who can settle it. The 403 body itself is still the generic notice: reasons go to the workspace’s owners, never to the end users of your apps.

Two more letters follow the same rule. If we correct the reason while a workspace is still suspended, the owners get a second email marked as an update to the first — the suspension itself is untouched, so how long it has been suspended does not reset. And when the suspension is lifted, the owners are told that too, so nobody is left assuming they are still locked out. Sign-ins may still be refused for up to five minutes after a workspace comes back: the entry gate caches its decision, and the email says so.

Board meetings run off an agenda, and each item is voted on separately. /api/org/{slug}/meetings gained the agenda surface: GET|PUT /{id}/agenda-items/{index}, POST /{index}/open and /close to bracket when voting is possible, PUT /{index}/recording, GET|POST /{id}/agenda-votes, GET /{id}/close-preview, POST /{id}/complete and POST /{id}/minutes/compile. A vote is one director, one vote — not weighted by shareholding — and it is confirmed with the director’s personal seal bound to that item. An item resolves to one of four values, and NoQuorum is not Rejected: a resolution that never became capable of being taken is a different fact from one the board turned down.

Board meetings are now Pro-or-higher, with the first one free per organization. From the second, POST /api/org/{slug}/meetings answers 402 with an ADDON_REQUIRED body naming the add-on key addon.board_meeting plus freePerOrg, everCreated and priceUsd. It is deliberately not UPGRADE_REQUIRED — raising the plan does not open this door, so the error names the add-on that does. The count is per organization for its lifetime and deleting a meeting does not give it back. The corporation / SPV entity gate still sits in front: an entity without a board has no board meetings at any price.

See Meetings.

Project Management AI (addon.pm_ai) — the AI plans a project and then works it. Priced per connected app group, and it gates only the AI layer; the manual boards, epics and backlog stay on the plan axis.

  • POST /api/org/{slug}/projects/ai/draft turns a summary plus reference images (uploads and/or Figma frames) into proposed epics, stories with sub-tasks, and a roadmap layout. It returns 402 PM_AI_REQUIRED with a connectPath when the app group is not connected. The draft is a proposal only — nothing is stored until a human confirms, and the wizard then writes it through the ordinary POST /projects, /epics and /stories routes, so every gate on those still applies.
  • Six axowl_pm_* MCP tools let an assigned agent read and move its own stories. They carry no scope of their own: being assigned the story is what opens them.
  • agent.wake gained two reasons — story_assigned when a story gains an AI owner, and story_state_changed when an assigned story moves (except for whoever moved it). The webhook carries no story title or state; the agent reads what changed through its own tools.

See Projects and MCP.

Disconnecting a data source never deleted anything — now there is a way back, and a way to really delete. DELETE …/data-sources/{id} only flips isActive to false; the row, and for Strix the provider project (and its metering), stayed. But the row vanished from GET /, so it read as a deletion with no undo. Three changes on /api/org/{slug}/app-groups/{groupId}/data-sources:

  • GET /?includeInactive=true returns disconnected rows too; every row now carries isActive. The default (active only) is unchanged.
  • POST /{id}/reconnect restores a disconnected row in place — same record, so the history connected → disconnected → reconnected stays on one row. It re-tests the stored credentials and returns { reconnected, connected, error }. Emits appgroup.datasource.reconnected.
  • POST /{id}/delete deletes for good. Body { assertionResponseJson, confirmName }: a personal seal bound to this source and the delete action, and the connection’s exact name. Only a disconnected row is accepted (400 STILL_ACTIVE). For Strix the provider project is destroyed first — all data, no undo, no backup kept by Axowl — and the row is soft-deleted only after that succeeds (502 PROVIDER_DELETE_FAILED keeps the row for a retry). External connections lose only Axowl’s record. Emits appgroup.datasource.deleted with pd = provider delete confirmed.

See Database management.

Strix credit gate and non-payment enforcement. POST …/data-sources/strix and POST …/data-sources/{id}/reconnect (managed rows) now return 402 { code: "CREDIT_REQUIRED", balance, required } unless the paying organization holds at least $5 of Axowl Credit or has auto top-up on. When a monthly charge stays unpaid past the 3-day grace period, the organization’s Strix databases are suspended (disconnected, password rotated — data kept) and deleted 30 days later unless reconnected; owners and each database’s creator are warned by notification and email at suspension, 7 days before, and 1 day before deletion. Details in Database management → Credit gate and non-payment.


MCP — agents file approvals. Four tools on /mcp: axowl_approval_forms, axowl_request_permission, axowl_approval_submit, axowl_approval_status. An agent refused a scope can ask for it: the Access Approval form that grants it is submitted under the agent’s badge, a person stamps it in the dashboard, and the scope lands on the badge. Agents never stamp, and a machine requester is never auto-approved — see MCP server.

Hiring an agent gives it a manager. POST …/agents now adds a reporting edge from the badge to the person who hired it (existing agents were back-filled), and brings the shared AGENT_{KIND} role up to the kind template on every hire — previously a role created before a template change kept the old scope set.

Every agent kind carries the employee scopes (org.work_item.list/read, org.reporting_instance.create/read/list). test included.

Fixed: a dashboard-issued MCP credential (ah_live_…) was refused as credential not recognised — hash case mismatch between issue and resolve. Fixed: editing a form in the dashboard cleared its linked role, turning an Access Approval into one that granted nothing; PUT /approvals/work-items/{id} now leaves linkedRoleId untouched when omitted.


POST /api/org/{slug}/app-groups/{groupId}/data-sources/{id}/credentials — read back a connection’s credentials. Provisioning a Strix database handed you a password exactly once, in the create response, and nothing could show it again — so a database you owned could become one you had no way to reach. This route returns the host, user, password and two ready-to-paste forms (connectionString for ADO.NET, connectionUri for psql and most ORMs). It works for external connections too.

It requires a personal seal, bound to this specific data source and to the reveal action, so a seal taken for one database cannot be replayed against another. Every successful call emits appgroup.datasource.credential_revealed — this is the only path by which a stored password leaves the system in plaintext, and the record exists so that question has an answer later.

POST …/data-sources/{id}/rotate-password — reissue a Strix password. Reissues the managed role’s password and stores the new one. Same seal requirement, bound to rotate rather than reveal. The previous connection string stops working immediately — anything using it has to be updated with the value this call returns. Managed databases only; external connections return NOT_MANAGED, because their credentials are not ours to change. Emits appgroup.datasource.password_rotated.

Strix usage is now readable — GET /api/org/{slug}/settings/usage and the data-source listing. Consumption has been metered and billed since 2026-08-05, but nothing exposed it, so the first time you learned what a database cost was the invoice. The org-level route now returns strixGbMonths, strixCuHours, the amounts they come to, and the published rates; the data-source listing carries the same figures per database, so “which one is costing this” has an answer.

Two things the response is careful about. strixMinimumDueUsd is the gap to the monthly minimum, not a surcharge — it falls to zero once usage passes the floor. And strixMetered / usageMetered distinguish “measured, and it is zero” from “not measured yet”: consumption is read from the provider every few hours, so a database created moments ago legitimately reads zero. Figures are the current month in progress, while billing settles the previous month.

PATCH …/data-sources/{id} — edit a connection instead of replacing it. Until now the only way to change a password or a host was to disconnect and register again, which broke the record of how long that connection had been in place. Sends only what changes; an absent or empty password means unchanged, not cleared. Connection fields are editable on external connections only — for Strix, host and user are ours to keep correct, so only name applies. Connection-affecting edits are tested before the response returns, and the call emits appgroup.datasource.updated carrying the names of the changed fields, never their values. See Database management.


GET /api/v1/admin/audit/behavior/rollup — visitor behavior aggregates. The behavior beacon shipped with somewhere to send events but nowhere to read them back. This route closes that: pages, dwell time, referrers, campaign sources and countries for an app group, from sealed daily rollups. Takes orgId, days (1–90) and an optional appGroupId.

There is no add-on gate — the full 90-day window is free. Behavior analytics is metered on collection, not on looking at it, which makes it a different axis from Audit Analytics and its 3-day preview clamp.

Two things the response says out loud rather than papering over. Visitor counts are named visitorsUpperBound / identifiedUsersUpperBound because rollup rows are keyed by page, referrer and country — one visitor reading three pages is counted once per page. Views and dwell time are exact. And days without a rollup are omitted rather than drawn as zero, with coveredDates versus requestedDates telling you how much of the window is real.

Each daily rollup emits a sealed anchor (audit.rollup.behavior_created) carrying the file’s SHA-256 and a hash of its source list. Individual page views are not sealed — at that volume it would be neither affordable nor useful — so the integrity claim is made once a day at the aggregate: this count was computed over rows that have not been altered.


You choose what a session clash does — prompt on the owner gate. Handing someone over with login_hint used to have exactly one behaviour when the browser held a different Axowl session: stop, explain the clash, and make them pick. That screen is a wall for the common case, because the hint came from your own session — you already know who they are. The gate now takes prompt, in OIDC’s grammar: omitted (or login) honours the hint — Axowl signs that browser out of the other account and opens the form as the hinted address, saying on the card which session it ended — and select_account keeps the old chooser, signing nothing out until the person picks. An unrecognised value is treated as the default: a hint is an explicit instruction, and dropping it because a parameter did not parse is the worse failure. Axowl.Sdk.Identity.Abstractions carries the type as OwnerGatePrompt with AxowlOwnerGate.BuildLoginUrl(...), so the parameter names live in one place. Existing links keep working; they move from “ask” to “honour the hint” unless you send select_account. See The account-owner gate.


Seal who acted — actor_connected_id on the seal API. A seal used to record what happened but had no place for who did it: the SDK authenticated with an org API key, so a customer’s action sealed by your backend left no actor on the anchor. SealRequestProto (and the REST transcoding shape) now takes an optional actor_connected_id plus an actor_is_employee claim; the .NET SDK exposes it as SealActor on SealNewAsync / ReSealAsync. The actor is always a ConnectedId — end customers hold badges too — the server verifies the badge belongs to your organization and that the employee claim matches its subject axis, and the claim joins the hashed input so it cannot be swapped after the fact. Requests without an actor are unchanged, byte-for-byte. This closes the identity half of billing your own customers on Axowl: identity from introspect, attribution in the seal. See Integrity SDK.


IEntitySealer — edit a sealed record without breaking its seal. Sealing used to be a one-shot call at creation: the SDK gave you SealAsync and left the chain bookkeeping to you, so a record could be sealed but never legitimately changed. Editing a field inside the sealed payload made the row read as tampered, and the only alternative was to keep the interesting fields out of the seal entirely. IEntitySealer closes that: implement ISealedRecord on the entity, call SealNewAsync on create and ReSealAsync after an edit, and each version claims the next chain position — so Axowl holds one anchor per version and the edit is recorded, rather than hidden. Registered automatically by AddAxowlIntegrityClient(...), along with a default Sha256Hasher. Note the chaining rule it enforces for you: position n links to the sealedHash the server returned at n − 1, not to your own recordHash. See Integrity SDK.

Unpaid subscriptions now downgrade instead of running on indefinitely. A failed plan charge used to produce one low-balance notification and nothing else, so a plan could stay active without ever being paid for. A shortfall now opens a 3-day grace period — owners get an in-app notification and an email naming the exact deadline — and the plan drops to Free if it is still unpaid when the grace ends. Topping up at any point clears the clock, and a later shortfall starts a fresh one. Free organizations have nothing to downgrade and Enterprise agreements are settled contractually, so both are exempt; add-ons are not disabled by a downgrade. Your data is never touched. See Invoices.


POST /api/org/{slug}/app-groups/{groupId}/data-sources/strix — provision a managed database. Creates a Strix (sealed Postgres) instance and registers it as a managed data source, sealed from the first row. Body: { name, region? }, where region falls back to the organization’s home region. Requires the Strix integration to be active. See Database management.

Data source listings now carry Region and ProviderRegion. Region is two axes — where the organization is homed, and where a given provider actually runs the instance — so a listing tells you both rather than implying they are the same. See Database management.


GET /api/v1/admin/audit/analytics and /analytics/rollup — aggregated audit analytics. Usage aggregates, plus sign-in outcomes and event volumes read from sealed daily rollups. Rollups are built once a day just after midnight UTC and backfilled from archived raw events, so coverage reaches as far back as your raw history — not just from the day you enabled it. Both routes work without the Audit Analytics add-on over a 3-day preview window: the server clamps days to 3 and says so with "addonActive": false, "freeWindowDays": 3. The add-on opens the full 90 days. See Audit.

POST /api/org/{slug}/endusers/bulk-import — up to 500 end users per call. Body { appGroupId, users: [{ email, displayName? }] }. Each row goes through the same creation path as a single create — created Pending with an org identity, granted app access, sent a Magic Link invite in the background. Returns per-row created / duplicate / invalid. See End users.

enduser.security.login_failed — new event. Emitted on every rejected end-user sign-in. If you subscribe to event patterns on an outbound webhook, this one is new traffic. See Events.

GET …/data-sources/{id}/coverage — integrity coverage for a connected database. Reports hashes and row counts for a database you registered read-only. Contents stay yours — the check reads counts and hashes, not values. See Database management.

Withdrawals now disclose the cost-recovery deduction. Withdrawing prepaid cash returns it as a refund to the original card payment, minus 2.9% + $0.30 — the processing fee the payment processor does not return on refunds, recovered at cost with no margin — plus a proportional clawback of any volume bonus that rode in with that top-up. Both appear as their own ledger lines. See Invoices.


Outbound webhooks — Axowl can now call you. Register HTTPS endpoints under /api/org/{slug}/webhook-endpoints, subscribe them to event patterns, and receive signed deliveries. Signatures follow Standard Webhooks, so an existing Stripe/Svix verification library works unchanged. See Webhooks.

POST /api/public/v1/orgs/owner-status — is this person an Axowl account owner? Answers no_owner_account / owner_account_exists plus the organizations they own. The e-mail is read from the Bearer end-user token, not the request body — a body parameter would let any ah_live_ holder probe which e-mails have accounts. See Owner gate.

Fixed — ready was always false on organizations. The flag that tells you an organization can actually sign was reading a column nothing ever wrote, so every organization looked unusable. It now derives from the owner’s real credential. See Public API.

Fixed — /openapi/v1.json returned 500. The machine-readable schema was unavailable because a value-type default broke schema export. The document now generates.


.NET SDK — IAxowlAuth.RequireSignInAsync(returnUrl). The “this page needs a signed-in user” primitive: redirects once, guards against redirect loops, and returns to where the user was. Previously every consumer hand-rolled this. See .NET auth SDK.


End-user MFA (SMS) for your app’s users. An app group can require a second factor for its end users: POST /apps/{key}/auth/mfa/enroll/send and /mfa/verify. Existing login routes are unchanged — a login that needs MFA returns { mfaRequired, mode, mfaToken } instead of a session, and you exchange the token after verifying.


Full OIDC / OAuth 2.0 provider. /authorize, /token, /userinfo, /end-session with Authorization Code + PKCE. A standard OIDC client can now use Axowl as its identity provider with Authority alone — no Axowl SDK. Implicit and hybrid flows are intentionally not supported. See OIDC.


End-user tokens are RS256, verified through JWKS. Tokens were HS256, which meant verifying one required holding an Axowl secret — that is not a service. Tokens are now signed per organization with RS256 and published at /api/public/orgs/{slug}/.well-known/jwks.json. No shared secret.

OIDC discovery. /api/public/orgs/{slug}/.well-known/openid-configuration, with the token iss changed to the per-org URL so Authority auto-wiring works. In ASP.NET Core the whole integration becomes o.Authority = "https://…/api/public/orgs/{slug}".

Per-application CORS origins are enforced. The dashboard’s Allowed Origins field previously saved but did nothing. Browser origins you register are now honored, so cross-origin token exchange works without an Axowl code change.


Earlier changes predate this changelog. If you need the history of a specific endpoint, the reference pages cite the source file and line for every claim — that is the authoritative record.