Skip to content

Changelog

Dynamic Client Registration caps. POST /v1/oauth/register accepts at most 10 redirect_uris (512 bytes each) in a 16 KB body, and is limited to 30 registrations per hour per source IP and 500 per hour overall (429 rate_limited, Retry-After: 3600). Registered clients live 30 days: register once per installation. See Rate limits.

Token endpoint hardening.

  • Rate limit: POST /v1/oauth/token is limited to 100 requests per 5 minutes per source IP; /v1/oauth/register and /v1/oauth/revoke are 20 (the page said 10). WAF 429s carry Retry-After: 300. See Rate limits.
  • Authorization codes are single-use, whatever the outcome: a token request that presents a valid authorization code consumes it, so a mismatched redirect_uri, code_verifier, client_id, resource or client_secret requires a new authorization. Do not retry the same code after an error.

Account deletion: resolve answers 404 from the moment a traveler requests deletion; data deleted 14 days later. See Operations and compliance.

@movmo/connect-button 3.0.0: “Not you?” is in-page, the FedCM client path is gone. See Migrating from 2.x.

ChangeEffect for you
”Not you?”Device-scoped and in-page: no redirect, no movmo_signed_out, onSignedOut fires once, the Movmo session stays signed in. The next click asks “Continue as {email}? / Use another account”.
FedCMfedcmConfigUrl removed; mechanism is always 'rt', isAutoSelected always true.
Device keyDeleted when a sign-in ends without a handle (declined, or an ?error= redirect).
Authorize URLrt_request and dpop_jwk travel in the URL fragment.
  • POST /v1/oauth/consent: a remember-only request now returns 400 unless the traveler has a standing consent for your client.
  • Recognition limit: recognize and forget are limited to 120 requests per minute per IP.

Identity integration guide, Remembered devices (§4): four partner notes. Recognition never signs in, unlocks payments or opens a PNR (rule M10); “Not you?” and “Forget this device” call preventSilentAccess() for your whole origin; a silently minted code carries only the scopes already granted; the chip renders the traveler’s first name in your DOM. The storage table now lists every key the SDK writes, and Operations and compliance adds the storage-notice duty.

@movmo_app/payments 0.8.0: R1 403s (recovery_cooldown, reauth_required, movmo_session_required, first_party_only) no longer trigger a session refresh; API helpers throw PaymentsApiError with status/code/until; onError payloads carry code/until; the recovery cool-down is shown with its end time.

PUT /v1/users/{userId} profile contract. email and phone_number in the body are now ignored (previously email was required, so a profile-only body returned 400); the update is a full replacement of the profile fields; date_of_birth is documented as RFC3339 date-time (it always was).

Access tokens are now resource-bound (RFC 8707). aud is a JSON array with the API URL first, and each surface requires its own resource in it.

