What's New
Release notes for the Amba API, SDKs, and CLI — new features, improvements, and breaking changes with migration notes.
Breaking changes normally follow semver and ship with migration notes here. Pin your SDK versions and check this page before upgrading.
2026-11-07
Collections: server-only writes and nullability alters
- New
write_policy: "server": only server code writes the collection (server keys, admin tokens, Amba functions). Every client write (insert, upsert, batch, update, bulk update,update_many, delete,delete_many, restore and each transaction op) returns403 SERVER_WRITE_ONLY, checked before column validation. Reads keep followingread_policy, soread_policy: "public"pluswrite_policy: "server"is a leaderboard every player reads and none can edit, with no row signing in app code. Set it on create, withset_policyonPATCH, or inamba.collections.json. See Server-only writes. - New alter ops
drop_not_nullandset_not_nullonPATCH /v1/admin/projects/:projectId/collections/:nameandamba_collections_alter.drop_not_nullis metadata-only and instant.set_not_nullrefuses with409 NULLS_PRESENTanddetails.null_countwhile any row holds NULL, and otherwise validates without an exclusive lock during the row scan. See drop_not_null and set_not_null. - Collections as code: a column the file declares
nullablethat isNOT NULLlive is now an applicabledrop_not_nulloperation, where it used to be a deploy-blocking difference. A column the file requires that is nullable live stays a reported difference; apply never addsNOT NULL.
React Native and Expo SDKs: secure random numbers work with no polyfill import
@layers/amba-react-native4.3.1 and@layers/amba-expo4.3.1 installcrypto.getRandomValuesthemselves.react-native-get-random-valuesis now a dependency of the SDK, andAmba.configure()loads it when the runtime has none, so a fresh Expo app, Expo Go, or a bare React Native app no longer needsimport 'react-native-get-random-values'on the first line of its entry file. Acrypto.getRandomValuesthat's already there (a newer engine, or the import your app already has) is used as it is, so existing apps keep working unchanged.- Expo Go includes the polyfill's native module, and Expo SDK 54+ builds link it on
their own. Bare React Native and Expo SDK 50 to 53 builds link only the packages in the
app's
package.json, so keepreact-native-get-random-valueslisted there. When no secure source is linked,Amba.configure()still throwsSecureRandomUnavailableError, and its message now names the install command and the rebuild step. See React Native crypto setup.
Currencies: auto-recharge now works
auto_recharge_amountandauto_recharge_interval_hours(andrecharge_cap) did nothing until now: no recharge ever ran. Recharge now applies on every balance read and write (balance reads, grants, spends, store and inventory purchases, referral rewards, and event-triggered grants), inside the same transaction, so balances always include it. Every whole interval since the last refill is credited at once, up to the recharge ceiling, as onerechargetransaction. See How auto-recharge works.- The recharge clock runs only while a balance is below its ceiling. A spend that takes a full balance below the ceiling starts it; a balance at or above the ceiling accrues nothing. Existing balances in recharge-configured currencies start their clock the first time they're read or written after this release, with no back-credit.
- Balance reads (
GET /v1/client/currencies,GET /v1/admin/projects/:projectId/currencies/balances/:userId) and grant/spend responses carrynext_recharge_at: when the next unit lands, ornullat the ceiling or without auto-recharge. - Store purchases, inventory purchases, referral rewards, and event-triggered grants now
credit a currency's
initial_balanceon a user's first touch, as admin grants and spends already did. A fresh user can spend the starting balance the client balance read shows them.
2026-11-06
Score leaderboards: best score per player, per game, per UTC window
- Leaderboards take a new metric,
high_score. Your server submits each score withPOST /v1/admin/projects/:projectId/leaderboards/:slug/scores{ app_user_id, score, key?, idempotency_key? }, and the board keeps each player's best score per window. The response carriesnew_personal_best,best_scoreand the player'srank. Concurrent submits are safe: the better score always wins. MCP:amba_leaderboards_submit_score. keyed: truelets one definition serve many boards, one perkey(for example a game id).sort: "asc"ranks the lowest score first, for time-based games. Ties go to whoever reached the score first.- Windows are UTC (
daily,weeklyfrom Sunday,monthly,all_time), and past windows stay readable. The client and admin/entriesreads, and the client/meread, accept?key=and?period_start=YYYY-MM-DD. The client entries response addsme, the signed-in player's own entry. See Score leaderboards. - SDKs:
leaderboards.getEntries(key, limit?, boardKey?, periodStart?)andleaderboards.getMyRank(key, boardKey?, periodStart?)in@layers/amba-web4.3.0,@layers/amba-node4.3.0 (entries/myRank),@layers/amba-react-native4.4.0, Swift 4.4.0 and Android 4.4.0. Existing calls are unchanged. - Creating or updating a leaderboard now validates
metric,period,sortandkeyedand answers400 VALIDATION_ERRORon a bad value (a duplicate name answers409 LEADERBOARD_EXISTS). Before, these surfaced as a500.
Collections as code: diff, apply and pull a schema file; function deploys check it
- Keep your collection schema in
amba.collections.json(the collection-create shape, one entry per collection) and compare it with the live project. See Collections as Code. amba collections difflists what the live project is missing and the differences apply leaves alone, and exits 1 when anything declared is missing.amba collections applycreates missing collections and adds missing columns, indexes, the shared flag and declared policies. It never drops anything and never changes a column type or nullability.amba collections pullwrites the live schema to the file.amba functions deploychecks the schema file before uploading and stops with the diff when a declared collection or column is missing.--apply-schemaapplies the additive changes first;--skip-schema-checkbypasses the check.- New endpoints
POST /v1/admin/projects/:projectId/collections/diffandPOST /v1/admin/projects/:projectId/collections/apply. MCP:amba_collections_diffandamba_collections_apply(dry-run plan first, thenconfirm: true). - CLI 4.2.0,
@layers/amba-mcp4.2.0.
Remote config: read a user's resolved values from server code
GET /v1/admin/projects/:projectId/config/resolve?app_user_id=<id>returns the config mapAmba.config.fetch()serves that user, with segment conditions, percentage rollouts, the kill switch andvalue_typecoercion applied by the same resolver the app route uses. An Amba function calls it withenv.AMBA_INTERNAL_TOKEN, so a value the server enforces (a daily reward cap, a limit, a price) can live in remote config with rollouts instead of in a separate settings collection.meta.matchessays per key whether the value came from the default or from a condition, with the condition's index,segment_idandpercentage. Omitapp_user_idfor the anonymous view; an unknownapp_user_idreturns404 USER_NOT_FOUND.- MCP:
amba_remote_configs_resolve({ project_id, app_user_id? }). See Reading config from server code.
Functions: webhook senders that authenticate with an Authorization header
- New per-deployment setting
forward_webhook_authorization(CLIamba functions deploy --public --forward-webhook-authorization, MCPamba_functions_deploy({ …, public: true, forward_webhook_authorization: true }), RESTmetadata.forward_webhook_authorizationonPOST /v1/admin/projects/:projectId/functions/deploy). A public function deployed with it receives the caller'sAuthorizationheader on calls that carry no API key, so senders whose only auth option is a fixed header value, such as RevenueCat, can authenticate without a secret in the URL. - Credentials Amba issued (API keys, developer and service-account tokens,
session and OAuth tokens) are still stripped on that path. The setting
requires
public: true(400 INVALID_FORWARD_WEBHOOK_AUTHORIZATIONotherwise) and travels with the deployment, so a rollback restores it. Walkthrough with RevenueCat: Senders that authenticate with an Authorization header.
Functions: secrets set before a deploy are live from its first request
- Every deploy and rollback now binds the function's current secrets
(function-scoped and project-wide) into the deployment itself. Before, a
deploy started without them and a background sync added them a moment
later, so a secret set before a function's first deploy could be missing
from the running code even though
amba secrets listsaidsynced. sync_statusis now truthful:syncedmeans the value is live in the running deployment. A secret set before its function exists reportspending(with a note that it is waiting for a deployment) until the deploy binds it. A deploy fails with503 SECRET_BIND_FAILED, before anything is uploaded, when a secret it must bind cannot be read.
Push: send one notification to specific users from server code
POST /v1/admin/projects/:projectId/push/sendsends one push to 1 to 500 app users and delivers it to every active device each user has registered, in the same request. It is built for events about particular users: a rival beat their score, a gift arrived, challenge results posted. Call it from an Amba function withenv.AMBA_INTERNAL_TOKEN, or from any backend with a server key.- The response reports each user as
sent,no_active_tokens,rate_limitedorfailed, with per-device counts and the first provider error for a failed user. - Each user receives at most 10 transactional pushes per hour. Set
rate_limit_per_user_per_hour(1 to 100) on a call to use another cap. - Pass an
idempotency_key: a retry of the same request returns the first response withreplayed: trueand sends nothing; the key with a different request answers409 IDEMPOTENCY_KEY_CONFLICT. Keys are remembered for 24 hours. - A missing or empty
app_user_ids, more than 500 ids, a reserveddatakey, or a title, body and data over 4096 bytes answers400with a code naming the problem. - Free projects: each user a call actually sends to counts as one push in the daily direct-push budget (100 per day).
- MCP:
amba_push_send_to_users. Guide: Transactional push.
Collections: row writes keep every value, and errors name the column
These changes apply to the client and admin row routes alike: insert, batch, upsert, update, bulk update, and transactions.
- A bare
true/falsein a JSON column is stored. Writing a boolean to ajsonorjsonbcolumn used to fail with500 CREATE_FAILED. It now stores a JSON boolean, the same way numbers, strings, objects and arrays are stored as themselves. datecolumns read back asYYYY-MM-DD. A date written as2026-11-06used to read back as2026-11-06T00:00:00.000Z. Every read (get, list, find, query) and the row a write returns now give2026-11-06. Timestamp columns are unchanged and stay ISO-8601. The same applies to the other date fields Amba returns, such as a streak'scurrent_period_startand a league cohort'sweek_starton the admin routes. If your code parsed a date column as a full timestamp, read it as a calendar date instead.- A caller-chosen
idis honored. An insert that carried a UUIDidused to store the row under a new server-generated id without saying so. The row is now stored under the id you send, on the client and admin routes, in batches, and in transaction inserts. A malformed id returns400 INVALID_ID; an id already in use returns409 CONFLICT. Leaveidout (or sendnull) and the server assigns one, as before. Ownership is still set by the server, so a chosen id never reaches another user's row. See choosing the row id. - Rejected values and unknown columns name the column. A value its column rejects
(
"abc"for an integer,"soon"for a date) returns400 INVALID_COLUMN_VALUEwithdetails.columnanddetails.type; several of these used to be500errors. A key that is not a column returns400 INVALID_COLUMNwith every unknown key indetails.unknown_columns, and the message no longer includes the internal table name. Column validation runs after the access check, so a write the caller may not make still returns its403. See writing values. - Text sent to a boolean column is read as Postgres reads it.
"true"or"yes"sent to a boolean column used to storefalse. It now storestrue, and text that is not a boolean returns400 INVALID_COLUMN_VALUE.
Currencies: live balance on every grant and spend, lookup by idempotency key, recharge_cap
POST /v1/admin/projects/:projectId/currencies/grantand/spendresponses carrycurrent_balance, the user's live balance read in the same request, and always carryidempotent_replay(falseon a first write). On an idempotent replaybalancestays the original transaction'sbalance_after, which later writes may have moved;current_balanceis where the balance stands now.- The replay contract is now documented: a reused
idempotency_keyreplays the original transaction at any amount, with HTTP 200, and never answers 409. This is unchanged behavior. See Idempotency. GET /v1/admin/projects/:projectId/currencies/transactions/:userIdacceptsidempotency_keyandcurrency_codefilters.?idempotency_key=…returns the transaction a grant or spend wrote under that key, or an empty list, so you can check whether a spend landed without spending again. MCP:amba_currencies_find_transaction.- Transaction history orders ties by insertion. Rows written together (a fresh user's
starting balance and their first spend) share a timestamp, and newest first could list
the starting balance first. Each row now carries an insertion sequence
seq, and the first row is always the latest write. - Currencies take an optional
recharge_cap: the ceiling auto-recharge refills to. Grants and purchases stay bounded bymax_balance, so "refill to 5, bought coins stack past 5" is one setting. Unset, recharge stops atmax_balanceas before. Set it on create or update (amba_currencies_create/amba_currencies_update). GET /v1/admin/projects/:projectId/currencies/balances/:userId(live per-currency balances) is now documented with its response shape.
2026-10-30
Events: batches are recorded in one round of writes
POST /v1/client/events/batchrecords the whole batch in one transaction. A batch of 100 events used to be written one event at a time and could take close to 20 seconds; it now takes about as long as a single event. Responses, ordering and idempotency are unchanged. A key that appears more than once in one batch is settled by its first occurrence: a repeat with the same payload is acknowledgedduplicate, and a repeat with a different payload is rejectedIDEMPOTENCY_KEY_CONFLICT. If writing the batch fails, none of it is recorded, and a retry with the same keys records it once.
2026-10-24
Auth: a refresh token can be presented again for 30 seconds
- A refresh token can be presented again within 30 seconds of its first use and returns a new pair, as long as the session it was exchanged for is still active. A lost refresh response, or two refreshes sent at once, now keep the user signed in. After those 30 seconds, or once that session has signed out or refreshed again, a used token returns
401 INVALID_TOKENas before, and a token sent toPOST /v1/client/auth/logoutnever refreshes again. See POST /client/auth/refresh.
Admin API: a malformed project id gets 404 PROJECT_NOT_FOUND
- A developer or delegated operator token calling a project's feature routes
(
/v1/admin/projects/:projectId/<feature>/...) with a project id that is not a UUID (for exampleplaceholderorundefined) gets the answer a project you don't manage gets: HTTP 404, codePROJECT_NOT_FOUND, messageProject not found. Until now it failed with a 500INTERNAL_ERROR. - On those routes that 404 now carries the code
PROJECT_NOT_FOUND. It used to carryHTTP_ERROR, with the same status and message. The project's own routes (/v1/admin/projects/:projectIdand its API keys) keep answering 404NOT_FOUND. - A server key or internal token still gets 403
WRONG_PROJECTfor any project id other than its own. - MCP:
amba_secrets_delete,amba_functions_deleteandamba_sites_remove_domainfail withPROJECT_NOT_FOUNDwhen the project id is wrong. They used to report the item as already deleted.
React Native and Expo: a lost signed-in session asks the user to sign in again
- A session that gained an identity (any sign-in other than
signInAnonymously(), or a link) and then ends without a sign-out asks the user to sign in again. That covers Amba rejecting or revoking its refresh token, a refresh that renews a different user than the stored one, and an anonymous id that now needs the account's linked sign-in. The SDK clears the dead tokens and starts no anonymous user:onAuthStateChangesubscribers hearnullwithevent.reason'sign_in_required', and calls that need a session fail with theSIGN_IN_REQUIREDerror code, across relaunches, until the user signs in, the app callsAmba.auth.signInAnonymously()(which starts a new anonymous user), or the app callsAmba.auth.signOut(). Until now the SDK started a new anonymous user here, and the person on the device landed in an empty account. - Action: if your users link an identity (email, phone, Apple, Google),
show your sign-in screen when
event.reason === 'sign_in_required'or a call fails withSIGN_IN_REQUIRED. See When a signed-in session ends. signOut()is a deliberate end and keeps its behaviour: subscribers hearnullwith'signed_out', and withautoAnonymousSessionon the next call that needs a session starts a new anonymous user with a new anonymous id. A purely anonymous session whose refresh token Amba rejects still signs the same anonymous user back in (reason'session_ended').onAuthStateChangecallbacks receive a second argument,{ reason }:'signed_in','token_refreshed','signed_out','session_restored','session_ended'or'sign_in_required'. One-argument callbacks keep working.- Session writes never overwrite a newer session: a sign-in that lands while
signOut()waits for the server keeps its session, the anonymous sign-in the SDK starts on its own gives way to a sign-in that landed first, and a secondAmba.configure()hands a refresh already in flight to the new configuration, so the refresh token is sent once. - Expo: both parts of the stored identity carry the sequence number of the
write that produced them, so tokens always come back with the user they
belong to, even after a write that failed halfway. While SecureStore can't
be read (an iOS device not yet unlocked),
Amba.configure()resolves, the stored session stays, and calls that need it fail withSECURE_STORE_UNAVAILABLEuntil the device unlocks. The session then loads on the next call orgetSession(), and subscribers hear it with'session_restored'.Amba.events.track()keeps resolving in that window: events wait in memory (up to 500) with their call times and are sent in order once the device unlocks. - Versions:
@layers/amba-react-native4.3.0,@layers/amba-expo4.3.0.
2026-10-23
SDKs: a refused session token is refreshed and the call sent again
- When Amba refuses the session token a call carried (
INVALID_SESSION_TOKEN: the token expired, or Amba rotated the key that signs session tokens), the Web, React, Node, iOS, Android, Flutter and Unity SDKs refresh the session with the refresh token and send the call once more. Calls refused at the same moment share one refresh, and a call is sent again at most once.onAuthStateChangehears the refreshed session. On Web and Node, the SDK's own requests recover the same way:events.explainis sent again, an AI stream refused before its first event reopens, and a realtime subscription reconnects with the new token. Until now every call failed withINVALID_SESSION_TOKENuntil the app calledauth.refresh(). - When Amba rejects the refresh token as well, the SDK signs the user out:
onAuthStateChangefires withnulland the call fails with itsINVALID_SESSION_TOKENerror. A network failure or server error during the refresh keeps the session.auth.refresh()waits for a refresh already in flight, so the single-use refresh token is never sent twice. - Versions:
@layers/amba-web4.2.2,@layers/amba-react4.2.2,@layers/amba-node4.2.2, iOS (Swift Package) 4.3.1, Android (com.layers.amba:amba-sdk-android) 4.3.1, Flutter (amba) 4.2.1, Unity (com.layers.amba) 4.2.1.
2026-10-22
Console and Amba QR renew your session before asking you to sign in
- Console. When the console's access token (24 hours) has run out or Amba refuses it, the console renews the session with your 30-day sign-in and returns you to the page you opened; a call a page makes in the background is renewed and sent again. You sign in again only when that 30-day sign-in has ended. Open tabs renew one at a time, so a renewal in one tab keeps every other tab signed in.
- Amba QR dashboard (
go.amba.host/me). The dashboard renews your sign-in the same way and then finishes the page or form you sent. A sign-in from before this change carries over on your next visit.
React Native and Expo: a launch-time session check renews a refused token
Amba.auth.me(),Amba.auth.getSession()andAmba.auth.getCurrentIdentifiers()renew the session when Amba refuses its access token (INVALID_SESSION_TOKEN,SESSION_REVOKED): the SDK refreshes it with the stored refresh token and sends the call once more, the same way every other authenticated call already did. A session check at launch now shows the signed-in user. When Amba rejects the refresh token too, the session ends once,onAuthStateChangesubscribers hearnull, andgetSession()resolvesnull.getSession(),me()andgetCurrentIdentifiers()called while the SDK is restoring a stored session at launch wait for that restore.- The link and unlink calls (
linkAccount,linkSmsOtp,linkEmailOtp,linkEmailPassword,unlink*) renew a refused session the same way and are sent once more with the renewed token. - Calls refused together share one refresh, and each call is sent again at most once.
- Versions:
@layers/amba-react-native4.2.2,@layers/amba-expo4.2.2.
2026-10-20
SDKs: every request has a deadline; offline replays are recorded once
- Timeouts now bound every request. A request still unfinished after
requestTimeoutMs(default 15 seconds, from sending it to reading the whole response) is aborted and fails with the retryableNETWORK_ERROR. A request the network never answered (on Expo for Android, a failed DNS lookup could leave one pending forever) used to hold the offline queue and the anonymous sign-in for the life of the app; the next flush, sign-in or call now proceeds. AI generation,functions.invoke()and media uploads and downloads wait up tolongRequestTimeoutMs(default 5 minutes), andfunctions.invoke(name, body, { timeoutMs })sets the deadline for one call. A request that moves more than 64 KiB gets one more second for every further 8 KiB. A realtime subscription that doesn't start streaming withinrequestTimeoutMsis retried; once it streams it stays open for as long as you subscribe. See Request deadlines. - Offline replays are deduplicated. A
trackreplayed from the offline queue carries its idempotency key, and Amba records each key once, so an event replayed any number of times is recorded once. On React Native the live attempt of an engagement event carries the same key, so an event whose request reached Amba but timed out is recorded once when it replays. A replay Amba refuses for good (an invalid event) leaves the queue so it can't block the writes behind it. - Versions:
@layers/amba-web4.2.1,@layers/amba-react4.2.1,@layers/amba-react-native4.2.1,@layers/amba-expo4.2.1,@layers/amba-node4.2.1.
Amba QR
- QR Pro, $10/month per account. Scans go straight to the destination,
stats keep their full history with country and device breakdowns, brand
shares are unlimited, and Pro adds CSV export and custom QR colours. Free
keeps unlimited codes, the disclosure page, 30 days of stats and one brand
share. QR Pro is a subscription of its own on your Amba account, billed
apart from project plans. Upgrade with
amba_qr_upgradeorPOST /v1/qr/billing/checkout(a checkout link for the account owner), read the plan withamba_qr_billing_statusorGET /v1/qr/billing, and change the card or cancel throughPOST /v1/qr/billing/portal. See Plans and pricing. - CSV export.
GET /v1/qr/:id/export.csv(Pro) downloads a code's daily stats by country and device OS. - Custom colours on the free plan now answer
402 QR_PRO_REQUIRED(was403), as CSV export does, withdetails.upgradenaming the upgrade call. A second active brand share on the free plan still answers403 QR_SHARE_LIMIT, now withdetails.upgradetoo. - Your own creator host. Every code prints
https://<handle>.go.amba.host/<slug>, and each code keeps the host it was printed with for good (hoston every code). Set the handle at sign-up (handleonPOST /v1/auth/creator/verify) or withamba_qr_handle_set(PUT /v1/qr/handle): 3 to 30 characters of a-z, 0-9 and single hyphens, with reserved words and brand names kept free. It can change until the first code and is permanent after. Creating a code without a handle answers409 QR_HANDLE_REQUIRED. A code answers only on your hosts; the same slug on another creator's host shows the "does not exist" page, andgo.amba.host/<slug>sends visitors on to your handle host. See Your handle and creator host. - Custom domain on Pro. Attach one domain you own (for example
qr.yourbrand.com) withamba_qr_domain_attach(POST /v1/qr/domain), publish the DNS records it lists, and new codes print on it once Amba has proven you own it (Amba checks every few minutes on its own). Codes printed on it keep working after Pro lapses, with the free disclosure page. See Custom domain.
MCP
@layers/amba-mcp4.1.18:amba_qr_billing_statusandamba_qr_upgrade.@layers/amba-mcp4.1.19:amba_qr_handle_get,amba_qr_handle_set,amba_qr_domain_attach,amba_qr_domain_get,amba_qr_domain_refreshandamba_qr_domain_remove;amba_qr_createprints on the creator host.
Amba QR: make a code in the browser at amba.dev/qr
- amba.dev/qr makes an Amba QR code from a link, the platform and an optional post label. Sign-in is a 6-digit code sent to your email (the creator sign-up flow); the page keeps the session in an httpOnly cookie and never shows the account's personal access token.
- The page shows the framed image the API renders. Copy image puts the PNG
on the clipboard (the short link, where a browser copies text only), Save
PNG and Save SVG download
amba-qr-<slug>, and on phones Share opens the share sheet, where "Save Image" adds it to Photos. Signed-in visitors see their 10 most recent codes with the same actions. - While creator signups are closed, the page says Amba QR is opening soon and offers to email you when it opens.
SDKs: remote config and flags on the first frame
configDefaultsat configure. Pass the remote config values your app ships with, keyed by config key:configure({ configDefaults: { ... } })on Web and React Native,configDefaults:on Swift, Dart and C#, and aconfigDefaultsoverload ofconfigureon Android.config.getCached(key, default)returns a config value with no request: the last fetch, else the value the device cached on an earlier launch, elseconfigDefaults, else the default you pass. A value of another type than your default returns your default.flags.isEnabled(name, default)andflags.getCached(name)read flags the same way;isEnabledreads a booleanconfigDefaultsentry under the flag's name. React addsuseConfigValue(key, default)anduseFlagEnabled(name, default), anduseFlag/useVariantserve the cached assignment while their fetch runs.- Device cache. The SDKs keep the last config and flags they fetched per
project and per signed-in user (
localStorageon the web, AsyncStorage on React Native, an app-private directory excluded from backups on iOS, Android, Flutter and Unity) and serve them on the next launch. A signed-in user reads only their own values; before sign-in the SDK reads the values fetched without a session. The first cached read for each user starts one background fetch that replaces them (after configure, and again after a sign-in or sign-out); configure itself sends no request. A key the server leaves out (a config switched off with its kill switch, a disabled flag) leaves the cache with the next fetch, so lookups fall back toconfigDefaultsand then your default. Sign-out deletes the signed-out user's cached values. See Startup values. - Fixed:
flags.get(name)returnsnullfor an unknown or disabled flag on Web, Node, iOS and Android (it rejected with aNOT_FOUNDerror), and returns the flag on Flutter and Unity (it returnednullfor every enabled flag). - Versions:
@layers/amba-web4.2.0,@layers/amba-react4.2.0,@layers/amba-react-native4.2.0,@layers/amba-expo4.2.0, iOS 4.3.0, Android 4.3.0, Flutter 4.2.0, Unity 4.2.0.
Fixed
- A canceled subscription stays canceled. Billing updates for a project's subscription now apply in the order they happened, whatever order they arrive in. An update from before a cancellation that arrives after it never restores the paid tier, even when the cancellation is the first event for that subscription to arrive. A new subscription (a fresh checkout) puts the project back on its paid tier. See Billing.
- Event idempotency keys are kept for 90 days. An event sent with an
idempotency_key(POST /v1/client/events/batch, orPOST /v1/client/eventswith a key) and replayed within 90 days of its first delivery is acknowledged as a duplicate and recorded once. Keys older than that are cleared as your project receives keyed events, so a replay arriving after 90 days may be recorded as a new event. See Events.
2026-10-19
Remote config
- Kill switch.
PATCH /v1/admin/projects/:projectId/config/:keytakesis_active.falsestops serving the key:GET /v1/client/configleaves it out, so the SDK returns the app's cached or shipped default, and the key's value, conditions and history are kept.trueserves it again. The admin list now shows every key with itsis_active, and a scheduled content delivery into a switched-off key leaves it off. From an agent:amba_remote_configs_set_active({ project_id, key, active }). See Kill switch. - Change history. Every change to a remote config key is recorded with
who made it (
actor_kindpat,server_key,operatororsystem) and before / after snapshots:GET .../config/historyfor the project,GET .../config/:key/historyfor one key, andamba_remote_configs_history. Segment definitions get the same:GET .../segments/history,GET .../segments/:segmentId/historyandamba_segments_history. See Change history. - A project import now moves the config
ETag, so clients holding a cached copy see imported values on their next fetch.
Segments
- Built-in device fields. Segment rules can target
platform(the SDK in use),os(ios,android,web,macos,windows,linux,other),app_versionandcountry(the device locale's region, such asUS). The SDKs report them on requests from signed-in users through theX-SDK-Platform,X-Device-OS,X-App-VersionandX-Device-Regionheaders, and Amba writes them only when a value changes.app_versioncompares in semantic-version order, so{ "field": "app_version", "op": "gte", "value": "2.4" }matches2.10.0. See Built-in device fields. inoperator. A condition matches when the field equals any value in an array of up to 100:{ "field": "country", "op": "in", "value": ["US", "CA"] }.- An unknown operator, or an operator a built-in field does not support, now
answers
400 INVALID_SEGMENT_RULESwhen you create or update a segment.
MCP
@layers/amba-mcp4.1.15:amba_remote_configs_set_active,amba_remote_configs_historyandamba_segments_history;amba_remote_configs_updatetakesis_active; segment conditions takeinand the built-in device fields.
SDKs
- Experiments in every SDK.
experiments.getVariant(key)assigns the signed-in user to an experiment and returns{ experimentKey, variant, bucketingId }; the assignment is sticky, so every later call returns the same variant.bucketingIdis the user id Amba hashed to pick the variant.experiments.trackExposure(key)records that the user saw it. On Node, both are onamba.asUser(userId).experiments. React addsuseExperiment(key). See Experiments. onExperimentAssignedfor your analytics. The listener receives each user's assignment once per experiment per session (the firstgetVariantresult while it is subscribed; a sign-out starts a new session). Forward it to Layers as anexperiment_assignedevent withexperiment_key,variantandbucketing_id. An error in the listener is caught and logged and never reaches thegetVariantcaller.- Targeting headers. Requests carry
X-App-Version(the app's version from the native app info; on the web and React Native, theappVersionyou pass toconfigure; on Android, passcontexttoAmba.configure),X-Device-Region(the device locale's two-letter region) andX-Device-OS(ios,android,web,macos,windows,linuxorother), each read once at startup and left out when unknown. Segments target them as the built-inapp_version,countryandosfields (see Segments above). - Versions:
@layers/amba-core4.1.0,@layers/amba-web4.1.0,@layers/amba-node4.2.0,@layers/amba-react4.1.0,@layers/amba-react-native4.1.0,@layers/amba-expo4.1.0, Swift and Kotlin 4.2.0, Flutter 4.1.0, Unity 4.1.0.
New
- Amba QR. Trackable QR codes for creator posts. Each code gets a
permanent short link on
go.amba.host, and every scan is recorded, with bots and link previews left out. On the free plan a scan opens a short Amba disclosure page with one Continue button; on Pro it goes straight to the destination. Amba checks each destination when you create a code and every time you change it: http(s) only, no IP addresses oruser:password@part, no other link shorteners or open redirectors, noamba.hostaddresses, no international domain names that mix alphabets or imitate a brand, and a lookup against known malware and phishing sites. The account needs a proven email address (claim an agent-created account first). Free accounts create up to 50 codes and change destinations up to 20 times per 24 hours (429 QR_DAILY_CAP). Every code comes with daily stats, a framed SVG and PNG image and its full destination history. QR codes belong to your account and need no project. API:/v1/qr. MCP:amba_qr_create,amba_qr_list,amba_qr_get,amba_qr_update,amba_qr_delete,amba_qr_stats,amba_qr_image(@layers/amba-mcp4.1.16). See QR Codes. - Amba QR creators and brand shares. Creators sign up with an email code,
no password and no project (
POST /v1/auth/creator/start,/verify); a new account gets a session and its personal access token. A creator shares a read-only stats dashboard of chosen codes with a brand by email (amba_qr_share,amba_qr_shares_list,amba_qr_unshare; one active share on the free plan; shares open together with the brand dashboard page), and sets the name scanners see withamba_qr_profile_updateorPATCH /v1/qr/me. Amba re-checks active destinations daily (weekly for codes with no scans in 30 days) and turns off codes whose destination has since been listed.@layers/amba-mcp4.1.17. - Amba QR dashboards. Brands open a read-only dashboard at
go.amba.host/s/<token>with one card per shared code: scans, people who continued, unique visitors, a daily chart, top countries and devices (Pro) and the destination history, with 7-day, 30-day and all-time views. Creators manage everything atgo.amba.host/me: email-code sign-in, create and edit codes, stats, SVG and PNG downloads, turn a code off or on, and share with brands. See QR Codes.
Fixed
amba_deeplinks_deletedescribes the delete it performs. Deleting a tracked link deletes its click rows with it, so its click stats go too; the tool description now says so.
2026-10-16
Security
- Tighter daily project capacity for unproven accounts. Every open
signup still gets its PAT and its first project at once, through the API,
MCP (
amba_developer_signup), the CLI or the console. Accounts whose owner has not proven the inbox now share a smaller daily capacity slice, and each network and each company or custom email domain gets a few of those projects a day. When the slice is spent for a signup, the account is still created: signup returnsproject_unavailablein place ofproject, withdetails.reason: "inbox_proof_required"and the claim call, and the account gets its first project once the owner proves the inbox (the claim link or an emailed sign-in code). Then create the project withamba_projects_create, or rerunnpx @layers/amba init. See Daily project capacity for unproven accounts. - App agent surface: only data and function tools are metered. On your
app's MCP (
/app/<project_id>/mcp),<app>_whoamiand<app>_list_collectionsno longer count as agent tool calls. Each client key has a budget of 300 collection and function calls a minute and 50,000 a day there; past either budget the surface answers429 RATE_LIMIT_EXCEEDEDwithRetry-After. One request may carry at most 10tools/callmessages; a larger JSON-RPC batch is refused with a-32600error before anything runs. See Metering. - Rate limits on end-user sign-up.
POST /v1/client/auth/anonymousand/email/signupnow have a per-address budget on every request and a per-app budget on new users, and/linkhas per-address and per-user budgets. They bound how fast anyone holding your client key can add users to your app's MAU count. Past a budget the route answers429 RATE_LIMIT_EXCEEDEDwithRetry-After: anonymous sign-in 120 a minute and 1,200 an hour per address, and 300 new users a minute per app (3,000 on a paid plan); email signup 10 a minute and 100 an hour per address, and 60 new users a minute per app (600 paid); link 20 a minute and 200 an hour per address, and 10 a minute and 60 an hour per signed-in user. Only a new user spends the per-app budget, so resumes, taken addresses and malformed requests never use it up. Calls made with a server key skip the per-address budgets.
CLI and MCP
- Signup source.
AMBA_SIGNUP_SOURCE(oramba init --src <source>) and the optionalsignup_sourceargument onamba_developer_signuppass along the source an install snippet carries: at_…token from a snippet copied on amba.dev, or a short tag such asdocs. The hosted MCP server also reads it from a?src=parameter on its URL. It records which page an install came from and changes nothing about the account. CLI 4.1.32,@layers/amba-mcp4.1.14.
Fixed
- Events after a link carry the account's new anonymous id. Since
2026-10-15 a link that adds an identity gives the account a new
anonymous_id. The SDKs now adopt the id the link returns, so events tracked afterwards carry it (they carried the pre-link id). This covers every link in@layers/amba-web4.0.15 and@layers/amba-node4.1.7, whose writes queued offline before any session stay with the signed-in user and replay, andlinkAccountin the Swift and Kotlin SDKs 4.1.5, Flutter 4.0.10 and Unity 4.0.8. React Native and Expo have adopted it since 4.0.14 / 4.0.13. - SDK libraries carry no build-machine paths. The native libraries and the
WebAssembly build inside every SDK named source files with the path of the
machine that built them (its home directory and user name). In
@layers/amba-core4.0.15, Swift and Kotlin 4.1.5, Flutter 4.0.10 and Unity 4.0.8, those locations start at/cargo/for dependencies and at the package for the SDK's own code, and every release checks its libraries for a build-machine path before it ships. Unity's macOS plugin has the install name@rpath/libamba_core.dylib. - SDKs report the core's version. The SDKs built on core 4.0.15 send
amba-core/4.0.15inX-SDK-Versionand as each event'ssdk_version:@layers/amba-web4.0.15,@layers/amba-node4.1.7, Swift and Kotlin 4.1.5, Flutter 4.0.10 and Unity 4.0.8. Earlier builds sent4.0.2. - Linking documents the new anonymous id. The SDK reference for
linkSmsOtpandlinkEmailPassword, the authentication guide and Account linking say what the server does: a link that adds an identity gives the user a newanonymous_id, and anonymous sign-in with the old one answers401 IDENTITY_SIGN_IN_REQUIRED.
2026-10-15
Security
- Signing out ends the session everywhere. A developer access token now
belongs to the sign-in session that issued it and works only while that
session is live.
POST /v1/auth/developer/logoutends the session: its refresh token and every access token it issued stop working within 30 seconds.amba logoutnow calls it for the CLI's stored session, andamba loginthrough the console gets a session of its own, so signing out of one leaves the other signed in. Access tokens issued before this release stop working: the console asks you to sign in once more, and the CLI refreshes on its own. See Sessions. - Refresh is atomic and detects a copied refresh token. A refresh
replaces the refresh token in one step. A refresh token presented again
after it was replaced ends the whole session (
401 REFRESH_TOKEN_REUSED), so whoever copied it and the client that refreshed both sign in again. App users get the same one-step rotation: of twoPOST /v1/client/auth/refreshcalls with one refresh token, one returns a new pair and the other401 INVALID_TOKEN. - OAuth connections cannot create lasting access. A token from an app
connected through OAuth (Claude, Cursor and others connecting by URL) gets
403 OAUTH_TOKEN_CANNOT_MINT_CREDENTIALon server keys, service accounts, org and project invites, member role changes, ownership transfers,POST /v1/admin/provision, reading the project's internal credentials, deploying functions and claiming the account. Client keys still work, and a project created over OAuth comes with its client key only. See MCP install. - See and disconnect OAuth apps. The console's Connected apps page
lists every app connected through OAuth and disconnects any of them.
Replacing the personal access token now disconnects all of them; the
response's
oauth_grants_revokedcounts them. - Upgrading a project takes the paying organization's owner.
POST /v1/admin/projects/:projectId/billing/checkoutsubscribes the project on the paying organization's payment account, so it now answers403 BILLING_AUTHORITY_REQUIREDto anyone but that organization's owner, the rule the billing portal already applied. - Organization transfers need confirmation.
POST /v1/admin/orgs/:orgId/transfer-ownershipnow requires"confirm": true. Without it nothing changes and the answer is400 CONFIRMATION_REQUIREDwith a preview of the handover. The target must already be a member of the organization; an unknown id gets the same404 NOT_A_MEMBER. - Project transfers need confirmation too.
POST /v1/admin/projects/:projectId/transfer-ownershipnow requires"confirm": true. Without it nothing changes and the answer is400 CONFIRMATION_REQUIREDwith a preview inerror.details: the new owner's id, email and role, the personal organization the project moves into, and your role afterwards (admin).amba_projects_transfer_ownersends the confirmation its preview step already asked for. Breaking for local MCP installs:@layers/amba-mcpbelow 4.1.10 (or@layers/ambabelow 4.1.28 running it) cannot transfer a project until upgraded; the hosted MCP server is already current. See Team. - OAuth connections cannot set lasting delivery targets. Creating a
webhook subscription (tenant or control plane), changing a subscription's
target_url, setting an email forward, turning catch-all forwarding on, and creating or onboarding a payout account (the project's payments account or an organization's affiliate payout account) answer403 OAUTH_TOKEN_CANNOT_SET_DELIVERY_TARGETover OAuth: those deliveries would keep reaching their destination after the app is disconnected. Use a PAT or the console. Pausing or deleting a subscription, deleting a forward and turning catch-all forwarding off still work over OAuth. - More agent tools preview before they run.
amba_payments_charge,amba_functions_deploy,amba_sites_deploy,amba_webhooks_rotate_secret,amba_developer_rotate_pat,amba_app_mcp_set_exposure, andamba_app_mcp_update_configwhen it setsenabledorauth_modenow return a dry-run preview (CONFIRMATION_REQUIRED) unless the call carriesconfirm: true. A deploy's preview names the live version it replaces, and the preview call may leave the code or files out so they are sent once; an exposure preview lists what it would expose and hide. The two exposure tools now carrydestructiveHint: true. See MCP tools. - Tool errors are marked as data. An API error can repeat stored text (a
name, a function's error body), so MCP error results now wear the
untrusted-data envelope: the readable
API error <status>:line tags the message with<amba-data …>, and the JSON error keepsstatusfirst,errorinside the markers, andnoticesafter them.error.codeanderror.detailsstay at the same paths. Amba's own refusals (FREE_TIER_LIMIT_REACHEDwith its one-call upgrade, theOAUTH_TOKEN_CANNOT_*refusals,RATE_LIMIT_EXCEEDED,MISSING_PAT) stay unframed. - Each agent-surface client has its own budget. Loading an app's agent
surface (
/app/<project-id>/mcp) is now rate-limited per client IP address (600 loads a minute), so one holder of the app's client key cannot use up the budget of your other users, and a tool list that has not changed is served again without rebuilding it. A client over its budget gets HTTP429 APP_MCP_RATE_LIMITEDwithRetry-Afterin place of an empty tool list; when the app's tool list is being rebuilt for many clients at once, the answer is429 APP_MCP_BUSY. amba functions devanswers local callers only. The dev server now refuses (403) a request whoseHostis a name other thanlocalhostor an IP address, a browser request from a page that is not local, and a request the browser marksSec-Fetch-Site: cross-site, so a website open in your browser cannot drive your function (DNS rebinding, or a plain link or image).--allow-host <name>admits a tunnel's public name or a LAN address./amba-buildruns the prompt it ships with. The skill no longer tells an agent to fetch the prompt from the docs site and prefer it; it runs the copy bundled with the CLI and treats pages it reads as reference material. Re-runnpx @layers/amba init --sandboxto update an installed skill.- The MCP server's internal errors are generic. An unhandled error
answers
500 INTERNAL_ERRORwith a fixed message and arequest_id; a failed OAuth client registration answers a fixedserver_error. - Amba Payments takes a minimum platform fee set by your tier. Every
payment (
amba_payments_charge,POST /v1/client/payments/intents, agent checkout) takes at least the tier minimum: Free 5%, Pro 4%, Scale and Enterprise 3.5% of the amount (rounded up), each plus 30 minor units. Yourapplication_fee_amount,platform_fee_bpsor default fee can raise it; a lower fee, a fee of 0, or no fee is raised to it, and400 NO_PLATFORM_FEEis gone. A payment too small to cover the minimum answers400 FEE_EXCEEDS_AMOUNT. The account read andPUT /payments/configreturnplatform_fee_minimum, andPOST /v1/client/payments/intentsreturns theapplication_fee_amounttaken. A refund (amba_payments_refund) pulls the refunded amount back from your balance and Amba keeps its platform fee. See The platform fee. The MCP tool descriptions for these payment, affiliate payout, grant-rule and store-listing rules ship in@layers/amba-mcp4.1.5 and the CLI agent skill in@layers/amba4.1.24. - Your affiliate program's ledger is yours to settle. Amba sends affiliate
payouts only for programs it runs itself, on conversions a payment provider
confirmed.
amba_affiliate_payout_runon a customer's program answers409 PAYOUT_FUNDING_UNAVAILABLEand the scheduled settlement skips it; commissions keep accruing in the ledger, and conversions now carryverified_source. See Settle with your affiliates. - A paid tier follows the subscription status. An
activeortrialingsubscription holds the paid tier. Apast_dueone keeps it for a 14-day grace window while the payment is retried, then drops to free until it succeeds.canceled,unpaid,incomplete_expiredandpausedsubscriptions leave the project on free, and anincompletefirst payment grants nothing. See Billing. - Currency grant rules state a daily limit. Creating a rule with
amba_currency_grant_rules_create(orPOST /v1/admin/projects/:id/currency-grant-rules) requires exactly one ofmax_per_dayoruncapped: true; neither answers400 DAILY_CAP_REQUIREDand both answer400 INVALID_GRANT. On events an app tracks with its client key, a rule grants when it setsmax_per_day, or when it is fixed-amount anduncapped. A property-scaled rule needsmax_per_daythere. Events recorded by a server key, the admin events API or the subscription cascade match every enabled rule. Existing fixed-amount rules withoutmax_per_daystop granting on client-tracked events and keep granting on server-recorded ones; delete and recreate such a rule withmax_per_dayoruncapped: trueto restore it. See Daily limits. - A store's per-user purchase limit holds on catalog purchases.
POST /v1/client/inventory/purchasewithoutstore_idanswers409 STORE_PURCHASE_REQUIREDfor an item an active store lists withmax_purchases_per_user; buy it withstore_idor throughPOST /v1/client/stores/:storeKey/purchase. A listing's limit also counts the user's earlier purchases of the item straight from the catalog. See Purchase limits. - A segment-targeted store is open only to its segment. A store with a
segment_idis listed byGET /v1/client/stores(Amba.stores.list()) only for users the segment's last evaluation recorded as members. For anyone else, reading it, its purchase options and buying from it (includingPOST /v1/client/inventory/purchasewith itsstore_id) answer404 NOT_FOUND. A user the segment has not evaluated yet sees the store after the next evaluation adds them. Deleting a segment a store targets answers409 SEGMENT_IN_USE; detach it from the store first. An item that only segment-targeted stores list is exclusive to those segments: buying it withoutstore_idanswers403 STORE_ACCESS_REQUIREDfor anyone outside them; list it in a store without a segment to sell it to everyone. See Segment targeting. - A store charges the price it shows. A listing's
override_pricesnow applies toPOST /v1/client/stores/:storeKey/purchase(Amba.stores.purchase), andGET /v1/client/stores/:storeKey/optionsshows it asprice_cents, so the option shown and the amount charged agree.GET /v1/client/stores/:idlistings carrypriceswith the override applied and leave out deactivated catalog items. See Price overrides and the Stores API reference, which now covers the options and purchase routes. inventory.purchasecan buy through a store. The purchase request takesstore_id, so an app buys an item an active store limits per user (which answers409 STORE_PURCHASE_REQUIREDwithout it) at the store's price and within its limit. It needs@layers/amba-web4.0.14,@layers/amba-node4.1.6,@layers/amba-react-native4.0.13, the Swift and Kotlin SDKs 4.1.4, Flutter 4.0.8 or Unity 4.0.7; earlier versions dropstore_id.- Store writes are validated. Adding or updating a listing answers
400 INVALID_MAX_PURCHASES_PER_USERunlessmax_purchases_per_useris a whole number from 1 or null, and400 INVALID_OVERRIDE_PRICESunlessoverride_pricesmaps currency codes to whole-number prices; a badsort_order,available_fromoravailable_untilanswers 400 with a code naming the field. Creating or updating a store checksnameandsegment_id(400 SEGMENT_NOT_FOUNDfor an unknown segment). In@layers/amba-mcp4.1.11,amba_stores_patchtakessegment_id: nullto detach a segment andamba_stores_patch_listingtakesnullto clearoverride_pricesor liftmax_purchases_per_user. - A sign-in nonce is accepted once.
POST /v1/client/auth/socialandPOST /v1/client/auth/linkrecord each Apple or Google sign-in nonce they accept until the identity token expires. A later request with a token carrying that nonce returns401 NONCE_REUSED, with or without the raw nonce in the body, so a captured request body cannot be sent again. A request that ends before it changes the account (a retry-after answer, the MAU cap) gives its nonce back, so it can be resent. See Sign-in nonce. - Unregistering a push token is scoped to the user.
DELETE /v1/client/users/me/push-tokendeactivates the token only when it belongs to the signed-in user. Another user's token stays active, and the answer is the same as for a token that does not exist. - Prompt budgets hold under concurrent calls. Each call to a budgeted
prompt holds its maximum cost (input plus
max_tokens) while it runs and settles to its actual cost when it ends, so many calls at once, streamed or not, can no longer pass the budget together. A call whose maximum doesn't fit what is left gets429 ai_budget_exceededwithreserved_usdandcall_max_usdindetails, even before spend reaches the budget. See prompt budgets. - AI provider errors no longer reach your app's users. A failed model call
answers a stable code (
ai_provider_auth_failed,ai_provider_rate_limited,ai_provider_unavailable,ai_context_too_long,ai_content_blocked,ai_invalid_requestand others) with a fixed message. The provider's error text, which could name your provider account and its quota, is no longer returned. A rejected provider key is now424(it was the provider's401). See AI errors. - Functions can require a server key. Every request a function receives
carries
X-Amba-Key-Type(server,client,internalornone), covered by the newX-Amba-Edge-Signature-V2. Your app's client key also reaches private functions, so a function only your servers may call should check forserver. See Require a server key. - Function URLs refuse floods of unknown names and keys. A request with no
key for a function that isn't public, or with a key that doesn't exist, is
answered without a backend lookup each time, and one client IP that keeps
sending such requests gets
429 rate_limited. - Site addresses are unique. Two projects can no longer end up with the
same
{slug}.app.amba.host; a new site whose usual slug is taken gets a longer one. Read the slug from the API response. - Link conflicts name no account.
POST /v1/client/auth/linkanswers an email, phone or social identity held by another user with409 *_ALREADY_LINKEDwhosedetailsis an empty object;error.details.conflicting_user_idis gone (it readsundefined), so a session can no longer map an email address to a user id. To switch to the account that holds the identifier, sign in with it (the matchingverify*call). - Push-token registration answers alike for every token. A device token
belongs to the user who registered it last, as before; the response to
POST /v1/client/push/registerandPOST /v1/client/users/me/push-tokenis the same whether the token was new or held by another user, andcreated_atis when the token was registered to its current user. - Media storage keys use a URL-safe form of the filename. Uploads through
POST /v1/client/media/upload, the SDK presign andPOST /v1/admin/projects/:projectId/mediastore the object under the filename's last path component, with accents dropped and every character other than letters, digits,.,_and-replaced by_(the extension is kept). The asset'sfilenamestays exactly as sent. A missingfilename, or one over 1024 characters, returns400 INVALID_INPUT. A rawPUTpresign key containing?,#,%,\or a control character returns400 INVALID_BODY, since its URL would address a different object. - Deleting an AI provider deletes its key.
DELETE /v1/admin/projects/:projectId/ai/providers/:namenow removes the stored key as well as the registration. Copies of the key cached by the API and the AI gateway expire within ten minutes, after which nothing can use it. - App builds run on the project's own AI key. A build's coding agent uses
the project's registered Anthropic or OpenAI key, and its sandbox gets a
repository token for that build's repository alone. Starting, iterating or
resuming a build without such a key returns
424 BUILDER_AI_KEY_MISSING; register one withamba ai providers add. - Every MCP tool error carries the untrusted-data markers. An error a tool
hits outside an API call (a response that is not JSON, a request that never
reached the API) now reaches the agent as
Tool error: <amba-data id>…</amba-data id>followed by{ "error": { "code": "TOOL_ERROR", "message": … } }inside the envelope, since its message can quote a response. A tool's own refusals (a missing argument, a bundle over the size cap, an expired login) stay plain text. Hosted MCP now;@layers/amba-mcp4.1.13 and CLI 4.1.31. - Older
/amba-buildskills are refreshed. A skill installed by a CLI before 4.1.28 told the agent to fetch the build prompt from the docs site and prefer that page. From CLI 4.1.31, anyambacommand run in the project rewrites.claude/skills/amba-build/SKILL.mdwith the prompt bundled with the CLI and prints the path it updated. - An anonymous id signs in only to a purely anonymous user.
POST /v1/client/auth/anonymouswith ananonymous_idsigns back in to the user that holds it while that user has no email, phone, password, or linked Apple / Google identity. For a user with any of those, the call answers401 IDENTITY_SIGN_IN_REQUIREDand creates nobody: that user signs in with the linked identity, or the app continues the session with/refresh. Every link that adds an identity gives the user a newanonymous_id, returned inuser.anonymous_id. See POST /client/auth/anonymous. - Update React Native and Expo apps. SDK versions before
@layers/amba-react-native4.0.14 /@layers/amba-expo4.0.13 re-sign in with the anonymous id when the stored session token expires. For a user with a linked identity, every authenticated call then fails with401 IDENTITY_SIGN_IN_REQUIREDuntil the user signs in again, and that repeats each time the session expires. 4.0.14 / 4.0.13 continue the session with its refresh token. - Apple and Google links sign out earlier sessions. A
/linkwithappleorgooglethat adds the identity revokes every session of the user minted before it. The session pair in the response keeps working, and every SDK stores it. - OTP and email + password links can rotate the session.
/linkwithsms_otp,email_otp, oremail_passwordacceptsrotate_session: true: the link then revokes every earlier session of the user and returns a newsession_tokenandrefresh_token. Without it, the caller's session keeps working as before. See POST /client/auth/link. - Another user's
anonymous_idstays private.PATCH /v1/client/users/:idon a different user (with ausers:writerole) returns the updated user withoutanonymous_id. A user's own responses still include it. - Apple and Google emails count only when the provider verified them. Sign-in and
/linkrecord the email from the identity token, as verified, only when the token says the provider verified it. Any other address from the token is left off the user. A returning sign-in or a link records a verified address only while no other user holds it, and otherwise completes with the user's email fields unchanged. - Email sent from the app goes to a verified address.
POST /v1/client/email/sendanswers403 EMAIL_NOT_VERIFIEDand sends nothing when the signed-in user's email is not verified. Templatedatavalues are HTML-escaped in the HTML body. Each user can send 10 emails an hour and 50 a day, and each project accepts 1,000 sends from the app an hour; past a budget the call answers429 RATE_LIMIT_EXCEEDEDwithRetry-After. Sends from your server, agents, and functions are unchanged. See From the app.
Fixed
-
Flutter iOS crash reports symbolicate the SDK. Flutter 4.0.9 ships the iOS framework's debug symbols (
amba_core.framework.dSYM). CocoaPods copies them into your app's archive, so App Store Connect and crash reporters show function names, files and lines for the SDK's frames, and the upload stops warning about a missing dSYM foramba_core.framework. -
The Flutter SDK page lists the platforms that ship. The
ambapackage supports iOS 13+ and Android API 24+; it has no native code for macOS, Windows or Linux. See Flutter. -
SDK builds report the core's version. The
amba-core/<version>part of theX-SDK-Versionheader and each event'ssdk_versioncarry the core's release version. The Swift, Kotlin, Flutter and Unity SDKs report it from their next release (4.0.14, Flutter 4.0.9 first). The web and Node SDKs (and React, through web) load@layers/amba-corefrom npm and report it from the core's next npm release after 4.0.14. Builds before those send4.0.2. -
GET /v1/client/inventory/:itemIdis documented. It returns one of the caller's inventory rows byid. See Inventory. -
New site names are hostname-safe. A site name becomes part of its hostname, and the edge resolves only letters, digits and hyphens there, so a site named
my_sitewas created but no URL reached it. Creating a site with an underscore now returns400 INVALID_NAME; existing sites keep working by name.amba sites deploy(CLI 4.1.30) turns underscores in a folder name into hyphens. -
Staging site addresses name the staging host. On staging, a deploy's
urland a custom domain'sdns_targetare<slug>.staging.app.amba.host. -
The Flutter SDK works on iOS devices and simulators. Flutter 4.0.7 ships its native core as a framework that CocoaPods embeds in the app, with a device slice and a simulator slice (
arm64,x86_64). Earlier versions shipped a library the app's build dropped, so the first call,Amba.configure, failed withFailed to lookup symbol 'amba_init', and simulator builds failed withLibrary 'amba_core' not found. Upgrade toamba: ^4.0.7; nothing changes in your Podfile or Xcode settings. See Flutter. -
Remote config is evaluated per user.
GET /v1/client/configreads the session token when the request carries one and applies each key's segment and percentage conditions for that user (a server key evaluates for theX-On-Behalf-Ofuser). Callers without a session, including one whose token has expired, get each key's default value. Responses areprivate, cached 60 seconds when signed in and 300 seconds otherwise. See Segment targeting. -
SDK calls return their typed results. The client routes for deep links, inventory, content items, friendships and referral codes return the fields the SDK types declare, alongside their columns. In the web, Node, Swift, Kotlin, Flutter and Unity SDKs,
deepLinks.get,inventory.getItems/getItem/consume,content.library/item/createItem/updateItem, thefriendsmethods andreferrals.getReferralCode/claimReferralused to fail reading the response after the server had made the change. They work now on earlier SDK versions too, apart from claiming a code no user owns, which needs the versions below.inventory.consumetakes the catalog item id or an inventory rowid. Friend requests acceptuser_idas well asaddressee_id. -
inventory.purchasereturns the purchase receipt (InventoryPurchase:purchased,currency_code,price,balance_after) in every SDK, andReferralClaim.referrer_idis optional. Both types changed in@layers/amba-web4.0.12,@layers/amba-node4.1.4,@layers/amba-react-native4.0.10,@layers/amba-expo4.0.11, the Swift and Kotlin SDKs 4.1.2, Flutter 4.0.4 and Unity 4.0.5. A single-item purchase response also carries the inventory row it added to, so earlier SDK versions read it too; a bundle purchase needs the versions above. A store override price that is set and is not a whole number answers400 PRICE_NOT_CONFIGURED; a null entry uses the catalog price. -
properties.*segment conditions compare by type. A string value matches the property's text ({ field: "properties.plan", op: "eq", value: "pro" }now matches), a number compares numerically (gt 9matches10), and a boolean matchestrue/false. See Operators. -
@layers/amba-reacthooks call the web SDK's methods.useXp,useStreaks,useAchievements,useChallenges,useCurrencies,useInventory,useFriends,useFeeds,useOnboarding,useStore,useLeaderboards,useGroups,useMessaging,useReviews,useContent,useDeepLinksanduseMediatake the web SDK's argument shapes (@layers/amba-react4.0.5, which requires@layers/amba-web4.0.12). See React. -
MCP tools send what their routes read.
amba_moderation_set_trusttakesapp_user_id,trust_level(new,trustedormoderator) andauto_approve;amba_groups_update_membertakes a role ofowner,admin,memberorviewer;amba_currencies_updatesends the fields the currency stores (name,description,is_premium,initial_balance,max_balance, auto-recharge);amba_content_bulk_importsays that it appends. The trust and member-role routes answer400 INVALID_INPUTfor values outside those sets (@layers/amba-mcp4.1.2,@layers/amba4.1.21). -
@layers/amba-react-nativefriends.sendRequestsends{ addressee_id }(4.0.10). -
A group-scoped
findin the web SDK reads one group.Amba.asGroup(groupId).collections.find(...)sends the group to the API, so it returns only that group's rows; it returned the rows of every group the user belongs to (@layers/amba-web4.0.13). See Team-owned collections. -
An unowned referral code's
referrer_idisnullin web and Node.referrals.claimReferralreturnsreferrer_id: nullfor a code no user owns, as every other SDK does; the web SDK and Node'sAmbaClientreturnedundefined(@layers/amba-web4.0.13,@layers/amba-node4.1.5). -
reviews.createworks in every SDK.POST /v1/client/reviewsreads thetarget_type/target_idthe SDKs send as well asitem_type/item_id, answers400 INVALID_ITEM_TYPE,400 INVALID_ITEM_IDor400 INVALID_RATINGfor a body the reviews table cannot hold (ratingis a whole number from 1 to 5, as a number or a digit string), and returnsauthor_id,target_typeandtarget_idfrom create and update. Earlier SDK versions work against it without an upgrade; the SDK core checks the rating before sending. See Reviews. -
friends.getList()lists pending requests.GET /v1/client/friendsreturns the accepted friendships, then the pending requests in both directions, up to 1000 rows (?limit=and?offset=page), and?state=acceptedor?state=pendingnarrows it; it returned accepted rows only, whatever the SDK asked for. Blocked rows stay out. A friend request and a block between the same two users no longer race into two rows. TheFriendshiptype carriesrequester_idandaddressee_id, so a pending row reads as incoming or outgoing (@layers/amba-web4.0.13,@layers/amba-node4.1.5,@layers/amba-react-native4.0.11,@layers/amba-expo4.0.12, the Swift and Kotlin SDKs 4.1.3, Flutter 4.0.5, Unity 4.0.6). See Friends. -
inventory.purchasebuys one item per call. A request with aquantityother than1answers400 QUANTITY_NOT_SUPPORTEDbefore anything is charged. The route ignoredquantityand charged for and granted one item. -
The setup guides describe these.
amba://setup/socialandamba://setup/economy, and the CLI's skill references, showreviews.createin every SDK, thecustomreview target and one item perinventory.purchase(@layers/amba-mcp4.1.7,@layers/amba4.1.25).
Improvements
- Auth responses say when the session ends. Every response that carries a session
pair now includes
expires_at, the session token's expiry (30 days out), so the SDKs keep a session for its real lifetime.POST /v1/client/auth/refreshalso returns theuser, like the other auth responses. - A failed link changes nothing. A
/linkwrites the identity, the anonymous id it rotates away, and any new session pair together, so an error leaves the caller's session as it was. Retry an Apple / Google or email + password link as is, and an OTP link with a new code.
SDKs
@layers/amba-react-native4.0.14.signOut()gives the device a new anonymous id whenever the session it ends was not purely anonymous, and sends the refresh token so the server revokes it.linkSmsOtp,linkEmailOtp, andlinkEmailPasswordrotate the session and store the new pair. After the session token expires, the SDK continues the session with the refresh token before it signs in anonymously, so a signed-in or linked user stays signed in across app launches.Amba.configure()throwsSecureRandomUnavailableErrorwhen the runtime has nocrypto.getRandomValues: installreact-native-get-random-valuesand import it on the first line of your entry file. See Crypto setup. While the secure store can't be read (an iOS device not yet unlocked since a restart), a call that would replace the stored session with a new anonymous user fails withSECURE_STORE_UNAVAILABLE; retry once the device is unlocked. A call refused withSESSION_REVOKED(a link on another device signed this session out) orINVALID_SESSION_TOKENcontinues the session with the refresh token and is sent again; when the refresh token is refused too, the session ends andonAuthStateChangesubscribers hearnull.@layers/amba-expo4.0.13. Where SecureStore holds the session tokens, the anonymous id is stored with them. The first launch after the upgrade moves it there from AsyncStorage. Includes the React Native 4.0.14 changes.
2026-10-14
Security
-
MCP tool results mark stored project data. A successful tool result that carries what a project stores (rows, user and group names, messages, email templates, function output and logs, webhook responses) wraps it between
untrusted_data_beginanduntrusted_data_endkeys. Their values carry a per-result random id and tell the agent the content is data. Every other key stays at its path, so code readingstatusordatakeeps working; a plain-text result sits between the same opening and closing tags. This applies onmcp.amba.devand on each app's agent surface. See Agent safety. -
Destructive MCP tools preview before they run. Deletes, removes, revokes, resets, bans,
amba_admin_bulk_update,amba_users_bulk_update,amba_projects_transfer_owner,amba_orgs_transfer_owner,amba_collections_alterwithdrop_column,amba_push_campaigns_send,amba_payments_refund,amba_affiliate_payout_run, andamba_affiliate_conversion_clawbackchange nothing unless the call carriesconfirm: true. Without it they return a dry-run preview as an error result witherror.codeCONFIRMATION_REQUIRED: the targets, counts read from the API, whether the change can be undone, and the call to repeat. Tools that already took a typedconfirm(collection name, column name, project id, bulk-delete row count) keep it and accepttruetoo. On an app's agent surface, developer-scope row deletes preview the same way; end-user deletes, which are recoverable soft deletes, run directly. -
MCP safety hints match what tools do. Bulk updates, ownership transfers,
alter, secret and PAT rotation, deploys, monetization apply, domain purchases, charges, refunds, payout runs, and clawbacks carrydestructiveHint: true. Tools generated for an app take their hints from their verb, so a collection namedorder_statuskeeps its update tool marked as a write. A function exposed without declared hints is marked as one that may change state. -
The MCP server reads a large request body only for a verified credential.
/mcpand/app/<project_id>/mcpaccept up to 16 MiB when theAuthorizationheader holds a live developer credential (personal access token, developer or OAuth access token, operator token, or server key). A call whose only credential is an inlinepatargument may also send up to 16 MiB, two such requests at a time per server instance (429withRetry-Afterwhen both are in use). Discovery, signup, and every other request without a credential may send up to 256 KiB and get413 PAYLOAD_TOO_LARGEabove it. A client key on an app's agent surface stays at 256 KiB. -
A rejected developer credential on an app's agent surface gets
401. When the API rejects the Bearer on/app/<project_id>/mcp, the request ends with401 INVALID_TOKENand aWWW-Authenticateheader. No tools are listed or run, and nothing is metered. -
Claim links take a press of Confirm. The link that
amba_developer_claimandamba claimemail opens a page naming the address, and the claim happens when the owner presses Confirm on it. Opening the link changes nothing, so a mail scanner that fetches it proves nothing. Links already in inboxes open the same page. -
Media from the CDN serves as media, on every plan. Every response from
<project>.cdn.amba.hostcarriesX-Content-Type-Options: nosniffandContent-Security-Policy: default-src 'none'; sandbox. HTML, SVG, XML, XSLT, JavaScript, and PDF files download as attachments when opened from their URL; images, audio, and video serve inline as before. A file stored with no type is served with the type its extension names. See Media Serving. -
Upload URLs carry the declared content type. A
PUTonPOST /v1/client/media/presignrequirescontent_type(400 INVALID_BODY) and applies the bucket's MIME allow-list (400 MIME_NOT_ALLOWED).POST /v1/client/media/uploadandPOST /v1/admin/projects/:projectId/mediarequiremime_type. The upload must send thatContent-Type, and the asset is served with it. An upload URL issued earlier without a type is refused at upload time; request a new one. -
Set-Cookieheaders from functions and sites stay on their own host. On an Amba host, a response keeps only host-only cookies and cookies whoseDomainis exactly the serving host; any otherDomainis dropped. On a custom domain, a cookie whoseDomainnamesamba.hostis dropped and the domain's own cookies pass through. Responses on the shared path URLfn.amba.host/<project_id>/<function>carry no cookies and aContent-Security-Policy: sandbox(scripts and forms allowed) that runs any page there in its own isolated origin. -
X-Amba-Developer-Idnames a developer of the function's own project. The header is set when the request's personal access token or developer token belongs to an owner or admin of that project. Any other developer's credential leaves it empty. See Signed-header identity. -
Custom site domains serve once ownership verifies. A hostname gets its edge route and host mapping when
ownership_statusturnsactiveand its DNS shows this attachment (theownership_verificationTXT record from the attach response, or a CNAME to your project'sdns_target). Amba routes it within minutes of that. Until then it is a claim that lasts 72 hours (claim_expires_at); after that, the next attach of the hostname by any project takes it over. Attaching is limited to 10 hostnames per project per hour, and detach responses includedns_cleanup. See Custom domains. -
Copying integration credentials needs access to both projects.
include_integration_credentials: trueon a project import copies stored credentials only when you manage the source project as well as the target (owner or admin on each, or an operator token whose scope covers both) and both belong to the same developer. Server keys and function tokens get403 INTEGRATION_CREDENTIALS_FORBIDDENfor the copy. Imports without the flag work as before. See Integrations and push credentials. -
Integration responses return the list fields only.
POSTandPATCHon/v1/admin/projects/:projectId/integrations, and theintegrationson the project detail, returnid,project_id,provider,is_active,created_atandupdated_at(pluswebhook_urlfromPOST), the same shape the list returns. Stored credentials stay on the server; usePOST .../integrations/:provider/testto check that an integration works. -
The full billing portal belongs to the organization's owner. On a project with an active subscription,
POST .../billing/portalstill gives any project owner or admin a session for that subscription. The full portal for the payment account (cards, every invoice, every subscription the organization pays for) now requires owner access to the organization that pays for the project; anyone else gets403 BILLING_AUTHORITY_REQUIRED. -
Organization transfers hand over every project.
POST /v1/admin/orgs/:orgId/transfer-ownershiptakes an existing member of the organization as the new owner (404 NOT_A_MEMBERotherwise) and makes them the owner of every project in it. Sub-organizations the previous owner owns, their projects, and the service accounts that acted for the previous owner there move to the new owner too. Withremove_previous_owner: true, the previous owner loses access to all of them, including projects they created. Inside an organization, project access comes from the organization and from project membership. -
Domain purchases need a developer sign-in and have a cap. A confirmed purchase (
amba_domains_purchase,POST .../domains/purchase) takes your PAT or a console session (403 DOMAIN_PURCHASE_REQUIRES_DEVELOPERotherwise), allows 3 purchases per project every 30 days (429 DOMAIN_PURCHASE_LIMIT_REACHED), and charges your account before the domain is registered. Domain charges aren't available yet, so a confirmed purchase returns503 DOMAIN_PURCHASE_BILLING_UNAVAILABLEand registers and charges nothing. Search and price checks work as before. See Errors. -
Webhook rate limits count verified deliveries only. The RevenueCat, Superwall and Stripe Billing webhooks check a delivery's signature before counting it toward your project's 100-per-second limit, so the whole budget stays available to your real deliveries.
-
Event names Amba records itself are reserved. A client key can no longer track the
ai.prefix,xp_awarded,streak_shielded,league_promoted,league_demoted, or any name in the platform event catalog (subscription.started,economy.currency.granted, ...). The track route (/v1/client/events) answers400 RESERVED_EVENT_NAMEand writes nothing; the batch route rejects only that event. Server keys and the admin events API record these names as before. See Reserved event names. -
Private groups are visible to their members only. For a private group the caller doesn't belong to, the group detail and member list routes (
/v1/client/groups/:groupId,/members),/joinand/inviteanswer404 NOT_FOUND, the same answer as an unknown group. An unknown group's member list used to come back empty, and/joinon a private group used to answer403 NOT_PUBLIC. Public groups work as before. -
Live collection subscriptions follow group access. On a group-owned collection,
.subscribe()delivers the rowsfind()returns: rows of the subscriber's groups, narrowed byasGroup. Membership is checked again while the subscription stays open, so a user who leaves a group stops receiving its rows. -
Upserts and idempotent retries on group-owned collections follow current group access. An upsert can update or return an existing row only in a group the caller can write or read now, and it never changes a row's
group_id. A conflicting row outside that reach answers409 CONFLICT(on_conflict=update) ordata: null(on_conflict=ignore); a retriedIdempotency-Keywhose row the caller can no longer read answers409 IDEMPOTENCY_REPLAY_GONE. -
Event previews show less to client keys. With a client key,
POST /v1/client/events/explainleaves webhook subscriptions out, andevent.user_idmust be the signed-in user (403 USER_ID_NOT_ALLOWEDotherwise). With a server key, webhook destinations appear as scheme and host. -
A column type change's
usingis a cast of that column. OnPATCH /v1/admin/projects/:projectId/collections/:name,change_type.usingmay reference only the column being changed, with literals, casts (score::integer,CAST(score AS integer)) and a fixed list of conversion functions such asnullif,trim,roundandstring_to_array. Another column, a subquery, an operator, a comment or a semicolon answers400 INVALID_COLLECTION_SCHEMAand nothing runs. The documented casts work as before. An expression that used an operator (ts / 1000),CASEor a function outside the list now answers 400; convert in two steps or add a new column. See change_type. -
Media job callbacks are bound to their job. A managed media job's runtime callback token is now tied to the project and job it was issued for, and is checked before anything else. A job created before this change and already handed to its runtime can no longer report from it; create it again with a new
idempotency_key. Retrying the create of such a job that was never handed over (sameidempotency_key) gives it a working token. -
Raw-key upload URLs register an asset. A
PUTonPOST /v1/client/media/presignregisters the object as the user's asset and returns itsasset_id. Once the upload arrives the asset is listed, counted as media storage, deletable by id and erased with its user. Presigning the same key again returns the same asset and restarts its retention. A raw-key upload URL lives at most one hour (expires_in_secondsabove 3600 is capped). A key another uploader's asset uses answers409 KEY_IN_USE, and a key that names no file or has an empty,.or..segment (written plainly or percent-encoded) answers400 INVALID_BODY. See POST /client/media/presign.
Changed
- Transferring a project moves it to the new owner.
POST /v1/admin/projects/:projectId/transfer-ownershipmoves the project from your personal organization into the new owner's, and the previous owner stays on as an admin. Future upgrades bill the new owner; a subscription that is already running keeps billing the payment account it started on until it is changed or canceled. A project in a shared organization belongs to that organization and answers409 PROJECT_IN_SHARED_ORG; transfer the organization instead. See Team. - Consuming inventory takes a whole-number quantity. The consume route
(
/v1/client/inventory/:itemId/consume) requiresquantityto be a whole number of at least 1 (400 INVALID_QUANTITY) anditemIdto be a uuid (400 INVALID_ITEM_ID). An empty body still consumes one. - Store purchases through the inventory route follow the store's rules.
With
store_id,POST /v1/client/inventory/purchasesells only from an active store inside the listing'savailable_from/available_untilwindow, and it refuses deactivated catalog items.max_purchases_per_userholds under concurrent purchases. - A user can be referred once.
POST /v1/client/referrals/claimanswers400 ALREADY_REFERREDwhen the caller already claimed another user's code. Each user can claim a code your team creates (no owning user) once, up to the code'smax_uses. - Property-scaled currency grants on app-tracked events need a daily cap.
A grant rule that scales by an event property applies to events tracked
with a client key only when it sets
max_per_day. Fixed-amount rules, and events recorded by a server key or the admin events API, are unchanged. See Property-scaled grant. - Prompt budgets hold when spend can't be read. A budgeted prompt answers
503 ai_budget_unavailablewithout calling the model when its current spend can't be checked. Retry shortly.
Docs
- The Expo tutorial keeps todos private. The kitchen-sink Expo tutorial stores each user's todos in an owner-scoped collection, so every user reads and changes only their own. Its earlier version kept todos in a content library, which every user of the project can read; if you built from it, move the todos into a collection.
- Expo installs include
expo-secure-store. The Expo install commands in the docs, the SDK README andamba init(CLI 4.1.18) addexpo-secure-store. With it and AsyncStorage installed, iOS and Android builds store session tokens in the device keystore; without it the SDK stores them in plain AsyncStorage. Add it to an existing app withnpx expo install expo-secure-store. See Token storage. - Rotating a webhook secret sends the whole config. A
configsent toPATCH .../integrations/:provideroramba_integrations_patchreplaces the stored config, so send the provider's API credential along with the newwebhook_secret. The agent skill reference showed apatchargument that the tool doesn't have, and a call with it changed nothing; if you rotated a secret that way, rotate it again. See Rotating a secret. - Docs match the API and SDKs. Pages that named routes, methods, or
behavior the product doesn't have now describe what it does. If you copied
an example from them, check it against the page.
- Project-scoped admin routes carry the project path:
/v1/admin/projects/:projectId/roles,.../integrations,.../push/campaigns(test sends at.../push/campaigns/test),.../content,.../segments,.../config,.../xp,.../achievements,.../challenges,.../leaderboards,.../leagues,.../moderationand.../api-keys. - Catalog purchases use
POST /v1/client/inventory/purchase, a user's referral code isGET /v1/client/referrals/my-code, and a deep link resolves withGET /v1/client/deep-links/resolve/:slug. - A function's URL is
https://<project_id>.fn.amba.host/<name>, and a call needsX-Api-Keyunless the function was deployed with--public. See Call it. - SDK examples call the methods the SDKs export: the JavaScript SDKs take
Amba.configure({ projectId, clientKey }); the Swift, Kotlin, Flutter and Unity SDKs take the client key asapiKey. The pages also useconfig.fetch(),currencies.getBalance(),friends.acceptRequest(),roles.hasPermission(),content.today(scheduleName)andauth.me(). Node server code tracks events for a user throughamba.asUser(verifiedUserId). - Where each SDK keeps the session: browser
localStoragefor web and React, a secure token store for React Native and Expo (thetokenStorageoption), and process memory for Node, iOS, Android, Flutter and Unity, so native apps sign in on every launch. The native SDKs accept any non-empty key; keep server keys (amb_dev_sk_…,amb_live_sk_…) out of app builds. See Session storage by SDK. - The SDKs keep no config or flag cache.
GET /v1/client/configreturns each key's default value to every caller; segment and percentage conditions are stored and have no effect on the returned values. Flags are one project-wide set, and React'suseFlag(name)returns the assignment ornull. - Projects stop metered writes at their included quotas by default
(
402 TIER_QUOTA_EXCEEDED); overage accrues inoverage_billmode. See Billing. DELETE /v1/admin/projects/:projectId/push/campaigns/:campaignIdcancels a draft or scheduled campaign, and the409 ALREADY_SCHEDULEDmessage from/sendnames it.- The webhook verifier example returns
falsefor a missing or malformedX-Amba-Signatureheader. See Webhooks. - API reference curl examples expand
${BASE_URL}and key variables and send each route's required body. The token reference is reachable again. - The agent skill references (
@layers/amba4.1.20) and theamba://setup/*MCP resources show each tool's real arguments.amba_sdk_get_setup_instructionsreturns the current install coordinates (https://github.com/layers/amba-sdk-ios,com.layers.amba:amba-sdk-android, pub.devamba), andamba initscaffoldsAmba.configure({ projectId, clientKey }).
- Project-scoped admin routes carry the project path:
SDKs
Released as @layers/amba-core 4.0.11, @layers/amba-web 4.0.11,
@layers/amba-node 4.1.3, @layers/amba-react-native 4.0.8,
@layers/amba-expo 4.0.9, iOS 4.1.1, Android 4.1.1, Flutter 4.0.3 and
Unity 4.0.4.
- Ids stay inside their path segment. Every id, key, short code or name
an SDK places in a request path is percent-encoded, so
/,?and#stay part of the value. An id of.,..or the empty string fails withVALIDATION_ERRORbefore any request is sent. - Offline writes belong to the user who queued them (web, Node, React
Native).
Amba.offline.flush()replays only the signed-in user's writes, andpending()lists only theirs. A write queued before a different user signed in waits for its own user, andAmba.auth.signOut()empties the queue (a write still in flight at sign-out is refused withOFFLINE_QUEUE_PURGED). Writes queued by an earlier SDK version, and pre-sign-in writes whose anonymous id is gone, are dropped on the next flush. See Offline queue. - React Native keeps session tokens out of plain AsyncStorage. The new
tokenStorageoption defaults to"secure": the Keychain/Keystore throughreact-native-keychainor your ownsecureStorageadapter, and memory only when neither is available. Action: installreact-native-keychain(or passsecureStorage) to keep users signed in across launches;tokenStorage: "asyncStorage"keeps the earlier behavior. Tokens an earlier version stored in AsyncStorage move into the secure store on first launch. Expo apps withexpo-secure-storeare unchanged. See Where session tokens are stored. - Memory-only web sessions.
Amba.configure({ sessionPersistence: 'memory' })keeps the session out oflocalStorage. See Session persistence. - Sign in with Apple and Google bind each attempt to a nonce.
Amba.signInWithApple()andAmba.signInWithGoogle({...})in@layers/amba-exposend the provider a one-time nonce's hash and Amba the raw value, so an identity token issued to another sign-in (a different app sharing your client id, or a different attempt) can't be exchanged without that attempt's raw nonce. Action: they needexpo-cryptoorreact-native-get-random-values; without either they fail withAPPLE_SIGNIN_UNAVAILABLE/GOOGLE_SIGNIN_UNAVAILABLE.Amba.auth.signInWithApple(token, { nonce }),signInWithGoogle(token, { nonce })andlinkAccount(provider, token, { nonce })in React Native take the raw nonce when you run the provider flow yourself. See Sign-in nonce. - The React Native build of the core needs a secure random source. It
reads
crypto.getRandomValuesfrom the app (installreact-native-get-random-values) and fails with a clear error when there is none. It no longer installs its owncrypto.getRandomValueson the global object.
API
POST /v1/client/auth/socialand/linkcheck the sign-in nonce. A request may carrynonce, the raw value whose SHA-256 the provider request carried; the identity token'snonceclaim must match it. Tokens whose nonce an Amba SDK set require it. A mismatch returns401 INVALID_TOKEN, and anoncethat isn't a string of 1–512 characters returns400 INVALID_INPUT.POST /v1/client/auth/socialreads the token fromid_tokentoo. The SDKs send the identity token asid_token; the route accepts it alongsidetoken.
2026-10-13
Security
- MCP OAuth clients must be registered. The hosted MCP server's OAuth
endpoints (
/authorize,/tokenand/revoke) accept only clients registered throughPOST https://mcp.amba.dev/register, andredirect_urimust match one of the client's registered URIs exactly. Claude, Cursor and other MCP clients register themselves when they connect, so connecting works as before. An OAuth connection made with an unregisteredclient_idstops refreshing and must reconnect: remove the connector and add it again. The sign-in page now names the app asking for access and the host your browser returns to. A malformed authorization request shows an error page onmcp.amba.dev; the browser returns to the app only with an authorization code. See Browser-only clients. - OAuth connections can be revoked.
POST https://mcp.amba.dev/revoke(RFC 7009, listed asrevocation_endpointin the authorization-server metadata) ends a connection: revoking its access token or refresh token revokes both. When a refresh token that was already used comes back, the server ends that whole connection, and the app signs in again. - OAuth connections can't rotate the PAT.
amba_developer_rotate_pat(POST /v1/auth/developer/pat/rotate) answers403 OAUTH_TOKEN_CANNOT_ROTATE_PATwhen the request comes through an OAuth connection. Rotate the PAT with the current PAT or a login session. - Request limits on the MCP endpoints.
/mcpand/app/<project_id>/mcpanswer413 PAYLOAD_TOO_LARGEto a body over 16 MiB and refuse a JSON-RPC batch of more than 20 messages with a-32600error. The OAuth endpoints take bodies up to 64 KiB. amba initkeeps credentials out of your repository (@layers/amba4.1.17).- The project's server key lives in the OS keychain, next to your token
(
~/.amba/server-keys.json, mode 0600, where no keychain is usable)..env.localand.amba/project.jsonhold the project id, client key and API URL, both at mode 0600.amba server-keyprints the key for scripts:AMBA_SERVER_KEY=$(amba server-key) node server.js. The nextamba initmoves a server key an older CLI left in.amba/project.jsoninto the keychain; when the keychain already holds a different key for the project, the stored key stays, the file's key is kept in~/.amba/backups/, andinitsays so. On a machine that stores no key for a linked project,initmints one, and revokes it again if it cannot be stored. AnAMBA_SERVER_KEYline in.env.localstays in place, since your server code may read it, andinitprints a note saying where it belongs.amba logoutleaves server keys stored. initadds.amba/and.env*.localto the project's.gitignore, creating the file when there is none, and warns when git already tracks a file that holds or held a credential (.amba/project.json,.env*.local, old MCP config backups).- An
ambaentry in a project-local MCP config (.mcp.json,.cursor/mcp.json) is rewritten to reference theAMBA_PATenvironment variable (${AMBA_PAT:-}for Claude Code,${env:AMBA_PAT}for Cursor), andinitsays where the PAT is stored; setAMBA_PATto your PAT (amb_dpat_…) in the environment your editor starts from. A project config without anambaentry stays as it is. The user-scope configs in your home directory keep the PAT and are written with mode 0600. - When
initreplaces a differentambaMCP entry, the old file is copied to~/.amba/backups/(mode 0600).initalso moves the.mcp.json.bak-<time>and.cursor/mcp.json.bak-<time>copies earlier versions left in the project directory, which hold the PAT, to the same place. Earlier versions also left such copies beside the configs in your home directory; delete those once you no longer need them. - Your token goes only to
AMBA_API_URL, elsehttps://api.amba.dev. Anapi_urlin.amba/project.jsonis a record, andinitwarns when it differs. init --sandbox --no-mcp-configprints the MCP snippet with a<your-pat>placeholder, then a masked form of the PAT and where it is stored. Seeamba initand Project config files.
- The project's server key lives in the OS keychain, next to your token
(
- Browser sign-in asks for a click and proves where it started.
amba loginopens the console with a one-timestatevalue. After you click Connect CLI, the console POSTs your session to the CLI's listener on127.0.0.1with thatstatein the body, and the CLI accepts it only on the port it opened and only with its ownstate. Tokens stay out of URLs and browser history. A sign-in link from a CLI older than 4.1.17 shows an update prompt: runnpx @layers/amba@latest login. Seeamba login. amba sites deployskips dotfiles..env*files,.amba/,.mcp.jsonand every other dot-prefixed file or directory stay out of the upload, at any depth, and the deploy output lists what it skipped..well-known/at the top of the directory still publishes. See Deploy.- Signup and login tell agents where a PAT belongs. The
mcp_configblock fromamba_developer_signupandamba_developer_loginnames each client's user-scope file as the place for the PAT, and shows theAMBA_PATreference a committed project file uses instead (@layers/amba-mcp4.0.17).
2026-10-10
New
- Challenges complete and reward on their own. Amba now evaluates
challenges about every 5 minutes after your app's activity: a participant's
progress and completion catch up within about 5 minutes of the event, XP or
streak that moves them. Progress counts activity since the challenge's
start_at, including activity from before the user joined, so participants who already met a goal complete on the first evaluation after this release. Each completion emitsgamification.challenge.completed, grantsreward_xp(emittinggamification.xp.granted, plusgamification.level.upfor each level crossed) and unlocksreward_achievement_id, once per participant; webhooks subscribed to those events receive them. Participants still short of the goal when a challenge ends are markedfaileda few minutes afterend_at. Challenges that ended before 2026-10-10 are left exactly as they were: their participants are not completed, failed or rewarded. See When progress updates.
Changed
- An
event_countchallenge counts one named event. Challenges gainevent_name, the name of the event anevent_countchallenge counts, matched exactly. A "10 workouts this week" challenge withevent_name: "workout_completed"completes on the user's tenthworkout_completedevent, and their other events don't count toward it. Creating anevent_countchallenge now requiresevent_name(400 VALIDATION_ERRORwithout it), and the other goal types reject it.PATCHsets it, and theamba_challenges_createandamba_challenges_updateMCP tools take it. Project exports and imports carry it. See Counting one event. - Existing
event_countchallenges wait for an event name. Anevent_countchallenge created before this change hasevent_name: null. It is skipped and never completes until you setevent_nameon it; progress then counts the named event from the challenge's start.xp_earnedandstreak_maintainedchallenges are unchanged.
2026-10-09
Security
- Function URLs keep function names private. A call to a function name
with no active deployment answers
404 function_not_foundonly to a caller with a validX-Api-Keyfor the project. Every other caller gets the same401a private function gives:auth_requiredwith no key, andinvalid_api_keywith a key that is wrong, revoked, or belongs to another project. Public functions still answer without a key, and a deleted function still stops serving at once for every caller. See Who can call a function.
Fixes
- Sending domains bought through Amba get every DNS record they need,
CNAME included. Some sending domains need a return-path CNAME (for
example
rsend.example.com) next to the DKIM, MX and SPF records. Amba now writes it with the rest: an identical record already in DNS is kept, a record Amba created is updated in place, and any other record at that name answers409 DNS_RECORD_CONFLICTand stays as it is.dns_statusispublishedonly once every record is in DNS; a response could previously saypublishedwhile the CNAME was still pending, and verification then stalled. Removing the sending domain deletes the CNAME with the other records Amba created. - Partial verification says what is left. While some records are
verified and the rest are still being checked
(
provider_status: "partially_verified"), the domain stayspending, the automatic checks keep running, andnext_stepnames each record still unverified. A verified domain that drops back topendingstarts a new 72-hour window of automatic checks. Self-hosted domains list the CNAME inrecordslike any other record to create. Recordtypeis now"TXT" | "MX" | "CNAME"in the API reference,@layers/amba-shared4.0.9, and theamba_email_domains_*MCP tool descriptions (4.0.16).
New
- Domains bought through Amba send email with no DNS step. Adding one of
the project's own domains bought through Amba (or a subdomain of one) as a
sending domain now has Amba write its DKIM, return-path MX and SPF, and
(when the domain has no DMARC policy)
v=DMARC1; p=none;records into the domain's DNS, start verification, and keep checking until it verifies. Responses fromPOST,GET, andPOST …/verifyon/v1/admin/projects/:id/email/domainscarrydns_managed: true,dns_status, apublish_stateper record, and anext_stepsentence, so an agent can tell no DNS action is needed. Amba leaves identical records alone, never touches the domain's root MX and SPF records (email forwarding keeps working), keeps an existing DMARC policy, and never overwrites a record it did not create: a conflicting record answers409 DNS_RECORD_CONFLICTnaming it. Removing the sending domain deletes only the records Amba created. A sending domain added before this release switches over on its next verify call or automatic check. See Sending domains. - Automatic verification checks for every sending domain. After Amba
publishes the records, and after any verify call, Amba checks the domain
again for 72 hours (every 5 minutes at first, backing off to hourly).
next_check_atandchecks_untilshow the schedule; when the window ends unverified the domain stayspendingandnext_stepasks for another verify call. email.domain.verifiedcontrol event. Fires once when a sending domain verifies, with{ domain, dns_managed }. Subscribe withamba_control_webhooks_create(email.domain.verifiedoremail.*).- Email API reference. Email documents the
sending-domain endpoints for both DNS paths, their errors (including
INVALID_SENDER,DOMAIN_CLAIMED,DNS_RECORD_CONFLICT), and the From rules for sends. Projects now documentsdefault_from_email,default_from_name, anddefault_reply_to.
Changed
- Sending-domain tools say what to do next.
amba email domains add|get|verify|list(CLI4.1.16) prints the records a self-hosted domain needs, or the publish state of an Amba-managed one, followed by the next step;--jsonprints the raw response. Theamba_email_domains_*MCP tools describe both paths and add thenext_stepline to their result. - The recommended DMARC record for self-hosted domains is now
v=DMARC1; p=none;. - A revoked API key's edge window is 15 seconds. The edge now honors a cached copy of an API key for 15 seconds from when it was cached (it was 60 seconds). Revoking a key still drops the cached copy before the revoke answers; an edge location that served the key just before the revoke can accept it for at most 15 seconds more, and the same bound holds when the edge cannot be reached at revoke time. See DELETE /admin/projects/:projectId/api-keys/:keyId.
The client SDKs need no change: the server picks the sender, so sign-in codes
and magic links use a project's branded sender as soon as its domain verifies
and default_from_email is set.
Fixes
- A billing request retried a day later takes effect once. A charge or
payment intent with a
client_reference, a refund with areference, a plan checkout, overage billing and payouts each repeat the first result when retried, and that held for 24 hours. A retry after that could create a second payment intent for the same order, a second refund, a second payout, or bill an overage again. Amba now records each of these requests and, on a retry, checks what the first one made before sending anything:POST /v1/admin/projects/:id/payments/chargesandPOST /v1/client/payments/intentsanswer the original payment for the same reference, amount and currency for 30 days (after that the reference starts a new payment),POST /v1/admin/projects/:id/payments/charges/:id/refundanswers the original refund for the same reference for 30 days, and a plan checkout for a project that already holds a live subscription answers409 SUBSCRIPTION_EXISTS. A project has one open checkout link at a time: a new checkout closes the link handed out before, and a checkout started while another for the same project is running answers409 CHECKOUT_IN_PROGRESS(retry in a moment). A retry whose first request cannot be confirmed sends nothing and answers a retryable error; when Amba cannot record the request at all it answers503 CONTROL_PLANE_UNAVAILABLEand sends nothing. - Function logs answer past time ranges in seconds.
GET /v1/admin/projects/:id/functions/:name/logs(behindamba functions logs, theamba_functions_get_logsMCP tool, and the console log viewer) now reads only the stored log batches around the requested range. A one-hour window from yesterday returns its events in about a second, and a full 24-hour window in a few seconds; before, a past window could run for minutes and a day-long window ended in a gateway timeout with no body. Each request now does at most 25 seconds of work. When more events match thanlimit, or the range cannot be read in full right now, the response carries the newest events withtruncated: trueand a newnext_untiltimestamp: request again with the returnedsinceanduntil=<next_until>for the older ones. A range that cannot be read in time answers504 LOGS_TIMEOUT, and a log store that answers no read answers502 LOGS_READ_FAILED. A stored log batch that keeps failing to read is skipped, so paging always moves on, and the response then carriesunreadable_batches(the count of skipped batches in the answered range; their events are missing until a retry succeeds). Withsince_request_id, a skipped batch that could hold the request's earliest event answers502 LOGS_READ_FAILEDrather than anchoring later.untilmore than 24 hours ahead, orsincebefore 2000, answers400. The console log viewer shows each invocation's time, log lines, exceptions, and outcome, adds a "Load older" button, and its tail now shows invocations whose logs land a few minutes late.
2026-10-08
Fixes
-
An upload counts once its file arrives. Registering an upload (the SDK's
presignandupload,POST /v1/client/media/upload,POST /v1/admin/projects/:id/media,amba_media_upload) used to create an asset that every read treated as real even when its file never arrived: it was listed, returned by id, signed for a download that answered 404, and counted toward media usage, the free-tier media budget and storage overage. An asset is now pending from registration until its file arrives, and committed after. A pending asset is left out of the media lists,get, download URLs and catalogs, and counts toward no usage.storage.commit(POST /v1/client/media/commit) checks that the file arrived and answers409 UPLOAD_NOT_FOUNDwhen it did not. A pending upload whose file never arrives is deleted by the daily cleanup once it is 24 hours old. The normal upload flow is unchanged: an asset is listed as soon as its upload finishes. On a free project, a pending upload holds its declared size against the media budget until it commits or is deleted. The SDKs'storage.commit,list,getanddownloadwork again: the media reads now send thebucket,keyandurlfields the SDKs read. Admin media responses carry the newupload_statefield. See When an upload counts. -
Deleting a project ends its subscription. A paid project's subscription kept billing after the project was deleted.
DELETE /v1/admin/projects/:id(andamba_projects_delete) now sets the subscription to cancel at the end of its current period before anything is deleted, so the period already paid for is the last one billed, and answerssubscription.ends_atwith the date billing stops. When the subscription cannot be set to cancel, the delete answers502 BILLING_CANCEL_FAILEDand the project stays whole; retry it. See Delete a project. -
The per-user data export carries every record of the user.
GET /v1/admin/projects/:id/users/:userId/export, the right-of-access bundle, exported the profile and ten sections. It now exports every record the project keeps about the user: the whole profile (phone, time zone and verification flags included), sign-in sessions, magic links and one-time codes, texts and emails sent to the user, XP, challenges and leagues, balances, transactions and inventory, agent checkouts, onboarding, roles, experiments, push deliveries and topics, friendships, owned groups, the user's own messages, attachments and reactions, feed items, reviews and reports, referrals, link clicks, affiliate touchpoints, the content and media the user created, audit events, and the user's rows in every collection (onecollectionssection whose lines carry acollectionfield). Password, session, sign-in-link and one-time-code hashes stay out. If you parse the bundle: it now ends with one closing line,{"section":"export_complete","data":{"sections":{…},"total_rows":…,"failed_sections":[],"generated_at":"…"}}, which counts the lines written per section. A bundle whose last line is anything else was cut off (a dropped connection or the request time limit); request it again. Skip theexport_completeline if your parser expects only data lines. See GET /admin/projects/:projectId/users/:userId/export.
Security compatibility notice
- React Native
4.0.7and Expo4.0.8add opt-in durable client-event batching. They also removeAmba.dev.resetSandbox()from the client SDK. This is a breaking change in patch releases: the old route let anyone holding an app's embedded client key delete all sandbox users, so it was removed from the server. For a sandbox reset, useamba_users_reset_sandboxfrom developer tools, or callDELETE /v1/admin/projects/{projectId}/users/reset-sandbox?confirm={projectId}with an owner or admin developer token from a trusted server. Never embed that token in a mobile app.
Fixes
- A revoked API key stops working at the edge when the revoke answers.
Before, a key deleted with
DELETE /v1/admin/projects/:id/api-keys/:keyId(oramba_api_keys_delete) kept working for function calls (*.fn.amba.host) for up to about a minute, because the edge served its cached copy of the key. Now the revoke drops that copy before it answers, so the next function call with the key fails401. Deleting a project does the same for all of its keys. An edge location that served the key in the minute before the revoke can still accept it until its local copy expires, at most 60 seconds. If the edge cannot be reached, the revoke still succeeds, the eviction is resent every minute, and each cached copy expires on its own about a minute after it was cached. See DELETE /admin/projects/:projectId/api-keys/:keyId. - Media past its retention deadline is deleted. The daily media cleanup
now runs at 03:00 UTC. It deletes each asset whose retention deadline
(
retentionDays, or the bucket'sretention_days) has passed, its stored file first and then its record, so the file's URL stops serving within about a day of the deadline. It also removes the hidden record of an asset deleted 7 days earlier. Assets deleted withstorage.deletebefore October 4, 2026 kept their stored file after the delete; the cleanup deletes those files too, within about 7 days. See Retention and Deleting an asset. - Tracked-link clicks store no IP address.
POST /v1/client/deep-links/clickrecorded the request'sX-Forwarded-Forvalue with each click, and nothing read it. A click now stores the link, the user, the platform and the time, and earlier clicks have their IP address cleared. Link stats are unchanged. See Clicks and deleted users.
2026-10-07
New
- Content runway. Every content schedule now reports how long its library
lasts. Schedule reads (
POST,GETandPATCH /v1/admin/projects/:id/content/schedules,amba_content_list_schedules) carryitems_remaining(items the schedule has not delivered yet in its current pass),items_per_day,runway_daysandexhausted. Two new events, available to webhook subscriptions andamba_events_list, tell you when to add items:content.library_running_lowfires once a day whilerunway_daysis below the schedule'srunway_alarm_days(default 30), andcontent.library_exhaustedfires on every delivery that finds the library used up. See Content runway. on_exhaustedon content schedules.wrapstarts the library over, so players see content again;stopdelivers nothing, so players see a gap,today()returnsnull, and the today read answers{ "data": null, "exhausted": true }. New schedules default tostop, and every schedule created before today is set towrap, the behavior it already had. Set it, andrunway_alarm_days, on create or withPATCH/amba_content_update_schedule. See Running out of content.
Changed
- Every content schedule walks its library from where it left off.
Rotation and weekly schedules continue in
sort_orderafter the last item they delivered, the "Tip 1 on day 1, Tip 2 on day 2" order the guide describes, so items you add extend the current pass. A daily schedule with one item per delivery that delivered yesterday from an unchanged library delivers today the item it would have delivered before. A rotation schedule withbatch_sizeabove 1 delivers the next N items each time; it used to move its window one item a day, repeating N-1 items. Random schedules draw each item once per pass before any repeats. Every delivery, of every schedule type and batch size, is recorded, soGET /v1/client/content/todaylists rotation deliveries too. - Changing a schedule's cron or timezone keeps its place in the library.
PATCH /v1/admin/projects/:id/content/schedules/:scheduleIdkeepslast_indexandlast_delivered_item_idwhen you replaceconfig, and thecronandtimezonearguments ofamba_content_update_schedulenow reach the schedule.
Fixes
- Content delivered through remote config reaches apps after the first
delivery. A schedule's delivery updated the stored value of its config key,
and apps read the key's served value, which kept the first delivery's item.
Each delivery now updates both, so
GET /v1/client/configreturns the latest item. A write that changes a remote config's value and leaves its served value behind now fails with an error naming the key. - Importing a bundle whose remote config carries two different values
returns 400.
POST /v1/admin/projects/:id/promotion/import(andamba apply) answers400 REMOTE_CONFIG_VALUE_MISMATCHwith the keys indetails.keysand writes nothing when a remote config in the bundle setsvalueanddefault_valueto different values. Set both to the value apps should get and import again. Re-applying a project's own export imports as before. - Deleting a user deletes the user's telemetry events. Deleting an app user
(
DELETE /v1/admin/projects/:id/users/:userId, theanonymous_idform, the sandbox reset, oramba_users_delete) removed the user's engagement events and kept the user's telemetry events with the user id cleared, so they still showed up in event lists, counts, analytics, funnels, and exports read withsource=telemetryorsource=all. A delete now removes both kinds of events in the same transaction. See DELETE /admin/projects/:projectId/users/:userId. - Deleting a user keeps other users' replies. Deleting an app user removed
the messages the user sent and, with them, every reply to those messages,
including replies other users wrote, along with their reactions and
attachments. A delete now removes the deleted user's messages, their own
replies included. A reply another user wrote stays in the conversation with
parent_message_id: null, so it reads as a top-level message, and the thread list for the removed message (?parent_message_id=<id>) returns an empty page. This coversDELETE /v1/admin/projects/:id/users/:userId, theanonymous_idform, the per-user reset with?delete=true, andamba_users_delete. The moderation delete (DELETE /v1/admin/projects/:id/messaging/messages/:messageId,amba_messaging_delete_message) removes the one message and keeps its replies the same way. See Threaded replies. - Deleting a user deletes the media they uploaded. Deleting an app user left
every media asset the user had uploaded in place: the stored files stayed
served at their URLs, the records stayed in the admin media list, and media
catalogs kept them. Every user delete now purges them:
DELETE /v1/admin/projects/:id/users/:userId, theanonymous_idform, the per-user reset with?delete=true, the sandbox reset, and theamba_users_delete,amba_users_reset_userandamba_users_reset_sandboxtools. In the delete's transaction the user's assets leave every read and their catalog entries go. The stored files and then the records are deleted right after, and a file that cannot be deleted at once is retried in the background until it is gone. An asset registered in the six hours before the delete has its file deleted again six hours after registration, for an upload still streaming at the delete. The newmedia_purgefield on each delete response reportsassets,purgedandpending. Assets your project uploaded and other users' assets stay. A deleted user's session can no longer get an upload URL:POST /v1/client/media/uploadand/presign(SDK mode andmethod: "PUT") answer404 USER_NOT_FOUND. The admin media list (GET /v1/admin/projects/:id/media,amba_media_list) now returns live assets only. See Cascade on user delete. - Deleting a project deletes its stored files. Deleting a project
(
DELETE /v1/admin/projects/:id,amba_projects_delete, oramba projects delete) removed its database, secrets and API keys and left its stored files in place: media uploads, files its functions wrote, and every hosted site's deployed files. A delete now deletes all of them. The files go in the background right after the delete answers, so the delete never waits on storage; a file that cannot be deleted at once is retried every few minutes, and six hours later each location is checked once more for a file an upload or deploy in flight at the delete wrote afterwards. The response's newfile_purge.locationscounts the locations whose deletion started. A project on hold or archived keeps every file. See DELETE /admin/projects/:projectId. - Deleting a user clears the anonymous id on the user's affiliate
touchpoints. An affiliate touchpoint (
POST /v1/client/affiliate/touchpoint) records the anonymous id the app sends, and a user delete kept it, so the deleted user's affiliate clicks still carried the identifier tying them to the user. Every user delete now clears it in the delete's transaction:DELETE /v1/admin/projects/:id/users/:userId, theanonymous_idform, the per-user reset with?delete=true, the sandbox reset, and theamba_users_delete,amba_users_reset_userandamba_users_reset_sandboxtools. The touchpoints stay with their affiliate code, source and time, so the affiliate program's click counts are unchanged. See DELETE /admin/projects/:projectId/users/:userId. - Deleting a user clears the IP address on the user's tracked-link clicks.
Deleting an app user cleared the user id on each of the user's tracked-link
clicks and kept the IP address the click recorded. Every user delete now
clears the IP address too, in the delete's transaction:
DELETE /v1/admin/projects/:id/users/:userId, theanonymous_idform, the per-user reset with?delete=true, the sandbox reset, and theamba_users_delete,amba_users_reset_userandamba_users_reset_sandboxtools. The clicks stay in the link's stats as anonymous clicks with their link, platform and time, sototal_clicksandclicks_by_platformare unchanged, andunique_userscounts the users who still exist. See link stats. - The per-user data export includes the user's telemetry events.
GET /v1/admin/projects/:id/users/:userId/export, the right-of-access bundle, streamed the user's engagement events and left out the events the app tracked with{ telemetry: true }. The bundle now ends with atelemetry_eventssection: one line per telemetry event the user sent, with itsevent_name,properties,occurred_atandingested_at, oldest first. Like every other section it streams in batches and has no row cap. See GET /admin/projects/:projectId/users/:userId/export. - Link stats count all
unknownclicks. When some of a link's clicks sent no platform and others sent the labelunknown,clicks_by_platform.unknown(GET /v1/admin/projects/:id/deep-links/tracked/:linkId/stats,amba_tracked_links_get_stats) reported only one of the two groups. It now adds them. amba initwires MCP clients to the endpoint you signed up on (@layers/amba4.1.14). WithAMBA_API_URLset to another Amba endpoint, the MCP client configs thatamba initwrote still pointed at the production MCP server, where that endpoint's token is unknown. The configs now point at the MCP server for the same endpoint, andAMBA_MCP_URLsets it explicitly.amba_projects_deletedescribes what it deletes. Its description said a deleted project's database stayed behind. Deleting a project removes its database with all of its data, its secrets, and its API keys in one call; the description now says so and lists the 409 answers that block a delete.- An admin XP grant to a user's first XP lands every time.
POST /v1/admin/projects/:id/xp/grant(amba_xp_grant) answered500when the user earned their first XP from a claim or an XP rule at the same moment. The grant now adds to the XP the other write created, so the total includes both.
2026-10-06
Fixes
- Telemetry events are readable. Events tracked with
{ telemetry: true }were stored and billed, but the event list, event count, analytics, and funnel reads covered engagement events only, so a project sending page views or funnel steps as telemetry saw{"total":0}. Those reads now takesource=engagement|telemetry|all(defaultengagement, unchanged):GET /v1/admin/projects/:id/eventsand/events/count,GET /v1/admin/projects/:id/analytics, and the funnel query. Withall, list rows carrysourceand counts addby_source. The MCP toolsamba_events_list,amba_events_count,amba_analytics_get, andamba_funnels_querytake asourceargument, and the CLI (@layers/amba4.1.13) takes--sourceonamba analytics export --type=eventsandamba logs tail. See Engagement and telemetry events. amba logs tail --followprints every event of a burst (@layers/amba4.1.13). Each poll printed only the newest--limitevents that arrived since the previous poll, so a burst larger than--limitlost its older events. A poll now pages through all of them, up to 20 pages, and warns when it holds any back.amba config setwrites the key (@layers/amba4.1.12). It returned404 Route not foundbecause it called an update route the API doesn't have. It now creates or replaces the key through the config endpoint the API serves, and keeps the key's existing description and targeting conditions (segment and percentage overrides) when you set only a value.- Updating a config value reaches your app. Changing a key's value through
PATCH /v1/admin/projects/:id/config/:key(which the MCP toolamba_remote_configs_updateuses) returned the new value but kept serving the old one to apps. Value updates now change what apps receive. If you updated a value this way before today, set it again to publish it. amba push testsends to a target you name (@layers/amba4.1.12). It returned404and sent no target. Pass--user <app_user_id>to notify that user's newest active push token, or--device-token <token> --provider apns|fcmto notify one device. The command prints the delivery id and the provider used.
2026-10-05
Fixes
amba loginsessions renew themselves (@layers/amba4.1.11). Before, a browser login stopped working when its access token expired, and every command failed withINVALID_DEVELOPER_TOKENuntil you ranamba loginagain. The CLI now refreshes the session before a request when the access token expires within five minutes, and once more after a401 INVALID_DEVELOPER_TOKEN, then writes the new tokens back to the OS keychain (or the 0600 fallback file). Concurrent commands share a lock in~/.amba, so the session is refreshed once.amba tokenrenews an expired session before printing, andamba whoamishows the token's expiry and whether it renewed the session. A session that can no longer be renewed (refresh token older than 30 days, or revoked) prints one line telling you to runamba login.--token,AMBA_PAT, and stored Personal Access Tokens are used as given. See Session renewal.- The recorded expiry matches the token.
amba loginrecords the access token's own expiry. It used to record 24 hours from the moment of the login, while the token it received had been issued when you signed in to the console, so the token could stop working hours before the recorded time. - MCP client configs get Personal Access Tokens only.
amba initused to write a browser-login session token into~/.claude.json, Cursor, and Windsurf configs, where it failed with401 INVALID_DEVELOPER_TOKENwithin a day. It now leaves those configs alone for a session and prints how to wire a PAT (amba init --token <pat>). Theamba_developer_loginMCP tool no longer returns anmcp_configbuilt from the session token. See Browser login and MCP. - The console's Tokens page hands out Personal Access Tokens. It used to reveal
the console's own sign-in token next to the MCP URL, and that token expires within
24 hours, so an MCP config built from it failed with
401 INVALID_DEVELOPER_TOKENthe next day. The page now shows when the account's PAT was created and last used, mints a new one after a confirmation that says it replaces the current PAT everywhere it is used (the old one stops working within 30 seconds), and shows the new token once inside a readyamba init --tokencommand and MCP config entry. The MCP URL it shows is the full endpoint (https://mcp.amba.dev/mcp). The Tokens page and the dashboard no longer show the console's sign-in token.
2026-10-04
Security
- A password sign-up no longer verifies the email.
POST /client/auth/email/signupandlinkEmailPasswordstore the address withemail_verified: false(an address the user had already verified stays verified). The user is still signed in at once. Proving the inbox from that session (linkEmailOtpwith the same address) keeps the password. When the inbox is proven by a sign-in from anywhere else (email OTP, magic link, or an Apple / Google email the provider verified) on an account that still holds an unverified password, Amba clears the password, revokes every earlier session and access token (they now answer401 SESSION_REVOKED), and detaches the phone, Apple / Google links, and anonymous id attached before the inbox was proven; the account no longer resumes by anonymous id. This closes account pre-hijacking, where someone registers another person's address before they arrive. Accounts without an unverified password see no change. Password accounts created before this release keep their verified email unless the operator marks a project's legacy accounts unproven. - Password sign-in is budgeted.
POST /client/auth/email/loginanswers429 RATE_LIMIT_EXCEEDEDwithRetry-Afterpast 20 attempts a minute or 200 an hour per IP, 10 per 15 minutes or 100 a day per account, or 60 a minute per free project (600 per paid project). The budgets run before the password check. - Free-tier cost limits fail closed. When a free project's limits or daily
counters cannot be read for a moment, calls that send, charge, or start work answer
503 LIMITS_UNAVAILABLEwithRetry-After, and free-project function invocations get the router's retryable503. Paid and comped projects are unaffected, and sign-in email keeps working. - The CLI keeps your token in the OS keychain (
@layers/amba4.1.10).amba loginandamba initstore the access and refresh tokens in macOS Keychain, Windows Credential Manager, or the Linux Secret Service, under serviceambaand accountdeveloper:<api host>, so production and staging logins stay separate. A replaced token moves to the endpoint's:previousslot (the last three are kept). Non-secret details move to~/.amba/config.json. The first command after the upgrade moves an existing~/.amba/credentials.jsoninto the keychain, reads it back to confirm, and then deletes the file; if the keychain write fails, the file stays and the CLI keeps using it. Where no keychain is usable (CI, containers, Linux without a Secret Service), the token goes to~/.amba/credentials.jsonwith mode 0600 and the CLI prints one line naming that file. A keychain that holds the token but is locked stops commands with an error naming the entry.--tokenandAMBA_PATstill take precedence over both.amba loginsigns in through app.amba.dev and stores a production session; for another endpoint useAMBA_PAToramba init --sandbox. New commands:amba whoamishows the account and where its credential is stored,amba tokenprints the token for scripts (replacingjq -r .pat ~/.amba/credentials.json), andamba logoutremoves the keychain entries and any fallback file entries. See CLI authentication.
2026-10-03
Security
- Client keys can no longer reset a project.
POST /v1/client/dev/reset-sandboxis removed and answers404, and the React Native SDK'sAmba.dev.resetSandbox()is removed with it. A client key ships inside your app, so it must never be able to delete users. Reset a free project with theamba_users_reset_sandboxMCP tool orDELETE /v1/admin/projects/{projectId}/users/reset-sandbox?confirm={projectId}using an owner or admin developer token. Server keys and function tokens now get403 OWNER_CREDENTIAL_REQUIREDon that route. - Webhook destinations must be public HTTPS endpoints. Creating, updating,
or testing an app or control-plane webhook subscription refuses loopback,
private-network, link-local, carrier-grade NAT, multicast, and cloud metadata
destinations, including hostnames that resolve to them, with
400 TARGET_URL_NOT_ALLOWED. Every delivery checks the address it connects to again, treats a3xxresponse as a failed attempt, and reads at most 64 KB of the response body. See Outbound webhooks. - Function storage accepts only string keys. Every
env.STORAGEmethod takes primitive string keys and list options of their documented types; any other value throwsTypeErrorbefore the request reaches storage. Listings return only objects under your project's storage scope.
Changed
- Free-tier limits on the surfaces that run on Amba's domains, sending
reputation, and compute. New free projects keep everything needed to build
an app and gain free-tier limits: custom
domains, domain purchase, email to arbitrary recipients, custom email
templates, and the app builder are paid features; sign-in email is capped at
100 a day, sign-in SMS at 10 a day, push campaigns at 3 a day, direct pushes
at 100 a day, function invocations at 10,000 a day with 50 ms of CPU each,
function schedules at 2
(hourly or slower), queue messages at 1,000 a day, and new tracked links at
50 a day. Hosted sites serve with a "Built with Amba · preview" bar and
noindex; HTML, SVG, XML, and scripts on the media CDN download as attachments; HTML documents from functions serve sandboxed. A refusal returns402 FREE_TIER_LIMIT_REACHEDwithdetails.limitnaming the surface anddetails.upgradecarrying the one-call upgrade. Projects created before this release keep their previous behaviour. - Sign-in email on new free projects is capped at 25 a day (was 100). Magic
links and email OTP share one sending reputation across every project, so the
daily sign-in email allowance per free project is now 25. A refusal is the
same
402 FREE_TIER_LIMIT_REACHEDwithdetails.limitset toauth_emails_per_day; upgrading lifts the cap. - Unclaimed sandbox accounts follow a timeline. Accounts an agent created
with
amba_developer_signupand nobody has claimed get a claim notice on every admin response and MCP result once a free project reaches day 7 of its own clock. The project goes on hold on day 14 (reads work, writes return403 SANDBOX_CLAIM_REQUIRED) and is archived on day 90, after a full 76-day hold. Archiving deletes nothing. Each project's clock starts when it was created or became subject to the timeline, so a new project in an old account gets the whole window. Claiming lifts all of it in one request. See Unclaimed accounts. - An account is unclaimed until its owner proves the inbox, whatever its
address. The claim link, signing in with an emailed code, and GitHub
sign-in with a verified primary email prove it; the
verify_tokenthat signup returns does not. Free projects of any unclaimed account follow the timeline above, and an account that already uses its owner's real address claims that same address withamba_developer_claim. Accounts that proved their inbox, sign in with an emailed code, or own a paid or comped project before this release count as proven. - New projects for unclaimed accounts draw on a small daily capacity
slice. When it is spent, creating a project returns
503 PLATFORM_CAPACITYwithdetails.pool: "unproven"and the claim call, and signup still creates the account, returningproject_unavailablein place ofproject. Claiming moves the account to the main pool. - Free media storage is capped at 250 MB (was 100 MB, and not enforced).
On new free projects every upload registration takes
size_bytes, the upload URL accepts at most that many bytes, and an upload past the budget returns402 FREE_TIER_LIMIT_REACHEDwithdetails.limitset tomedia_storage.GET .../billing/statusreports real media usage on every tier. On a free project a raw-keyPUTpresign returns400 PRESIGN_PUT_NOT_AVAILABLE(register the upload instead), and deleting an asset within about 11 minutes of registering it returns409 UPLOAD_WINDOW_OPENwithRetry-After, because its upload URL could still recreate it. Deleting media removes the stored file before it frees space. - Hosted sites on new free projects hold 50 MB. One deploy carries at most
50 MB and 1,000 files, and every site plus its kept deployments share 50 MB;
older deployments are dropped to make room. A refusal is
402 FREE_TIER_LIMIT_REACHEDwithdetails.limitset tosite_storageorsite_files. - The two-free-projects limit covers app-factory provisioning and needs a
paid subscription to lift.
POST /v1/admin/provisioncounts each new child against its owner's free projects unless the developer or the parent org pays or is comped, and an unpaid checkout no longer lifts the limit on any path. - Account emails run on daily budgets. Claim links, sign-in codes, and
invites share durable daily budgets per account, per address, and
platform-wide (
429 EMAIL_BUDGET_EXCEEDEDwithRetry-After). An address gets at most one claim email per 10 minutes (429 CLAIM_EMAIL_RECENTLY_SENT; the earlier link keeps working), and a new claim link replaces the account's older ones.
New
amba_developer_claimMCP tool. Hands an agent-created account to its human owner in one call; the owner clicks the emailed link. A sandbox that already redeemed its signupverify_tokencan now be claimed too.
Fixed
- An inline
patworks on the hosted MCP server with no Authorization header. Afteramba_developer_signup, an agent passing the returned PAT as thepatargument now keeps working in the same session; the call previously answered401 MISSING_AUTHORIZATIONwhen the MCP client sent no Authorization header. - MCP tool errors carry the API's
codeanddetails. A failed call's result now ends with the JSON error{ status, error: { code, message, details } }after the readableAPI error <status>: <message>line it always had, so a402 FREE_TIER_LIMIT_REACHEDreaches the agent with the limit, the reset date, the billing URL and the one-call upgrade. A call with no credential returnsMISSING_PATas an error result too. See MCP tool errors. - Tracked-link destinations and the deep-link fallback URL refuse script,
data, and file URLs (
400 INVALID_DESTINATION_URL/INVALID_FALLBACK_URL). http(s) URLs and your app's own URL scheme are accepted as before. - SDK errors expose
detailsas a plain object. In@layers/amba-web4.0.10,@layers/amba-node4.1.2 and@layers/amba-react4.0.4,err.detailson an API error reads by property (err.details.limit,err.details.upgrade,err.details.opIndex) andJSON.stringifyprints it. Earlier releases of those three SDKs returnedundefinedfor those reads;err.code,err.statusanderr.messagewere always correct. The React Native, Expo and native SDKs already returned plain objects.
2026-10-02
New
- Invite-only developer signups. Amba is reopening new-account signups in
steps. While signups are invite-only, a new account needs an invite code
(
AMBA-followed by four groups of four letters and digits, such asAMBA-7KQ2-M9XD-4TNB-HR3W; case, spaces, and dashes are ignored). Pass it asinvite_codeonPOST /v1/auth/developer/signup, as theinvite_codeargument onamba_developer_signupandamba_affiliate_signup, withnpx @layers/amba init --invite <code>(orAMBA_INVITE_CODE), or in the console signup form, which the invite email's link fills in. Refusals are403 INVITE_REQUIRED,403 INVITE_INVALID, and403 SIGNUPS_CLOSED, each withdetails.request_access_url. An account created with a code can create projects right away. Existing accounts are unaffected: login, refresh, PAT rotation, and existing projects keep working. See Getting access. - Request access.
POST /v1/auth/developer/waitlist(email, optionalnote) and the console page at app.amba.dev/request-access put you on the list. When access is granted, the invite email carries the code, a prefilled signup link, and a one-line prompt to paste into your coding agent. - Redeem an invite on an existing account.
POST /v1/auth/developer/redeem-invitelets an account that already exists use an invite code to create projects while access is limited. - Free-tier database limits. Free projects run on 0.25 compute units with
10 CU-hours of database compute per billing month (about 40 hours awake) and
a 512 MB database. At the compute cap the database pauses until the period
resets on the 1st (UTC); at the storage cap, writes that add data are
refused while reads and deletes keep working. Both return
402 FREE_TIER_LIMIT_REACHEDwith the limit, the reset date, and anupgradefield carrying the one-call upgrade. The owner gets an email at 80% of the compute allowance. See Database limits. Paid projects and projects that existed before this release are unaffected. - Database usage in billing status.
GET .../billing/status(andamba_billing_status) gains adatabaseblock with compute CU-hours and storage MB against the caps, the reset date, andlimit_reached.amba billing statusand the console billing page show the same numbers. amba_billing_upgradeMCP tool. Starts a Pro or Scale checkout in one call and returns the link for the project owner to open.
Fixed
- Sites serve directory-style links. A request to
/start/now servesstart/index.html(it previously returned400 invalid_path), and/startservesstart.htmlor redirects (308, query string kept) to/start/when the page lives atstart/index.html(it previously rendered the home page). Static exports from Astro, Hugo, Next.js, and similar generators work with their default link styles, including dotted slugs (/blog/release-2.0). Exact file paths resolve as before. See Sites routing. - File names with spaces or non-ASCII characters work end to end. Site
deploys that include a non-ASCII file name (
café.html) previously failed withSTORAGE_WRITE_FAILED; they now upload, and a request for/caf%C3%A9servescafé.html./my%20report.pdfservesmy report.pdf. - Custom 404 pages. A site that ships a top-level
404.htmlnow serves it, with status404, for every path that doesn't resolve. Sites without one keep the single-page-app fallback toindex.html. - The single-page-app fallback is no longer cached for an hour. Deep links
that fall back to
index.htmlare sent with the same no-cache policy as every other HTML page, so a new deployment shows up on the next request.
Changed
- Background evaluation runs when there is something to evaluate. XP rules,
segments, and leaderboards still pick up new data on their usual cycle (XP
within 2 minutes, segments and leaderboards within 15). An app with no new
data is now left idle between cycles. Segment rules with a
within/not_withinwindow re-evaluate when the next user crosses the window's boundary: within 15 minutes for windows under a day, within the hour for longer ones. Daily, weekly, and monthly leaderboards roll over at UTC midnight as before. See Evaluation cadence. - Engagement events record when Amba received them. New events carry a
received_attimestamp next tooccurred_at(which can be the client's time). It appears in the user data bundle and inGET /users/:id/events. A daily XP pass uses it to find events no evaluation run picked up and grants their XP once, checked against the daily limit of the event's own day and the cooldown around its own time, and dated at the event. An event an evaluation run already held back on a daily limit or cooldown stays held. A grant healed after its period closed adds to the user's total XP and appears in their XP history at the event's time; daily, weekly, and monthly leaderboards for that closed period are not recomputed. - One developer account per mailbox. Developer signup, GitHub sign-up and
amba claimnow treat aliases of the same mailbox as one address: case is ignored, any+tagis removed, and for Gmail the dots in the local part are ignored (j.doe+work@gmail.comandjdoe@gmail.comare the same mailbox). Binding an alias of an address that already owns an account returns409 EMAIL_EXISTS. The address you typed is still the one we email. - Disposable inboxes are refused. The same paths return
400 DISPOSABLE_EMAILfor addresses on temporary-inbox domains. - Failed password attempts are capped per account. After 20 failed
password sign-ins in an hour (100 in a day) for one account, password
sign-in for that account returns
429 RATE_LIMIT_EXCEEDEDwithRetry-Afteruntil the window passes. The count coversamba login, the console and the MCP connector consent page together, and only failed attempts count. Sign-in with an emailed code keeps working.
Improved
- Clearer reserved secret names. Secret names starting with
AMBA_orEDGE_stay reserved for values the platform injects into every function. TheRESERVED_BINDINGerror,amba secrets set, and theamba_secrets_setMCP tool now list the injected values (AMBA_PROJECT_ID,AMBA_API_URL,AMBA_INTERNAL_TOKEN,AMBA_AI_GATEWAY_URL) and suggest a usable name, for exampleAPP_CLIENT_KEYin place ofAMBA_CLIENT_KEY. See Reserved names.
2026-09-07
Fixed
- Vision prompts accept every common image spelling. The client AI surface
now accepts
{ type: 'image', source: { type: 'base64', media_type, data } }(and theurlsource),{ type: 'image_url', image_url: { url } }, themedia_type/mime_typesynonyms formime, anddata:URLs, alongside the vendor-neutral{ type: 'image', url | data, mime }block. Previously the provider-native spellings were dropped before the model saw the image. - Rolling model aliases are recognised as vision-capable. Prompts
registered on
gemini-flash-latest,gemini-flash-lite-latest, orgemini-pro-latestno longer return422 ai_model_not_multimodalfor image input, and they are priced at the current generation's rate so per-prompt budgets bind.
New
- Passwordless developer sign-in by emailed code.
POST /auth/developer/otp/requestemails a 6-digit code to an existing account andPOST /auth/developer/otp/verifyredeems it for the normal session, so an account that never set a password (for example one created by an agent and later bound to your address) can sign in on the console and withamba login. Codes expire in 10 minutes, are single-use, and lock after 5 wrong attempts; redeeming one marks the account verified. The console sign-in page gained an Email me a sign-in code option. - Per-call
systemon registered prompts. A caller may passsystemalongsideprompt_slug; it is merged after the prompt's saved system prompt (the saved text stays first). A non-string value is rejected with400instead of being ignored. thinking_budgetfor reasoning models. Bound (or disable with0) the reasoning tokens that would otherwise be spent out ofmax_tokenson models that think before answering.
2026-08-26
New
- Gemini 3.5 Transcribe in the managed AI gateway. The OpenAI-compatible
/audio/transcriptionsroute now acceptsgemini-3.5-transcribe, uploads the file through Google's resumable Files API, waits for it to becomeACTIVE, invokes the unary Interactions API, and cleans up the temporary file on every terminal path. Automatic language detection, language hints, custom vocabulary, smart/verbatim modes, and diarized word timestamps are supported where Google allows them.verbose_jsonnormalizes WordInfo into backward-compatible speaker segments and words. Cost is non-null and calculated from Google's returned modality token usage at its published $2/M audio-input and $12/M text-output rates; no estimated per-minute charge is used. The live WebSocket model is not part of this release. - Gemini and Mistral key registration in the CLI.
amba ai providers addaccepts all four gateway providers, matching the admin API and internal key callback.
2026-08-25
New
- Function deploy readiness and controlled credential forwarding. CLI
4.1.5addsamba functions deploy-statusandamba functions deploy --forward-authorization. The latter is an explicit, per-deployment opt-in: only a bearer from a verified client request can reach customer code; API keys and internal trigger credentials remain stripped. - OpenAI-compatible image edits and variations. The AI gateway now routes
/images/editsand/images/variations, supports their multipart payloads, and metersgpt-image-2including its dated snapshot.
Fixed
- Deleted functions stop serving immediately. Function deletion now removes the deterministic schedule, disables the active deployment, and publishes a persistent edge tombstone before provider cleanup. Deploy, promote, and disable reconcile that edge state immediately, including safe cache invalidation when a direct write fails.
- App-MCP group scope and errors.
X-Group-Contextreaches generated collection and function tools, and non-2xx upstream responses are returned as MCP tool errors instead of successful results. - Function log visibility. CLI
4.1.5prints successful invocations with no console output as[outcome] ok; docs now state the provider's batched ingestion window. - Truthful custom-hostname status. Site and function-domain reads expose raw
provider ownership/certificate state and refresh it during list operations;
unknown non-empty certificate states fail visibly instead of collapsing to
pending. A five-minute background reconciler also re-fetches pending hostnames and publishes newly issued or rotated certificate-validation TXT values for domains whose DNS Amba manages, so activation does not depend on a customer polling the API.
2026-06-20
New
- Team-owned collections (group/org ownership). A collection can now be
owned by a group instead of a single user. Create it with
owner_scope: "group"plusread_policy: "member"/write_policy: "member:admin"(onamba_collections_create/_alteror the admin API): any member of the owning group reads, and members at the given role write — enforced server-side from the group's membership (owner>admin>member>viewer). Act within a group viaamba.asUser(uid).asGroup(gid)(Node SDK) or theX-Group-Contextheader; a non-member is rejected with403, and inserts must target a private group. Additive and backward-compatible — existing collections stay strictly per-user. See team-owned collections.
2026-06-13
New
-
Monetization apply creates Google Play products.
amba monetization applynow creates one-time products directly in Google Play — not just App Store Connect. Declarestore_metadata.push_to_store: trueon aplay_storeone-time product (one_time|consumable|non_consumable) withplay_price_micros(integer micros) andplay_currency(ISO code), connect agoogle_playintegration (a Play Console service account with the Android Publisher permission), and apply creates the managed product and sets its price in one step. The credential is validated when you connect it, store catalog writes still require an explicit confirm, and Play subscriptions stay on the human-floor checklist (create those in Play Console; apply registers them). See the monetization quickstart. -
amba ship— take your Expo app live. The CLI (@layers/amba4.1.0) now carriesamba ship: one config (amba.ship.json) and one command take a code-complete Expo app from build to live on the App Store and Google Play. Six idempotent phases — preflight, monetization, build, submit, metadata, release — with per-app state so a re-run resumes where it stopped. Every step no API can do (creating the store app record, the Apple Paid Apps agreement, screenshots, store review) is detected and surfaced with the exact console action, never pretended-automated.amba ship initscaffolds the config;--dry-runprints the whole plan and changes nothing;--phaseruns a single phase. Whenmonetization.enabledis set,shipverifies your in-app purchase catalog against the monetization control plane before building.
2026-06-12
New
-
Byte-range requests on the CDN. Asset responses from
{projectId}.cdn.amba.hostnow advertiseAccept-Ranges: bytes, and a singleRange: bytes=...request on an original asset returns206 Partial Contentwith a correctContent-Range— audio and video players can seek into large files without re-downloading the whole object. Suffix (bytes=-500) and open-ended (bytes=1024-) forms are supported; a range starting past the end of the file returns416withContent-Range: bytes */<size>; multi-range requests are answered with the full200body. Range headers on transformed image URLs are ignored (full response). The new Media Serving & CDN page documents the serving domain, cache lifetimes, image transform parameters, range behavior, and the cache-freshness model. -
Integrations promote with your project. Promotion bundles now include an
integrationssection: each active integration's non-secret config (push bundle id, team/key ids, environment) exports with credential material stripped and an explicitcredentials: "stripped"marker — bundles stay safe to commit and diff. On import, integrations land in a clearly signalledpending_credentialsstate (inactive, with per-provider guidance on exactly which call uploads the credentials), or — when the source and target projects belong to the same developer — passinclude_integration_credentials: trueand the stored credentials are copied directly between the projects' credential stores, entirely server-side, activating push on the target as part of the same import call. No more hand-reconfiguring APNs/FCM after every dev → prod promotion. See the promotion API reference for the new request field, response statuses, and error codes. -
Generated SDK platform-parity matrix. The SDK platform parity page is now generated directly from the SDK method catalog and the SDK sources — one method-level matrix (136 methods × 34 namespaces × 9 platforms) showing exactly which methods ship on Web, Node, React, React Native, Expo, iOS, Android, Flutter, and Unity. Because the page is generated from the same source of truth the SDKs are built from, it can't drift from what the SDKs actually ship.
Improved
- Push payload contract, documented field by field. The new
payload contract page shows the literal
on-device payload for both platforms — the APNs envelope (your
datakeys top-level alongsideaps,alerttitle/body,mutable-content: 1) and the FCM message (notificationtitle/body, high delivery priority, and thedatamap where every value arrives as a string — numbers, booleans, and objects are JSON-stringified, so parse them in typed Android handlers). It also states the stability guarantee explicitly: Amba injects nothing into yourdata— no campaign or segment identifiers (attribution is recorded server-side per delivery) — so tap handlers can rely on your own keys exclusively. No more reverse-engineering test pushes with console logs. - INSERT vs PATCH body shapes, stated as a contract. The
admin rows reference
now documents the deliberate shape split side by side: insert takes a flat
column → value map; single-row PATCH canonically takes
{ "set": { … } }(with the flat body accepted as an alias); bulk PATCH requires thesetwrapper because it disambiguates the update payload from the siblingwherefilter — and a wrong-shape body returns a pointed400naming the top-level keys you sent. Also corrected: bulk update rejects an emptywhere: {}(WHERE_REQUIRED) rather than matching every row, and the admin single-row PATCH'sexpectedcompare-and-set is now documented. return=minimalresponse shapes. The insert reference now shows the id-only response vs the full-row default for both the single insert androws:batch— pass?return=minimal(orPrefer: return=minimal) on agent-driven bulk migrations so large JSONB rows don't blow the token budget on echo.- Content library delete. The
library DELETE reference
now spells out the safety contract: deleting a non-empty library is refused
with
409 LIBRARY_NOT_EMPTYunless you pass?cascade=true, which atomically deletes the library and all its items, schedules, and deliveries — irreversibly. - Catalog-data docs: collections vs content libraries, search, and array
columns. The Content Libraries and
Collections pages now open with reciprocal
guidance on the global-catalog vs per-user decision — including the trap
where admin-seeded rows in a default (
read_policy: "owner") collection are invisible to end users, and theread_policy: "public"fix. The collections feature guide and client API reference now fully document typo-tolerant text search (search: { q, columns, fuzzy, threshold }— word-similarity ranking, default threshold0.3), thelike/ilikeoperators, when to reach for search vs find-nearest, native array column types (text[],integer[],uuid[], …) with thecontains/containedBy/overlapsset operators, and arrays-vs-jsonbguidance. The Migrating from Postgres cookbook gained explicit mappings forILIKE/full-text habits → thesearchparameter, array columns → native array types (notjsonb), and triggers → event-webhook-driven functions.
2026-06-10
New
- Filtered, projected, and nested eager-loading on
find. Anincludeclause can now carry its ownwhere(filter the related rows),select(project their columns), andinclude(load relations OF the related rows — novels → chapters → comments, up to 3 levels, one batched query per relation). The included collection's read policy and soft-delete filter still apply independently. Works on the client find (auto-scoped) and the admin row reads. See Eager-loading. having,order_by, andlimiton group-by aggregation. Filter the grouped result post-aggregation (having: [{ fn: "count", op: "gte", value: 3 }]), order groups by any group column or select alias, and cap the number of groups — "top 5 statuses by revenue" is now one call on both the client and admin aggregate endpoints.- Tail function logs from a request id.
amba functions logs <name> --since-request-id r_...starts the log range at the oldest event mentioning that request id and shows everything from there onward — the "show me what happened after this request" debugging flow. Composes with--request-id(filter) and--tail(anchor the backfill, then stream). Also available on the logs API (?since_request_id=) and theamba_functions_get_logsMCP tool.
Improved
-
Monetization & payments docs, zero to paid. New Monetization Quickstart walks the whole journey: declare entitlements / products / offerings in
amba.config.json, review the plan (including the human-floor checklist of store-side steps no API performs, with exact console locations), confirm and apply the durable operation, provision the store products, wire the RevenueCat webhook, gate features withAmba.entitlements.has(), and cascade subscribers into segments, push, XP, and currency rewards. The Environment Promotion page now documents themonetizationbundle section, the RevenueCat integration page documents the canonicalsecret_api_key/secret_api_key_writeconfig fields, and the Entitlements page's SDK snippets were corrected to the real method names (entitlements.has()/entitlements.list()). -
Storage bucket
bucket_pathfield. Bucket admin responses (/admin/projects/:projectId/storage/buckets) now returnbucket_path— the URL path segment assets are served under. The oldr2_bucket_pathfield is deprecated: it's still accepted on create and still emitted in responses, but it's an alias ofbucket_pathand will be removed in a future major. Switch reads and writes tobucket_path.
Fixed
includenow reaches the wire from every SDK. The shared engine dropped thefindoption'sincludeclauses before sending the request, so eager-loading silently returned bare rows on Web, Node, and React Native. The option now passes through verbatim, andFindOptionsgained the matchingincludefield on iOS, Android, and React Native — parity with Web/Node. Ships in the next SDK patch release.
2026-06-09
New
- Store-product provisioning through the monetization apply. Declared
products that don't exist upstream are now created by apply: each is
registered with RevenueCat (attached to the right app automatically), and App
Store products that declare
store_metadata.push_to_store(plusduration+subscription_group_namefor subscriptions) are created in App Store Connect through RevenueCat's store connection — one apply provisions entitlements, offerings, packages, paywall drafts, AND the store products they sell. Store catalog writes are held to a stricter gate than other gated ops: they never run off an up-frontconfirm— the apply waits until you explicitly approve the started run (re-call apply withconfirm, oramba monetization apply --confirm). The plan now returns ahuman_floorchecklist for what no API can do — pricing, agreements/banking/tax, app review, and store SKUs outside the App Store — each with the exact console location (your declaredstore_metadata.priceis echoed into the checklist). Store products join the three-way diff like every managed object: display-name updates apply automatically, removals archive (soft — the store product is untouched), and out-of-band edits surface as drift. Blocking gaps (a product referenced but never declared, or a store with no connected RevenueCat app) still fail fast with422 MONETIZATION_STORE_FLOORand the exact fix. - Your app's MCP: exposure allowlist, function tools, and surface config.
The per-app agent surface (Your app's MCP) now exposes
collections and deployed functions behind an explicit allowlist —
nothing is exposed by default; opt collections/functions in via
PUT /admin/projects/:id/app-mcp/exposure(or theamba_app_mcp_set_exposureMCP tool). Exposed deployed functions become callable tools (<app>_fn_<name>) that invoke through the same execution path as every other caller — and they're fully typed when the function declaresmcp: { description, input_schema }in its deploy metadata (supported schema subset validated at deploy time; promote restores the annotation that shipped with a version). New per-app surface config (GET/PATCH .../app-mcp/config,amba_app_mcp_get_config/amba_app_mcp_update_config): anenabledkill switch, anauth_moderestriction (all/client_only/developer_only), and a hostnameslugthat reserveshttps://<slug>.mcp.amba.host/mcpfor your app — hostname availability is rolling out, and the path-addressed endpoint keeps working unchanged either way. - Spend ceilings now enforce, and usage is fully visible. The ceiling you
set with
amba_billing_set_ceiling(orPUT …/billing/ceiling) is no longer advisory: when the current period's overage cost reaches it, the project degrades to read-only mode — metered writes (event tracking, collection inserts/updates, media uploads, push sends, new signups) return402with a machine-readable payload (code, current usage, ceiling, reset date) while reads and deletes keep working. Tier quotas enforce through the same path onthrottlemode (402 TIER_QUOTA_EXCEEDED). Two control-plane webhook events —billing.ceiling_warning(80%) andbilling.ceiling_reached(100%) — fire once per billing period so you can alert before the flip.GET …/billing/status(andamba_billing_status,amba billing status) now returns current-period usage and cost by meter, the billing period and reset date, percent of ceiling consumed, a projected end-of-period overage, and the live enforcement state; the console gains a per-project Usage page showing the same. See Billing & Usage. - Web subscriptions. Sell on the web through your own Stripe Billing
account — the path RevenueCat documents for web checkout — and web purchases
grant the same Amba entitlements as mobile ones: same
has()check, same auto-reward cascade, same segments and webhooks. Connect withamba_integrations_configure({ provider: "stripe_billing", … }), map your Stripe price to an Amba product (store_product_refs.web), and passamba_user_idin the checkout metadata. Ingest is signature-verified, idempotent, and order-safe. See Web Subscriptions. - Durable monetization apply.
POST /admin/projects/:id/monetization/apply(andamba monetization apply/amba_monetization_apply) now starts a durable, pollable apply instead of writing in-request: the response carries anoperation_idto poll viaGET /operations/:operationId(the CLI polls for you). Writes are paced against RevenueCat's API rate limits and carry per-step idempotency keys, so a large plan applies safely over several minutes and an interrupted apply resumes without double-creating anything. Destructive changes now wait for confirmation: apply withoutconfirmreportsconfirm_required: trueand holds the gated ops (up to 24h) until you re-call apply withconfirm=plan_hash; a confirmation that no longer matches the plan is rejected with nothing written. One apply runs at a time per project (409 MONETIZATION_APPLY_IN_PROGRESScarries the in-flightoperation_id). Migration note: the apply endpoint now returns202with{ operation_id, status, confirm_required, gated, refused }— poll the operation for the finalapplied/skipped_existing/gated_appliedcounts that previously came back inline, and confirm-required / invariant outcomes now settle the operation instead of failing the request. - Your app's MCP: end-user scope, vector search, faster connects, metering.
The per-app agent surface (Your app's MCP) now serves two
credential modes: the existing developer scope, and a new end-user
scope (
X-Api-Key+ the user's session token) whose tools run through the same enforcement path as the client SDK — collection access policies hold with no bypass, so an in-app assistant only ever sees the signed-in user's data. Collections with a vector column automatically gain a<collection>_find_nearestsimilarity-search tool. The tool surface is now built from a per-app manifest cached with explicit invalidation on schema changes (connects are faster; a schema change still shows on the nexttools/list). Usage is metered: tool calls appear as the new agent tool calls axis inamba_billing_status,amba billing status, and the console billing page. - Cursor pagination from page one. Collection list/find responses (REST
and the app-MCP tools) now return
next_cursoron the first page whenever the query has a single-columnorder— previously only continuation pages carried it, so there was no way to start a cursor chain. The app-MCPlist/findtools default tocreated_at descso agents paginate out of the box. - Agent build reference. An agent builds an app walks the entire journey — account, project, data model, SDK install, identity, XP + streaks, push, live updates, ship checklist — with every MCP tool call and SDK method taken from the live surface. Executable top to bottom by a coding agent.
- Capability Index. /capabilities maps every shipped surface — identity, engagement, gamification, economy & monetization, social, analytics, infrastructure, and the agent/developer surface — on one scannable page with one-liners and links.
- Generated OpenAPI reference. The full API surface is now published as a
machine-readable OpenAPI 3.1 document, generated directly from the live route
registrations — fetch it at
https://api.amba.dev/openapi.json(no auth, CORS-open) or browse the new endpoint index with per-endpoint auth requirements. - Live updates are generally available for shipped apps. Long-lived
subscriptions now connect to a dedicated realtime host
(
realtime.amba.host) with no request-duration ceiling, soAmba.collection().subscribe(), conversation subscriptions, andAmba.gamification.subscribe()stay open as long as your app does. The SDK routes there automatically when pointed at the default production API; a customapiUrlkeeps live updates on that same origin. Ships in@layers/amba-web4.0.6,@layers/amba-node4.0.7, and@layers/amba-react-native4.0.5 (Expo inherits via React Native). See Live Updates.
Improved
- React Native: offerings + Restore Purchases on npm.
Amba.offerings()andAmba.entitlements.restore()(with theOffering*/RestoreResulttypes) are now published in@layers/amba-react-native4.0.5 — parity with Web and Node. - SDK + tooling patch wave.
@layers/ambaCLI 4.0.4 (monetization plan/apply/adopt, payments setup/balance/payouts —amba --versionnow reports the real version),@layers/amba-mcp4.0.6 (monetization, payments, auto-MCP collection tools, AI transcription + vision, media catalogs, translations),@layers/amba-shared4.0.4, and@layers/amba-core4.0.6 pick up everything shipped on the platform since their last publish.
Fixed
- Collection row updates now merge JSON object columns (merge-patch).
PATCHon a collection row used to replace ajsonbcolumn wholesale, so two devices each updating their own key clobbered each other. Object values targetingjsonbcolumns now merge into the stored object — shallow, atomic, on every update path (single-row, bulk,update_many, and transactionupdateops, client and admin alike). Arrays, scalars andnullstill replace the column value. Need the old overwrite behavior? Pass?objects=replace(orobjects: "replace"on a transaction op / theamba_*_update_rowMCP tools). Matches the merge semantics userpropertiesalready had. See the client API reference.
2026-06-07
New
-
Monetization gated destructive apply.
amba monetization applycan now make destructive changes — detaching a product, archiving an entitlement or offering, and removing a package — behind an explicit confirmation. They run only when you re-apply with--confirm(orconfirmset to theplan_hash); without it they're reported but not executed. Confirmed applies enforce an ordering invariant before any write: Amba never archives the offering customers currently see (RevenueCat has no API to move the default pointer — do it in the dashboard) and never detaches the last product a live customer resolves unless you pass--allow-detach-live. New--reconcile amba|adoptcontrols how out-of-band drift is reconciled. If you configure a sandbox RevenueCat project, destructive changes are tried there first (with a synthetic purchase→entitlement-resolve check) before production. Setting the default offering and publishing a paywall live remain dashboard steps (no RevenueCat API) and are surfaced with the exact instructions. -
Subscription products, offerings & a paywall read. Declare a provider-neutral product catalog Amba owns: define an entitlement, declare the products that unlock it (with per-store identifiers), and group them into an offering your paywall renders. New client read
Amba.offerings()returns the offering + packages + the product behind each, in a neutral shape. Configure it agentically (amba_entitlements_define,amba_products_create,amba_entitlements_map_product,amba_offerings_create/amba_offerings_list) or via/admin/projects/:projectId/subscriptions/*. Any grant carrying aproduct_id— a store webhook or a server grant — now resolves to the mapped entitlement automatically, so every purchase path lands on the same entitlement set. -
Restore Purchases. New
Amba.entitlements.restore()(web / node / RN / Expo; iOS + Android in SDK 4.1.0) andPOST /client/entitlements/restorere-sync the user's owned entitlements from the configured subscription service and return the active set — the App Store 3.1.1 requirement. Fail-closed: a transport failure leaves access exactly as it was, never a silent unlock or revoke. -
Reward bundles on subscribe. Entitlement grants now emit neutral
entitlement.granted/subscription.started/subscription.renewedevents. Bind a currency/XP grant rule (or any event rule) to them to auto-reward new subscribers — grant currency, move to a segment, fire a push — with no glue. Idempotent: a redelivered webhook or repeat grant rewards at most once per billing period. -
Amba Payments — accept payments in your app. Your app can now collect payments from its users with Amba as the rail: your app is the seller, Amba takes a configurable platform fee, and the money settles to your own connected account. Onboard in one flow —
amba payments setup(or theamba_payments_account_create+amba_payments_create_onboarding_linkMCP tools) creates your account and returns a hosted setup link you complete once. Then create charges server-side withapplication_fee_amount, and read your balance and payouts withamba payments balance/payouts. The charge returns aclient_secretyour app completes on-device with the platform's standard payment sheet (any Stripe-compatible client SDK). See Payments. Availability is enabled per platform; checkamba payments statusif a call reports payments aren't enabled yet. -
Metered overage collection. When a project's spend mode is
overage_bill, usage past your included quotas is now tallied into a per-period usage ledger and reconciled daily onto your upcoming invoice — so overage genuinely rolls into your next invoice instead of being served for free.GET .../billing/statusgained three honest figures:recorded_overage_usd_this_period(tallied),billed_overage_usd_this_period(actually added to Stripe), andmetered_billing_live(whether charging is enabled yet). Set a spend ceiling for hard429throttling instead of accruing charges. Metered charging is rolling out behind a flag — until it's switched on, usage is tracked and visible but nothing is billed. -
Monetization apply (write to RevenueCat).
amba monetization apply(andamba_monetization_apply,POST /admin/projects/:id/monetization/apply) now pushes your declared subscription config to RevenueCat. Additive ops (create entitlements, offerings, packages, and paywall drafts; attach products) apply automatically. Pass theplan_hashfrom a freshplan; apply refuses if RevenueCat drifted since then, hard-fails if a referenced store product isn't registered yet, reads back every write to confirm it landed, and stops on the first failure without rolling back (the additive half-state still resolves your current offering).plannow returns aplan_hash. Apply needs a read-write RevenueCat key — set it assecret_api_key_writeon the integration, or use one full-access key for both read and write.
Changed
storevalue"stripe"→"web". The entitlementstorefield's vendor-named"stripe"value is now the neutral"web"(covering any non-native checkout). Existing rows are migrated automatically and the server still accepts"stripe"on write (folding it to"web"), so no client change is required — but new code and SDK types use"web".
2026-06-06
New
- Your app's MCP (preview). Every collection in your app is now automatically
a set of typed agent tools at your app's own MCP endpoint —
https://mcp.amba.dev/app/<project-id>/mcp. Create arecipescollection and the endpoint immediately exposesrecipes_list / get / find / insert / update / delete / aggregate, with a typed, per-columnwhereschema so an agent queries your data without writing query strings by hand. Tools are generated live from your schema at connect time — add a collection and the nexttools/listreflects it. Admin-scoped today; end-user scope, per-collection access policies, and typed tools for deployed functions are coming. See Your app's MCP. - Live updates on React Native & Expo.
Amba.collection(name).subscribe(),Amba.messaging.conversation(id).subscribe(), andAmba.gamification.subscribe()now ship in@layers/amba-react-nativeand@layers/amba-expowith the exact same surface and change shape as Web and Node. On mobile the live connection rides the platform's native networking, so it works in release builds with no extra setup or peer dependency, and reconnects with backoff just like Web. See Live Updates. - Audio transcription via the managed AI proxy. The AI surface now proxies speech-to-text alongside image generation and text-to-speech: upload an audio file and get a transcript back, with your provider key kept server-side and per-call usage metered per audio minute. Same managed pattern as the other media endpoints.
- Google Gemini in named prompts. Register a prompt against
geminiand invoke it by name like any other provider — amba builds Gemini's distinct request shape for you (system instruction + multi-turn content + variables), including multimodal image input, and parses the response back to the common result. Previously Gemini was reachable only through the raw passthrough. - Monetization control plane (preview). Manage your subscription config —
entitlements, products, offerings, packages, paywalls — as Infrastructure-as-Code
on top of RevenueCat.
amba monetization planpreviews the three-way diff (declared vs adopted baseline vs live) with a store-floor preflight and drift;driftsurfaces out-of-band dashboard edits (a daily background sweep records them automatically);exportsnapshots the live config to a declarative bundle;adoptrecords the live config as your managed baseline. Also wired into the promotion bundle and exposed as MCP tools (amba_monetization_*). Phase 1 is read-only against RevenueCat — plan/drift/export/adopt never write to RevenueCat (adopt writes only Amba's own state). Pushing declared changes back to RevenueCat ("apply") is coming in a later phase. - Automatic offline queue. Turn on
Amba.offline.enable()and a collection write ortrack()that fails because the device is offline is buffered to a durable on-device outbox instead of throwing — then replayed in order on reconnect. Replays are idempotent, so a write that reached the server before the connection dropped is never applied twice. Web auto-flushes on the browseronlineevent; React Native drivesflush()from your connectivity listener. Opt-in and additive. Web + React Native + Expo + Node SDKs. - Optimistic update helpers. New framework-agnostic
withOptimistic({ apply, mutate, rollback })applies a local change immediately, fires the real write, and rolls the local change back automatically if it fails — plusAmba.collections.optimisticInsert / optimisticUpdate / optimisticDeleteconvenience helpers for the common single-write case. - Push-token auto-rotation. Re-register the device push token whenever the
OS rotates it (reinstall, restore, key roll) so notifications keep arriving.
Web listens for the service-worker
pushsubscriptionchange; React Native wraps your token-refresh listener; Expo's one-callAmba.enablePushAutoRotation()wiresexpo-notificationsfor you. Re-registration is idempotent.
See the Offline & resilience guide.
- Realtime gamification. Subscribe to a user's live XP, level, achievement,
and streak changes over one stream — no polling.
Amba.gamification.subscribe((change) => { … })on the web and Node SDKs delivers each change (with itskindand new state) the moment it happens, and only ever surfaces the signed-in user's own updates. Backed byGET /v1/client/realtime/gamification. See Realtime. - Multi-collection transactions. Commit writes across several collections as
one atomic batch — the whole set applies, or none of it does.
Amba.collections.transaction([ … ])(web + Node) andPOST /v1/client/collections/transactiontake an ordered list of insert / update / delete ops, each with optional compare-and-set and upsert. See Collections (SDK). - Related rows in one query. Add
includeto a collection read to eager-load related rows in a single round-trip instead of an N+1 fan-out. See Collections (Client API). - Per-language content. Attach translations to a content item and Amba
serves the reader's language automatically from
Accept-Language(BCP-47), falling back to the base item when there's no match. See Content (Client API). - Admin collection-row tools. The MCP toolset gained
amba_admin_update_row,amba_admin_delete_row,amba_admin_bulk_update, andamba_admin_bulk_delete, so an agent can manage row data end-to-end — not just insert and read.
Fixed
- Subscription webhooks are now idempotent and order-safe. Subscription events are delivered at-least-once and can arrive out of order; ingest now deduplicates each event and refuses to let an older event overwrite newer entitlement state. A redelivered or late event can no longer silently revoke an active subscriber.
- Refunds revoke access. A refund now marks the entitlement inactive
immediately (previously a refunded subscription could stay active). A
cancellation correctly keeps access until the period's
expiration_daterather than revoking on the spot. - Expiry-aware reads.
GET /client/entitlementsnow derivesis_activefrom the expiration date, so a lapsed subscription never reads as active even before its expiry event lands.?active_only=truefilters to currently-active grants server-side.
Changed
- Paywall engagement events are now recorded with neutral
paywall_<event>names (previouslysuperwall_<event>), and subscription user properties use neutralsubscription_*keys (previouslyrc_*). Existing segment/rule references to the old names keep working on data written before this change; update rules to the neutral names going forward.
2026-06-05
New
- AI vision (image input). Send images to a registered prompt running on a
vision-capable model. Pass
images(each{ url }or{ data, mime }) on the web + Node SDKai.*.createcall — or supplymessageswith content blocks ({ type: 'image', url | data, mime? }) directly — and Amba maps each image to the model's native format. Sending an image to a text-only model returns a clearai_model_not_multimodalerror instead of silently dropping it. Works for Anthropic, OpenAI, and Gemini vision models; theamba_ai_prompts_invokeMCP tool gained animagesparameter for testing. See AI proxy → Vision. - On-device local notifications. Schedule reminders that fire on the
device itself — no server round-trip — with
Amba.notifications.scheduleLocal({ title, body, trigger }), thencancelLocal(id)andlistScheduledLocal(). Triggers can be an absolute time ({ at: Date }) or a delay ({ inSeconds, repeats? }). Available on the React Native, Expo, and web SDKs; iOS and Android schedule fully (even while the app is closed), and the web SDK schedules within the browser's capabilities and reports its mode (scheduledVia). Distinct from remote push — use local for device-owned timing (streaks, focus timers), push for server-owned timing. See Local notifications. - Event dry-run / explain. Preview what an event would trigger before you
fire it.
Amba.events.explain(event, properties, { userId })(web + Node) evaluates the rule engine — XP rules, currency grants, feed rules, streaks, and webhook subscriptions — against a candidate event and returns the would-be effects without committing any side effect. Matched rules that a per-user daily limit, cooldown, or balance cap would suppress carry a note. Backed byPOST /v1/client/events/explain(and the admin equivalent) and the read-onlyamba_events_explainMCP tool. See Events → Explain. - AI cost visibility. Every buffered AI response now carries a
cost_usdfield — the dollar cost of the call, computed from token usage — so you can show per-call spend or sum it per user without a separate metrics call. Surfaced on the web + Node SDKAiMessageResponsetype. See AI proxy. - Per-prompt AI budgets. Cap a registered prompt's spend with a per-period
USD ceiling (
monthly,daily, or lifetimetotal). Once the period's spend reaches the budget, invocations are denied with a clearai_budget_exceedederror until the period resets — so a prompt you expose to your app can't run away with spend. Off by default (unlimited). Manage it via the new budget endpoints or theamba_ai_prompts_set_budget/amba_ai_prompts_get_spendMCP tools. - Media catalogs. Group media assets into a named, slug-addressable
catalog your app fetches in one call — instead of hard-coding asset URLs in
client code. Items stay in the order you set; each resolves to a stable,
long-cacheable public URL. Curate with the
amba_media_catalogs_*tools and read from your app withAmba.media.catalog(slug)(GET /v1/client/media/catalogs/:slug). See Media → Catalogs.
2026-06-04
New
- AI — structured JSON output & streaming. Force a registered prompt to
return parseable JSON with
response_format(json_objectorjson_schema), and opt into a Server-Sent Events stream withstream: true— usage stays metered server-side. See AI proxy and the new client AI reference. - Async operation handles. Side-effecting actions (starting with domain
purchase) hand back an
operation_idyou poll tosucceeded/failedinstead of guessing. See Operations. - Configurable XP level curve. Set
xp_per_levelor explicitlevel_thresholdsper project, and read the effective curve from the client. See XP and Levels. - Collection access policies (public catalogs).
read_policy: "public"lets every signed-in user read a collection;write_policy: "authenticated"opens writes. The default stays strict per-user. See Collections (Admin API). - Idempotent collection writes. Pass an
Idempotency-Keyheader (oridempotency_keybody field) on a collection insert and a retry can never create a duplicate — the replay returns the original row. - Scoped reset. Reset one user's state to zero
(
DELETE …/users/:id/reset, optionally?delete=true), or empty a single collection's rows (DELETE …/collections/:name/data) — instead of the all-or-nothing sandbox wipe. See Users (Admin API) and Collections (Admin API). - Seed a cohort.
POST …/users/cohortmints N anonymous users sharing properties (and optional segments) in one call. - Daily content batches. Set
batch_sizeon a content schedule to deliver N distinct items per tick (delivered as an array). See Content scheduling. - Message read receipts.
GET …/messages/:id/receiptsreturns who has seen a message for a "seen by N of M" UI. See Messaging (Client API). - SDK — collection upsert, deleteWhere, restore. The web and Node SDKs
gained typed
Amba.collections.upsert(),deleteWhere(), andrestore(). See Collections (SDK). - CLI — infrastructure as code.
amba export/amba diff/amba applymanage your project's reusable configuration declaratively (amba.config.json), andamba functions deploynow accepts a directory (with--entry). See the CLI reference.
2026-05-30
Fixes
- Expo Go.
Amba.configure()now initializes cleanly on first call across Expo Go, development, and standalone builds. If you're on an earlier@layers/amba-expo4.0.x, upgrade withnpx expo install @layers/amba-expo— no code changes required on your side.
New
- Experiments — A/B testing with significance. Define weighted variants, get sticky per-user assignment, log exposures, and read per-variant conversion rates with a built-in two-proportion z-test. See Experiments.
- Environment promotion. Export a project's declarative configuration bundle and import it into another project (dev → prod) in one call — currencies, achievements, collection schemas, content libraries, flags, and more. See Environment Promotion.
- Messaging — threads, attachments, typing, reactions. Reply in threads with
parent_message_id, attach files, broadcast a typing signal, and add emoji reactions. See Messaging. - Auth — email + password account linking.
Amba.auth.linkEmailPassword(email, password)upgrades an anonymous session to an email-identified account in place, preserving the guest's data. See Account linking. - Collections —
return=minimal. Pass?return=minimal(orPrefer: return=minimal) on single or batch inserts to trim the response to row ids — keeps large-row migrations inside an agent's token budget. See Collections (Admin API). - Users — admin create.
POST /admin/projects/:projectId/users(MCP:amba_users_create) mints a seed / system user to attribute migrated rows to. See Users (Admin API).
2026-05-29
Breaking change — Admin collections: includeDeleted now defaults to false. The Admin
list rows and count rows endpoints used to return soft-deleted rows by default; they now
exclude them. If you relied on seeing soft-deleted rows from an admin call, pass
includeDeleted: true explicitly. The client (SDK-facing) surface has always defaulted to false
and is unchanged.
New
- Collections — atomic upsert. Insert with
on_conflict(ignoreorupdate) and aconflict_targetto insert-or-update in a single call. See Collections (Admin API). - Collections — compare-and-set. Conditional updates that only apply when
the row still matches an
expectedsnapshot, so concurrent writers don't clobber each other. See Collections (Admin API). - Collections — group-by aggregation. Roll rows up with
count,sum,avg,min, andmaxover a group-by key. - Collections — client-side bulk insert. Insert many rows at once via
/batch. Both single-row and batch insert return a clear 409 on a unique-key clash, so a duplicate always surfaces immediately. - Economy — exactly-once grants & spends. Pass an
idempotency_keyon a currency grant or spend and a retry can never double-apply. See Idempotent grants and spends. - Economy — spendable initial balance. A currency's
initial_balanceis now spendable: it's credited to the ledger the first time a balance is touched. See Spendable starting balances. - Functions — filter logs by request.
amba functions logs <name> --request-id <id>narrows a function's logs to a single request (r_...), and works with--tail. See the CLI reference.