{ "aud": ["https://e2e.api.movmo.io", "https://e2e.api.movmo.io/v1/mcp"] }
SurfaceRequires aud to contain
REST (/v1/*)https://e2e.api.movmo.io
MCP (/v1/mcp)https://e2e.api.movmo.io/v1/mcp
Client typeDefault aud
Onboarded partnerAPI URL only. Ask Movmo to add the MCP resource.
Self-registered (DCR)API URL and MCP
  • resource parameter: accepted on the authorize and token requests. Omitted at POST /v1/oauth/token, it inherits the authorize value; matching is exact.
  • invalid_target: returned for a resource outside your client’s allowed set.
  • Upgrade: verify aud as an array membership check, not a string comparison. Details: Token audience.

@movmo/connect-button 2.0.0: the chip recognizes a remembered device. No REST change for existing integrations; redirect_uri is simply not sent when redeeming a recognition code.

  • onRecognized({ code, codeVerifier, firstName, isAutoSelected, mechanism }): exchange the code at POST /v1/oauth/token with code_verifier and your client secret, without redirect_uri. traveler becomes an optional first-paint hint. Opt-in is per device on Movmo’s screen, unticked by default; recognition: 'off' restores 1.3.0 behavior.
  • “Forget this device” and onForgotten (drop your traveler hint there); “Not you?” and Forget call navigator.credentials.preventSilentAccess() for your origin.
  • Script tag: connect/v2/connect.js (or pinned connect/2.0.0/connect.js with its .sri hash); v1 keeps 1.x.
  • Upgrade: the chip now always claims the sign-out return; if you call consumeMovmoSignOut yourself, move the handler to onSignedOut or call it before mounting. Headless useMovmoOAuth / startMovmoOAuth default to recognition: 'off'.
  • Storage on your origin, limits and browser support: Remembered devices and Migrating from 1.x.

@movmo/connect-button 1.3.0 places the chip for you; @movmo/express-checkout-button 2.0.2 shows a pending state. No API change in either.

  • Connect chip: new chipPosition option ('bottom-left' default, 'bottom-right', 'inline'). The fixed positions render into a package-owned slot on document.body (40px from the side, 28px from the bottom), so you write no CSS and add no wrapper. Script tag: MovmoConnect.mountButton(options) with no target; mountButton(target, options) still works, and 'inline' requires the target. Upgrade by bumping the version, then delete your own chip wrapper and positioning CSS. See Add the chip.
  • Express Checkout button: on click it is disabled, aria-busy, and shows “Opening Movmo…” until Movmo’s page replaces yours; it resets if the offer save fails and when the page returns from the back/forward cache. See What your users see.
  • Pinned script tags: update the URL and hash together (connect/1.3.0, express-checkout/2.0.2; the .sri sidecars hold the hashes).

One self-switching chip: @movmo/connect-button 1.2.0. ConnectMovmoButton (React) and MovmoConnect.mountButton (script tag) now show both states: pass traveler={ firstName } from your own session for the recognized chip, null or nothing for “Checkout faster with Movmo”. No conditional logic on your side.

  • Upgrade: replace MovmoRecognizedChip / MovmoConnect.mountRecognized (1.1.0, now deprecated aliases) with the one chip and add traveler. Script-tag handles gain update({ traveler }).
  • onSignedOut: return a promise that resolves once your session is cleared; after a confirmed “Not you?” the chip stays anonymous until then. SSR: render traveler={null} when the URL carries movmo_signed_out.
  • Sign-out contract, return params and “Manage” are unchanged. See Not you? and sign-out.

Recognized chip and “Not you?”: @movmo/connect-button 1.1.0. Once you hold a traveler’s GUID, render MovmoRecognizedChip (React) or MovmoConnect.mountRecognized(target, options) (script tag) in place of the connect chip: “Welcome back, {firstName}”, “Not you?” and “Manage”. Options: firstName, clientId (required), onSignedOut, isProd, postLogoutRedirectUri, manageUrl, collapseAfterMs.

  • “Not you?” ends the Movmo session through Movmo’s own sign-out page and returns the traveler to your page; the SDK verifies the return, removes only movmo_signed_out and movmo_logout_state, and calls onSignedOut once. There is no endpoint for you to call. In onSignedOut, clear your own session, greeting, pre-fill and GUID cookie. Sign-out is not consent revocation; the GUID keeps resolving until the traveler revokes in Connected apps.
  • No chip on the page? consumeMovmoSignOut / MovmoConnect.consumeSignOut claims the return; both chips also accept onSignedOut.
  • @movmo/express-checkout-button 2.0.1 scrubs only its own return params (movmo_exit, movmo_state, movmo_booking_id, movmo_pnr), so both SDKs can share a page.
  • See the recognized chip.

Identity is now a one-click chip: @movmo/connect-button 1.0.0 (breaking). The recognition iframe and the recognition token are retired. The traveler clicks “Checkout faster with Movmo” on your landing page; Movmo returns a code to your callback with no screen when they have a session and a standing consent, and otherwise signs them in and asks for one consent. Your backend exchanges the code and calls POST /v1/identity/resolve with the guid.

  • Upgrade: npm install @movmo/connect-button@^1.0.0, or load the v1 script tag (https://cdn.e2e.movmo.io/sdk/connect/v1/connect.js; pin an exact version with SRI for production). /v0/ on the CDN stays on 0.x. Replace <MovmoRecognition /> / MovmoConnect.mountRecognition with <ConnectMovmoButton /> / MovmoConnect.mountButton(target, options) on the landing page, never next to the Express Checkout button.
  • Callback page: check error first (a denied consent returns ?error=access_denied and no code), then call completeMovmoOAuth / MovmoConnect.completeOAuth. It now throws MovmoOAuthError with code === "missing_code" when code is empty; show the same retry state as for MissingVerifierError and StateMismatchError. See the callback page.
  • Remove: the onToken, onState, onSignedOut, transparency, chipPosition, readyTimeoutMs, extraOrigins options, the /v1/identity/recognition-token call from your page, your backend’s recognition_token resolve, and the frame-src CSP entry. Your link-complete endpoint and resolve {guid} are unchanged. Clear your own session on sign-out; a GUID you stored keeps resolving until the traveler revokes your airline.
  • Removal date: POST /v1/identity/recognition-token and recognition_token on POST /v1/identity/resolve are removed from Movmo’s API in the week of 2026-10-05. After that, resolve accepts only guid, and a recognition_token body returns 400.
  • Origins: your registered origins now cover CORS for the Express Checkout offer save and the Express Checkout return URL; nothing embeds in your page. Sign-in is always on Movmo’s page, with an email code or a passkey.

Express Checkout is now a top-level redirect: @movmo/express-checkout-button 2.0.0 (breaking). Clicking the button navigates the whole tab to Movmo’s checkout page, and Movmo sends the traveler back to your page. The in-page overlay, the iframe, the /movmo/checkout route and the MOVMO_BOOKING_COMPLETE / postMessage events are gone.

  • Upgrade: npm install @movmo/express-checkout-button@^2.0.0, or load the v2 script tag. Props are unchanged, plus an optional returnUrl; /v1/ on the CDN stays on 1.x.
  • Return page: the traveler comes back by a full page load. Mount the button on the return page, or call consumeMovmoCheckoutReturn / MovmoExpressCheckout.consumeReturn there; SPA router state is empty. See The return.
  • Callbacks: onBookingComplete then onExit fire on that load, verified by a one-time state nonce; the airline PNR arrives only in the URL fragment. They are unauthenticated browser signals: confirm the booking server-side before fulfilling.
  • Remove: the /movmo/checkout* route, any message listener and the frame-src CSP entry. Your registered origins now also gate the returnUrl; an unregistered one gets no return link.
  • Sign-in: travelers sign in on Movmo’s page with an email code or a passkey, with no pop-ups or storage prompts.

In-checkout passkey sign-in for Express Checkout; identity SDK improvements. @movmo/express-checkout-button 1.14.0+ and @movmo/connect-button 0.7.0+ are on npm:

  • Express Checkout passkey sign-in. A returning traveler with a Movmo passkey can now sign in inside the Express Checkout iframe with one biometric, instead of the separate popup, when the browser lets the checkout use Movmo cookies (Chrome by default; others after the traveler allows it). Requires @movmo/express-checkout-button 1.14.0+; travelers without a passkey, and browsers that do not report the embedding page to the authenticator (Safari today), continue in the existing sign-in popup. The SDK grants publickey-credentials-get to the iframe when it’s embedded on a registered origin. See What your users see and Where it can run.
  • Iframe permissions. From 1.14.0, @movmo/express-checkout-button sets the iframe’s allow attribute to storage-access; publickey-credentials-get. Partners who embed the checkout in a raw iframe must set the same attribute. See @movmo/express-checkout-button.
  • Identity SDK allow attribute. @movmo/connect-button 0.7.0 adds publickey-credentials-get to the recognition iframe’s allow attribute in preparation for identity-scoped passkey features (no behaviour change yet). See Connect button.

Express Checkout guide fixes from the fourth pilot-kit validation run (a server-rendered stack, script-tag build).

  • The script-tag mount no longer sets a fixed container size (the old 240×44 wrapped the label); the button’s natural size is about 303×68 px, and Styling is now size and width only, matching the prompt.
  • onExit says your page is visible while it runs, recommends an interim state before navigating, and shows how to chain an async onBookingComplete (the button does not await it).
  • Routing: a reload re-opens the overlay with the URL’s offerId, mvs is an analytics session id, a reloaded overlay calls no callbacks, and a server-rendered site keeps the selection in its session.
  • Pre-saved offer ID shows the 201 { offer_id } and 400 { error: { message } } bodies; the onError stages 400 row points at error.message.
  • After the click: airlineRef is your company code as registered, uppercase; search_again comes only from “Search again” after a non-retryable failure, and a browser-back close calls no onExit.
  • The sandbox example now says what the checkout displays: the airline behind your iataCode, not owner. The providerID prop row lists the sandbox provider id, and the AI-assistant prompt covers rendering the API key on a server-rendered stack.

Express Checkout for airlines: a product section mirroring the identity guides.

Identity guide fixes from the third pilot-kit validation run (a server-rendered, no-React stack against a live e2e integration):

  • The script-tag callback sample now guards a declined consent / malformed callback the same way the React sample already does, and defines the showRetryPrompt helper it calls.
  • The return path: both callback samples now restore the page the traveler started from instead of always redirecting to /, and the retry copy no longer tells a traveler to “tap the chip” on a page that has none.
  • Flow B defines “server-sealed cookie” (encrypted + authenticated, key from your secret store, or HMAC-derived from client_secret with no key store) with a Python and a Node example; What your users see covers onSignedOut for a site with no accounts.
  • “Where it can run” says the origin-keyed allowlist reads the callback POST’s Origin header, and that a CDN or gateway in front of your backend must forward it.
  • Uniform 404 discipline notes there’s no test GUID — revoking your airline in Connected apps is the test — and that on a server-rendered page the next resolve call is the retry.
  • The onboarding checklist now mentions the script-tag install option alongside npm install.

Script-tag build of the recognition surface (no React). @movmo/connect-button 0.6.0 is on npm:

  • Any stack can now mount the recognition surface with one <script> tag and no bundler: https://cdn.e2e.movmo.io/sdk/connect/v0/connect.js defines window.MovmoConnect, with MovmoConnect.mountRecognition(...) and MovmoConnect.completeOAuth(...) mirroring the React component and completeMovmoOAuth. See No React? One script tag.
  • Pin an exact version with Subresource Integrity for production — https://cdn.e2e.movmo.io/sdk/connect/0.6.0/connect.js with integrity="sha384-Kqf/2NZVts5g/2grV+djlJl9T9bG8smNHQ4bbx6ALd88GxJ2VwjqgZXn0wgJb3hF" — or fetch the published .sri sidecar for any version.
  • Props reference documents three new MovmoRecognition / mountRecognition options: extraOrigins (non-prod origin trust), authorizeUrl (auth UI base URL override) and storage (a custom StorageAdapter for SSR/route-handler callbacks).
  • The AI-assistant prompt is now stack-aware: it tells the assistant to use the script tag for a non-React site and the React component otherwise, instead of assuming React everywhere.

readyTimeoutMs default raised 10 s → 15 s; frame_timeout is a signal, not a teardown. Use @movmo/connect-button 0.5.3 or later (earlier versions still default to 10 s):

  • Fixes a cold-load false negative: a slower boot handshake — a cold CloudFront edge container, a slow connection — could fire frame_timeout before the surface had a real chance to recognize the traveler, making the recognition chip intermittently absent even though it would have appeared moments later.
  • Props reference and What your users see are clearer that frame_timeout is a telemetry signal, not a teardown: the iframe keeps listening after it fires, and the chip can still appear once the network catches up. Keep your page fully usable without it — log the event, with elapsed time, for your own monitoring rather than hiding UI or treating it as permanent.

A Flow A must return to the origin it started on. A site served on more than one origin (www vs. apex, a staging alias, …) needs one registered redirect_uri per origin — the fix below turned out to be a workaround for eviction, not the root cause of the incident that prompted it:

  • New rule in Where it can run: build redirect_uri from window.location.origin plus your callback path at runtime, and have your backend send the token exchange the same value, chosen from its own allowlist keyed by the request’s origin, never taken from the browser. PKCE state is per-origin storage; a callback on a different origin cannot complete the flow.
  • Mobile Safari and the one-time code and What your users see each get a pointer: MissingVerifierError/StateMismatchError from an origin mismatch fails every time on that origin, not just occasionally like tab eviction (v1.14.0) does — “works the second time, not the first” or “greeting missing on one hostname” is the tell.
  • The AI-assistant prompt block now tells the assistant to build redirect_uri from window.location.origin, have the backend pick it from an origin-keyed allowlist, and ask for every origin the site is served on.

The callback example no longer treats a failed connection as success. Use @movmo/connect-button 0.5.2 or later:

  • Fixes a live defect where the callback example on this page (MovmoCallback) treated MissingVerifierError / StateMismatchError the same as success — a silent replace("/") with no greeting and no way to tell what happened. Causes include e.g. iOS Safari evicting a tab’s sessionStorage while the traveler switches to Mail to read a one-time code (see the origin-mismatch cause added in v1.14.1). The example now shows a visible “tap the Movmo chip to try again” state and logs the error name; success is judged only by a 2xx from your own link-complete endpoint, never assumed on redirect. See Add one thin OAuth callback page.
  • @movmo/connect-button 0.5.2 mirrors PKCE state to localStorage with a 10-minute TTL alongside sessionStorage, so the lost-tab case above self-heals with no code change on your side; a custom storage adapter is still available. See Mobile Safari and the one-time code.
  • The AI-assistant prompt block states the same rule explicitly, and What your users see gets a troubleshooting bullet for a missing greeting after the first sign-in on a phone.

The recognition surface reports sign-out: onSignedOut. @movmo/connect-button 0.5.1 is on npm:

  • <MovmoRecognition onSignedOut={() => …}> fires once after the traveler’s “Not you?” inside the surface — after Movmo’s own session is already cleared in this browser (other devices unaffected). Clear your own session for this traveler (greeting, pre-fill, any cookie carrying the GUID), but keep a consented GUID: logging out and removing consent are two different actions, and revocation still shows up as the 404 you already handle on resolve. See Props reference and What your users see.
  • Supersedes the earlier “a dedicated sign-out callback is on the roadmap” note and the do-it-yourself “Not you?” advice for Flow B sites — wire the callback, build nothing.

Express Checkout button ships an exports map; the button reports save failures via onError. @movmo/express-checkout-button 1.12.0, @movmo/connect-button 0.5.0 and @movmo_app/react-common 0.13.0 are on npm (PR #183):

  • @movmo/express-checkout-button now ships proper ESM/CommonJS builds behind an exports map (import, require, ./dist/index.css), so it loads in plain Node, Vitest/Jest and SSR frameworks without the bundler workarounds the Install (React) section previously documented; in SSR the component itself still renders client-side.
  • The button’s offer-save failure is no longer silent to your code: it always logs console.warn('[movmo] express checkout: offer save failed', { status, message }) and now calls an onError({ stage, status?, message }) callback — stage: 'save_offer' for a failed createProviderOffer call, stage: 'missing_offer' when neither flightOffer nor flightOfferID was passed. The script-tag mount() accepts the same option. See Handling save failures.
  • @movmo/connect-button 0.5.0 and @movmo_app/react-common 0.13.0 carry the same README/exports-map fix; no behavior change for the identity guide beyond the version bump.

Sandbox provider offers are now validated at save time (MOVMO-672). createProviderOffer for the sandbox provider used to accept any shape with 201 and only fail when the checkout read the offer back (undefined fields, a stuck click). It now rejects a bad offer immediately with 400 and a message naming the field; nothing is stored. Slice, segment and layover duration also now accepts ISO 8601 ("PT1H25M") in addition to "N hr M min" ("1 hr 25 min") — read-back is always "N hr M min". See the updated rules in “The offer object” and OfferSlice/OfferSegment in openapi.yaml.

Express Checkout page rebuilt from the second validation run (an engineer following only that page on a Vite app against the e2e sandbox provider), plus one platform fix:

  • Registered origins now get CORS as well as iframe trust. The button’s offer save and analytics beacon are cross-origin calls from your page; until now the response headers came from a static list, so a registered partner’s click did nothing. Identity partners’ embed origins now pass both the API Gateway preflight and the API’s CORS check automatically. The new Where the button can run section says what an unregistered origin looks like and how to detect it.
  • The offer object: per-provider shape (PSS JSON or provider offer id — the adapter does the unwrapping; Duffel is id-only), a complete sandbox-provider example, and the rules that were previously discoverable only by trial — decimal-string amounts, the empty passengers[].id (the checkout fills it; a non-empty value breaks passenger entry), provider_passenger_id back-references, the "1 hr 25 min" duration format, zone-less local times — and the offer lifecycle (30-minute expiry, bound to the first account that opens it). openapi.yaml gained OfferSlice, OfferSegment, OfferPassengerSlot, OfferCarrier and OfferPlace schemas.
  • Server-side save example for the pre-saved offer ID mode; routing rule for /movmo/checkout/*; the sign-in popup; a plain message listener instead of “intercept it”; the 1.11.1 package’s Node-loading gap with the Vitest and Next.js workarounds; version header, label wording and SRI example updated to 1.11.1.
  • The AI-assistant prompt now names the sandbox-provider shape, the passenger-id rule, the router rule and what a dead click means.

Identity guide fixes from the first end-to-end validation run (an engineer following only these pages on a Vite app):

  • @movmo/connect-button 0.4.1 (and @movmo/express-checkout-button 1.11.1) no longer read a Create-React-App process.env override without a guard; 0.4.0 threw ReferenceError: process is not defined at mount under Vite and similar bundlers. The mount step now says 0.4.1+ and gives the stopgap for older pins.
  • One backend path everywhere (/api/movmo/link-complete); the callback example checks the response status and no longer lets the browser supply redirect_uri.
  • The 404 rule is stated one way on every page: delete the stored GUID, never retry it; recover with the next recognition token.
  • Flow B for sites without customer accounts (a server-sealed HttpOnly cookie is an acceptable store), what “Not you?” does and doesn’t tell your page, both anonymous chip texts, the expected console 403 from the iframe’s session probe, and a note for stacks without React.

Registered origins are embed origins; identity endpoints in the OpenAPI spec.

  • The origins of an identity partner’s registered redirect URIs (plus its airline domain) are now the origins allowed to embed Movmo’s recognition surface and the Express Checkout iframe — automatically, within minutes of registration, with no deploy on either side. Register every host you embed from; see Where it can run. The public directory backing this is GET /v1/identity/embed-origins.
  • POST /v1/identity/resolve, POST / DELETE /v1/identity/loyalty-link and GET /v1/identity/embed-origins are now in openapi.yaml under the Identity tag (53 operations), matching the prose contract that was previously the only reference. The AI-assistant prompt is checked against the spec in CI on every docs change.
  • The Express Checkout button page gained its own AI-assistant prompt.
  • Effort framing removed from the identity pages: the build is described by its parts (What you build), not by a day count.

The amadeus provider id is no longer accepted. Amadeus shut its developer/test environment in June 2026, so the adapter could not be exercised anywhere and has been removed from the platform. Partner-observable changes:

  • provider path parameters and the Express Checkout button’s providerID accept darwin, duffel, dohop only; amadeus returns 404.
  • GET /v1/flights/providers no longer lists amadeus; the MCP resource movmo://providers/amadeus no longer resolves.
  • The provider coverage matrix now has three rows. Also corrected: Dohop does require the separate pricing step (requires_pricing: true), matching its runtime capabilities.
  • The @movmo/express-checkout-button package still ships its Amadeus- and Sabre-shaped flightOffer TypeScript types (they describe an airline PSS payload, not the Amadeus API), but no live adapter consumes the Amadeus shape. Submit the payload shape the adapter you name in providerID expects — see createProviderOffer.

Loyalty claim scoped to your own program; revocation and scope hygiene. Three partner-observable changes on the identity surface:

  • The loyalty claim group on /v1/identity/resolve now returns only the programs your airline linked for the traveler. Previously every membership saved on the traveler’s Movmo profile was returned with a per-carrier verified flag; memberships with other airlines are no longer disclosed, and verified is always true for a returned program.
  • When a traveler turns off recognition for your airline entirely, your loyalty links are now revoked and the member data you supplied is deleted — the same effect as your own DELETE /v1/identity/loyalty-link. See the updated loyalty revocation matrix. Turning off loyalty.read alone still keeps the association.
  • Scopes outside the set registered for your client_id are dropped before the consent screen (default profile.read + preferences.read, plus scopes enabled for you at onboarding). Read the scope field of the token response rather than assuming you received what you requested — see Flow A.

Loyalty account linking is now available, in early access — extends the loyalty single login with a public API contract. Two new endpoints on the identity surface:

  • POST /v1/identity/loyalty-link — associate a consenting traveler’s pairwise GUID with their membership in your loyalty program. You supply the member number, your loyalty account reference, your airline name/IATA code, and a verification_method (member_otp, loyalty_session, or enrollment) recording how you verified ownership through your own channel, plus optionally the tier/points you want reflected back through resolve — Movmo never fabricates member data. Returns 201 {"linked": true, "program_code": "..."}.
  • DELETE /v1/identity/loyalty-link — remove an association; idempotent, returns 200 {"linked": false, "program_code": "..."} whether or not a link existed.

Both authenticate with the same client_secret_basic credentials as resolve and share its anti-enumeration property: an unknown, revoked, or not-yours GUID is a uniform 404 {"error":"unknown_guid"}. Linking is independent of disclosure consent — a traveler can be linked without ever granting loyalty.read, and revoking loyalty.read alone stops disclosure without dropping the association. See the new loyalty claim group, the Loyalty account link section of the integration guide, and the loyalty revocation matrix.

New: Identity for airlines — a full onboarding and integration guide for the Movmo identity layer, live on the e2e environment for integrating partners. Three new pages:

  • Overview & onboarding — recognize consented travelers on your own site with no PSS change: the two-layer architecture (browser recognition vs. server-to-server consent + data), the pairwise per-airline GUID, what you build vs. what Movmo provides, and a concrete onboarding checklist.
  • Integration guide — the build with copy-paste examples: the <MovmoRecognition /> embed and props reference, the OAuth callback page, the three backend flows (first connection, stored-GUID resolve, anonymous recognition-token resolve at POST /v1/identity/resolve), and the scope-gated claim set.
  • Operations & compliance — security checklist, day-2 operating responsibilities, and your GDPR duties as an independent controller.

The identity surface uses the same confidential-client credentials and consent machinery documented on the Authentication page; recognition tokens and pairwise GUIDs are the new, identity-specific concepts the guide introduces.

Documentation reconciliation pass — no runtime behavior changes, but several pages had fallen behind what is already shipped:

  • The MCP tool catalog now lists two tools that have been live for a while but were missing from the docs: get_bags (checked-bag options for an offer; feeds create_booking’s bag_selections) and update_payment_method (edit cardholder name / billing ZIP on saved cards — card addition remains deliberately unexposed over MCP).
  • Under review no longer claims scopes are unenforced — scope enforcement shipped (see the v1.3.0–v1.6.4 entries below) and the stale section pointed at a heading that no longer exists.
  • The @movmo/connect-button examples now carry the isProd callout: since v0.3.0 the package defaults to Movmo’s e2e environment, so production integrations must pass isProd.
  • SDK pages updated to current package versions: @movmo_app/payments 0.6.0 (new onEvent action callback, useUpdatePaymentMethod hook) and @movmo/express-checkout-button 1.10.1 (booking-complete now replaceStates the checkout history entry so back-navigation after “Return to airline” lands on your real pre-checkout page; onExit gains the "booking_confirmed" reason).

Consent is now granular per scope (MOVMO-455, monolith-api #279). On the consent screen, users can untick individual permissions before approving — the resulting authorization code, and the token it exchanges for, are bound to the intersection of what you requested and what the user left checked (requested ∩ granted). Always read the token response’s scope field rather than assuming you received everything you asked for; see the new Granular consent section.

Two related behavior changes:

  • A scope the user has previously declined for your client is not re-offered on later authorizations — the code is simply issued without it, and only never-yet-decided scopes trigger the consent screen. If the user declines every requested scope, the code request fails 400 invalid_scope.
  • Users can now toggle individual permissions off or back on for a connected client from Connected apps, without disconnecting the client entirely. See the updated Revocation section.

Calls needing a scope the user withheld still fail 403 insufficient_scope exactly as documented in Enforcement — that contract is unchanged, only how the underlying grant is negotiated. The MCP reference is updated with guidance for agents that may now receive a real subset of their requested scopes: degrade gracefully per tool rather than failing the whole session.

The Security & data handling analytics disclosure now covers the post-booking satisfaction rating — a one-click, 1–5 booking-experience score collected on the confirmation page (numeric only; the card has no free-text field), with a screenshot of the card as travelers see it. Documentation only; the rating itself has always been part of the checkout.

The Security & data handling page’s Analytics section is now a categorized breakdown (MOVMO-519) — funnel/interaction events, product-quality signals captured inside Movmo’s checkout only, technical context, and the per-tab session identifier — plus an explicit “what’s never captured” list. No behavior change; documentation only.

New Security & data handling page (MOVMO-519), written for partner security and data-protection reviewers. Covers the PCI boundary for payment card data (hosted fields, vault token, forwarding to the airline’s own PSP, “PCI-descoped by architecture”), traveler PII handling (field-level KMS encryption, passwordless accounts, consent and deletion), the Express Checkout button’s footprint on a partner page (no cookies, no page observation, iframe isolation, CSP frame-src guidance), first-party analytics, platform controls, sub-processors, and Movmo’s current compliance posture. Linked from Authentication and the @movmo/express-checkout-button page. No API or SDK behavior changes — documentation only.

Express Checkout is now embeddable with a single script tag — no React, no build step (MOVMO-513/514). @movmo/express-checkout-button@1.9.0 ships a self-contained browser bundle hosted on the Movmo CDN:

  • Auto-updating: https://cdn.e2e.movmo.io/sdk/express-checkout/v1/express-checkout.js
  • Version-pinned + SRI (immutable, sha384 hash published per release): /sdk/express-checkout/1.9.0/express-checkout.js

The bundle exposes window.MovmoExpressCheckout.mount(target, options) taking the same options as the React component’s props, carries React internally, and injects its own styles — the default Movmo-branded button needs zero CSS from the host page. This unlocks Express Checkout for non-React stacks (Vue, Angular, server-rendered templates, plain HTML). The React package surface is unchanged; 1.9.0 also adds a fix so overlay restore-on-refresh works when the script loads after DOMContentLoaded. See the updated @movmo/express-checkout-button page.

Users can now self-revoke a connected app’s access (MOVMO-408). A new Connected apps page in Movmo account settings lists every application/agent holding an OAuth grant and lets the user revoke it. Revocation takes effect within minutes — the user’s session tokens are invalidated immediately and the underlying consent is deleted. For partners this surfaces on the token endpoint: a refresh attempt after revocation returns 400 invalid_grant (session invalidated) or 400 invalid_scope (consent gone); either means re-authorize from scratch. See the new Revocation section. No new partner-facing endpoint — the management surface is first-party.

Movmo scopes are now advertised in discovery, and consent is the default for dynamically-registered clients (MOVMO-409, completing the epic’s discovery story):

  • scopes_supported on both discovery documents — RFC 8414 /.well-known/oauth-authorization-server and the MCP server’s RFC 9728 /.well-known/oauth-protected-resource — now lists the OIDC identity scopes plus the full ten-scope Movmo catalog. MCP clients and OAuth libraries that derive their scope request from discovery route through the consent screen automatically.
  • Scope-less requests from dynamically-registered clients (RFC 7591) are now defaulted to the full ten-scope catalog: the consent screen is always shown and the issued token is narrowed and enforced. Omitting scope no longer bypasses consent for agents. Pre-registered partner clients keep the legacy un-narrowed scope-less behavior. See the updated Tokens without scopes.
  • Rollout is graceful: grants issued before this change continue to refresh without narrowing; the new behavior applies from each client’s next full authorization.
  • The MCP reference Scopes section and the OpenAPI spec’s discovery-metadata schema are updated to match.

Partner-level OAuth scopes and user consent are live (epic MOVMO-346). The Authentication page replaces the “Scopes (planned)” stub with the full model:

  • The authorization redirect accepts a space-delimited scope parameter drawn from a ten-scope catalog (profile / preferences / passengers / payments / bookings, each split .read / .write; write does not imply read). Unknown values are rejected with 400 invalid_scope.
  • A Movmo-hosted consent screen appears after sign-in whenever the requested scopes exceed the user’s prior grants to your client — first-time users see the full list, returning users only the delta. Consent is per-client and incremental (new approvals merge with prior grants). Denial redirects to your redirect_uri with error=access_denied and your state.
  • The token response now echoes the effective scope (RFC 6749 §5.1) on both grants — always the intersection of your request and the user’s current consent. Refresh preserves the original grant’s scopes and never escalates; after a consent revocation the refresh grant fails with 400 invalid_scope.
  • Enforcement: requests made with a scoped token outside its grants return 403 {"error":"insufficient_scope", "scope":"<required>"} with an RFC 6750 WWW-Authenticate challenge (delivered as x-amzn-remapped-www-authenticate through API Gateway). See Errors.
  • No breaking change: tokens issued without a scope request are not narrowed and keep the user’s full permissions — the pre-scopes behavior, still the default for autonomous MCP agents until the Movmo scopes are added to scopes_supported in discovery (upcoming). A new Scopes section on the MCP reference covers the agent perspective.

The OpenAPI spec adds the scope field to OAuthTokenResponse and rewrites the top-level scopes note to the shipped behavior.

The @movmo_app/payments reference is updated for 0.5.0. <PaymentMethodsManager> now accepts an optional onError callback that fires for every user-facing failure with a discriminated source (list, tokenization-session, spreedly-script, card-tokenize, save-from-token, set-default, update, delete) — partners wire it to telemetry (e.g. Sentry) and/or UI gates. The callback is additive: errors are still rendered in-component. A new Error handling section documents the PaymentManagerError shape and the source values, and <MovmoCardForm> gains a matching onErrorDetail prop. The page also now documents the edit-card row action (cardholder name + billing ZIP on Spreedly-vaulted cards).

New JavaScript / React SDKs sidebar section documents the two publicly-published partner-facing React packages. @movmo_app/payments wraps Spreedly Hosted Fields and the createPaymentMethodFromToken exchange into drop-in components — partners who want to embed Movmo card capture in their own UI now have a documented on-ramp without needing to redirect users to the Movmo accounts UI. @movmo/express-checkout-button — already published on npm but previously undocumented — gets its own reference page covering both raw-offer and pre-saved-offer-ID modes, props, and the iframe postMessage lifecycle.

The Payments domain page now cross-links to the SDK page for partners who want to embed capture directly.

No changes to the REST or MCP surface — this release is documentation only.

The Payments domain page and the matching OpenAPI sections have been rewritten to describe the vault + Receiver-forwarding model. Movmo is documented as never being the merchant of record — at booking time Movmo forwards the vaulted card to the airline’s PSP, which authorizes the charge and owns 3DS / SCA.

The PaymentMethod response shape is leaner: id, userId, methodType, last4, brand, expMonth, expYear, isDefault, createdAt, updatedAt. Legacy customerId, paymentMethodId, and token fields are no longer documented and should be ignored if present in transitional responses.

The CreateBookingRequest shape drops customer_id. Partners pass only payment_method_id (the Movmo UUID read from GET /v1/users/{userid}/payment-methods) at booking time. Existing integrations that already pass payment_method_id need no changes; those still sending customer_id should remove the field — it is ignored server-side.

Card capture is documented under a new operation: POST /v1/users/{userid}/payment-methods/from-token. It exchanges a vault token (produced in the Movmo accounts UI by Hosted Fields against the PCI vault) for a Movmo payment method UUID. The previously documented bare POST /v1/users/{userid}/payment-methods is removed from the partner-facing surface. Partners do not call the capture endpoint directly — they read existing methods and pass id on bookings.

DELETE /v1/users/{userid}/payment-methods/{methodid} is documented as best-effort against the vault: the server attempts to redact the vault token, then deletes the local row regardless of the vault result so the UI removes the card immediately. A 204 means the card is gone from the partner-visible surface; orphaned vault tokens are reconciled out-of-band.

DELETE /v1/flights/providers/{provider}/bookings/{booking_id} now returns 403 Forbidden when the calling user is not the booking owner (and not an admin). Previously the handler authorized on the bookings:delete permission only — any authenticated caller holding that permission could cancel another user’s booking by guessing the booking ID. The handler now mirrors the ownership check already applied to the matching GET operation: it fetches the booking, verifies booking.user_id against the caller, and rejects cross-user cancellations.

The cancelProviderBooking operation in the OpenAPI spec now lists 403 and the temporary authorization caveat in the operation description has been removed.

The Authentication page now leads with @movmo/connect-button — a published React component that bundles PKCE generation, state issuance, sessionStorage round-trip, and the redirect to auth.movmo.io. Drop the button onto your sign-in page; pair completeMovmoOAuth() on your callback page to validate state and forward code + code_verifier to your backend. Headless useMovmoOAuth() is exported for partners with their own CTA component.

The seven-step manual walkthrough remains for non-React partners (Vue, Svelte, plain JS, native mobile) under a Building your own subsection.

The token exchange itself is unchanged — client_secret still cannot reach a browser, your backend still calls POST /v1/oauth/token. This release only changes the recommended frontend on-ramp.

POST /v1/oauth/token now accepts Authorization: Basic credentials on the refresh_token grant for confidential clients registered with token_endpoint_auth_method: "client_secret_basic". Previously the refresh grant rejected Basic with 400 invalid_client and required client_secret in the form body. The authorization_code grant has always accepted Basic; the bug was specific to refresh.

Both client_secret_basic (header) and client_secret_post (body) now work on both grants. Partners who worked around the issue by sending client_secret in the form body can switch to Basic — the spec-preferred method per RFC 6749 §2.3.1 — or keep using body credentials.

Initial public release of the partner API surface.

  • 49 REST operations across Authentication, Profile, Passengers, Payments, and Flights — see the OpenAPI spec.
  • MCP server with tool and resource parity for the Aviare-scoped surface — see the MCP reference.
  • Provider coverage: Amadeus, Duffel, Dohop, Darwin — see Provider coverage.