MobiariDocs

API changelog

Every change to the public API, newest first.

Within a version, changes are additive (see versioning): new endpoints, fields, enum values and error codes can appear at any time and are listed here. Anything breaking ships as a new version or with a deprecation period announced here first.

2026-09-29: new token prefixes

Additive. New tokens carry the product's prefix; old ones keep working:

  • Store API tokens now start with mobi_st_ (was bod_). Existing bod_ tokens are accepted until they are rotated or revoked; rotating one returns a mobi_st_ value.
  • Refresh tokens now start with mobi_rt_ (was bpr_). A bpr_ token still trades at POST /v1/auth/refresh, for a mobi_rt_ one.

Treat tokens as opaque: don't validate them by prefix or length.

2026-09-29: ask the documentation

Additive. New tag Help:

  • POST /docs/ask: answers a question from the documentation, streamed as an AI SDK UI message stream (text/event-stream) with the pages it cites as source-url parts. Public, no token; 20 questions per hour per visitor, rate_limited (429) beyond that. The docs site's Ask AI panel uses it.

2026-09-29: plans, billing and setup

Additive. Mobiari is now sold in self-serve plans; these endpoints expose the organization's subscription.

  • New tag Billing:
    • GET /billing/plans: the plans on sale with prices (BRL cents) and limits. Public, no token.
    • GET /tenants/{tenant_id}/subscription: plan, status (trialing, active, past_due, suspended, canceled), trial end, paid period, the open invoice and usage against the plan's limits.
    • PUT /tenants/{tenant_id}/subscription: change plan or billing cycle (owner only; usage_exceeds_plan when the organization would not fit).
    • POST /tenants/{tenant_id}/subscription/cancel, .../resume, .../pay (issues or returns the invoice to pay, with a Pix "copia e cola").
    • GET /tenants/{tenant_id}/invoices: a Page of invoices.
  • New GET /stores/{store_id}/onboarding: the store's setup checklist (products, payments, freight, brand, WhatsApp, team, custom domain, first order), read from its data.
  • POST /tenants accepts an optional plan_code (the plan to try). A new organization starts a 14-day trial and defaults to BRL, America/Sao_Paulo and the caller's language.
  • New error codes, status 402: plan_limit_reached (with limit, max, used, plan in details) from POST /stores and POST /stores/{store_id}/invites; subscription_inactive from those and from the assistant chat when the subscription is suspended or canceled.
  • GET /stores/{store_id}/assistant/usage: cap is now the plan's monthly limit per store instead of a fixed 500.
  • Storefront themes: new routes.products_url (prefix of product pages), and shop.currency is the store's currency (BRL) so | money prints R$ 19,90.

v1 (2026-10)

The API gets a version, a single error shape, a single list shape and refreshable sessions. The unversioned paths keep answering during the transition, but they serve the new behaviour: the changes below reach an existing integration whether or not it moves to /v1.

Migrating

  • Point your client at https://api.mobiari.com/v1 and switch every moved path to its new one.
  • Branch on the error code, not on the error text; expect 422 validation_failed where you handled 400.
  • Treat any 2xx as success: many creates now answer 201 and many actions 204 with no body.
  • Read lists from the Page envelope (data, next_cursor) and pass include_total=true where you show a total.
  • Send an Idempotency-Key on POSTs you retry, such as orders, refunds and messages.
  • If you sign in with a password, keep the refresh_token and call POST /v1/auth/refresh when the access token expires. Store API tokens (bod_…) need no change.
  • Accept enum values you do not know: many string fields became enums in this version, and new values can appear without a new version.

Versioned base path

  • The API lives at https://api.mobiari.com/v1. The Storefront API is under /v1/storefront-api/....
  • The unversioned paths still answer, with the new behaviour, plus Deprecation, Link: <…>; rel="successor-version" and, once a date is set, Sunset headers. See Deprecation.
  • An unknown path under /v1 returns 404 route_not_found.
  • Two OpenAPI documents describe the documented surface: admin.json and storefront.json. The API reference is generated from them.

Errors have a code

  • Every error response is now {"error": "<message>", "code": "<stable_code>", "details"?: {...}}.
  • code is new and stable: branch on it. error keeps the message, so clients that read error keep working, but its wording may change.
  • Error bodies that used other shapes ({"message": …}, {"ok": false, "message": …}, plain text, bare status codes) now use this one. Any extra fields they carried moved under details.
  • Validation failures are 422 validation_failed with per-field messages in details.fields.

See Errors.

One list envelope: Page

  • Lists return {"data": [...], "has_more": bool, "next_cursor": string | null}, plus total when you pass include_total=true.
  • This replaces bare arrays and the per-endpoint envelopes such as {"orders": [...], "total", "limit", "offset"} and {"products": [...], "total", "page"}.
  • Paging uses limit (1 to 200, default 50) and an opaque cursor; offset remains for jumping to a page, in place of page parameters.
  • total is no longer computed by default.

See Pagination.

Idempotency keys

  • New: send Idempotency-Key on a POST to make retries safe. Replays within 24 hours return the original response with Idempotent-Replayed: true.
  • New error codes: 409 idempotency_request_in_progress, 422 idempotency_key_reused.

See Idempotency.

Rate limits

  • New: per-caller budgets per one-minute window (1200 requests/minute authenticated, 300 anonymous by default), reported in RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
  • Over budget: 429 rate_limited with Retry-After.

See Rate limits.

Sessions with refresh tokens

  • New: POST /v1/auth/login returns token, refresh_token (bpr_…), expires_in and token_type, or a two-factor challenge, told apart by status. It supersedes POST /login, whose token lasted 24 hours and could not be renewed.
  • New: POST /v1/auth/refresh trades a refresh token for a new pair. Refresh tokens rotate on every use; reusing one revokes the whole session (401 refresh_token_reused).
  • New: POST /v1/auth/logout, GET /v1/me/sessions and DELETE /v1/me/sessions/{session_id}.
  • Store API tokens (bod_…) are unchanged.

See Authentication.

Unchanged

  • Outbound webhook payloads and signatures.
  • Money in integer cents, timestamps in RFC 3339 UTC, UUID ids.

Endpoint-level changes

What changes per area, grouped like the API reference. Paths are relative to the base URL and link to their reference page.

  • Moved paths are written old → new. Where a line says "old path still answers", the old unversioned path keeps working with the new behaviour, but it has no /v1 form and sends no Deprecation header: switch to the new path.
  • Page marks a list that now answers the Page envelope; its old shape follows when it was not a bare array.
  • Across every area, validation failures that were 400 (often plain text) are now 422 validation_failed. Only other changes are listed.

Auth

  • POST /register → POST /auth/register, answers 201. Old path still answers.
  • POST /login → POST /auth/login. Old path still answers, with the new response.
  • Sign-in answers {"status": "authenticated" | "two_factor_required", ...} and adds refresh_token, token_type, expires_in, refresh_token_expires_in and session_id.
  • Wrong e-mail or password: 401 invalid_credentials (was 400 plain text).
  • POST /verify-totp → POST /auth/2fa/totp/verify. Old path still answers. It accepts only a sign-in challenge token (401 invalid_challenge) and no longer fails with a 500 on a bad one.
  • Two-factor and one-time-code errors are 401 or 400 with codes such as invalid_code, no_pending_code and too_many_attempts (send code, verify code).
  • An access token whose session was revoked is refused. Deactivating a user revokes all their sessions.
  • POST /auth/refresh: retrying with the previous refresh token within 30 seconds, while its replacement is unused, re-issues a pair instead of ending the session.
  • A refresh token of a session ended on purpose (sign-out, device revoke, password change) is 401 invalid_refresh_token, not refresh_token_reused.

Me

Tenants

Stores

Team

  • Page: GET /users, members, invites, roles, a user's roles, role audit.
  • Security: GET /users lists the users of your teams only (was every user on the platform).
  • 204 with no body: remove member, cancel invite.
  • 409 with a code: removing the owner (cannot_remove_owner), deleting a system role (system_role), changing the owner's roles (owner_roles_locked).
  • Security: accepting an invite for an e-mail that already has an account requires that account's password (401 invalid_credentials); an account with 2FA gets no session from an invite.

API tokens

Webhooks

Events

Audit

Places

Products

Categories

Collections

Product feeds

Files

Inventory

  • Store-owned routes moved from ?store_id= or a body store_id to /stores/{store_id}/...: /inventory-locations, /stock, /stock/overview, /stock/export, /stock/movements, /stock/bulk-adjust, /stock/bulk-reorder, /reservations, /stock-counts. Old paths still answer.
  • /stock/levels/{id} → GET /stores/{store_id}/variants/{variant_id}/stock; /stock/history/{id} → .../stock/movements. Old paths still answer.
  • /stock/adjust and /stock/transfer → POST .../variants/{variant_id}/stock/adjust and .../transfer; /stock/reorder → PUT .../stock/reorder (now PUT). Old paths still answer.
  • /stock-items, /component-inventory, /inventory-events and /product-variants/{id}/set-stock stay at the root only, undocumented.
  • Page for every list (were bare arrays, {items, total, limit, offset}, {events, total}, {movements, ...} or {levels}).
  • A stock count is flattened: {count, items} → the count's fields plus items.
  • Creates answer 201 with the resource (were {id}); updates return the resource (were 204).
  • Release and consume return the reservation and stock (were {ok: true}).
  • Movement filters item_id, from, to → variant_id, created_after, created_before; created_at is RFC 3339 (was epoch milliseconds).
  • reason, kind and status are enums: an unknown reason is 422 (was stored as correction).
  • A negative adjustment beyond on-hand stock is 409 insufficient_stock (was clamped); the transfer code insufficient_available became insufficient_stock.
  • Stock writes are 409 logical_stock_readonly on kits and 409 inventory_managed_externally on stores whose stock an ERP owns (unless force).
  • An inventory location's address is plain text (was JSON-encoded).
  • Security: /stock-items and /component-inventory listed every store's stock; adjustments and counts accepted other stores' variants and locations. Both are scoped to the store now.

Purchasing

Manufacturing

Shipping boxes

Integrations

  • ERP sync logs: Page (was {logs}) and readable only in their own store.
  • An unsupported ERP provider is 422 unsupported_erp_provider (was 400); a missing token is provider_token_missing.
  • Security: creating or updating an ERP provider masks its secrets (they were echoed back).

Orders

  • GET /stores/{store_id}/orders: always Page (was a bare array or {orders, total, limit, offset}). status is an enum (unknown values 400).
  • List rows drop freight_quote_id, freight_snapshot_json, freight_external_order_code and the NF-e fields; read them from the order.
  • Get, create and status change return the full order (shipments, refunds, refundable_cents, allowed_status_transitions, ...); create answers 201.
  • PATCH .../status enforces the lifecycle: 409 invalid_status_transition. shipped, delivered, confirmed and processing are no longer accepted (422); the code status_not_settable is gone.
  • POST .../orders/manual answers 201. Errors carry codes: 422 invalid_manual_discount, invalid_shipping, invalid_postal_code, pix_intent_failed; 404 variant_not_found; 409 insufficient_stock, insufficient_component_stock (extra fields moved under details).
  • /order-item-adjustments → /stores/{store_id}/orders/{order_id}/adjustments. The old path is removed (it always failed with a 500).
  • Removed: /fulfillments and /fulfillments/{id} (always failed with a 500).
  • Security: POST /orders refuses other stores' variants, customers and sales channels; a status change checks the store before releasing stock.

Carts

Quotes

  • GET /stores/{store_id}/quotes: Page (was {quotes}, capped at 200).
  • Quote status is open, expired or converted (was aberta, expirada, convertida).
  • Create answers 201. Errors carry codes: 422 empty_quote, invalid_max_installments, invalid_discount; 409 insufficient_stock; 404 variant_not_found, customer_not_found, sales_channel_not_found.
  • Security: a quote refuses another store's customer and sales channel.

Shipments

Payments

  • POST .../orders/{order_id}/refund answers 201 with the refund (was {ok, intent_id, ..., provider_response}).
  • Refunds: amount_cents below 1 is 422; above the refundable balance is 422 refund_exceeds_refundable (was silently capped); no approved payment is 409 no_refundable_payment.
  • Refunded orders are marked refunded or partially_refunded; the payment status gains partially_refunded.
  • Your Idempotency-Key on a refund is forwarded to Mercado Pago.
  • GET /stores/{store_id}/payment-providers: Page.
  • Creating or updating a provider masks its secrets; a duplicate is 409 payment_provider_exists.
  • Security: GET /payment-providers?store_id= still answers at the root, but masks secrets (it leaked them) and needs the manage permission.
  • Removed (they always failed with a 500): /refunds (see GET /stores/{store_id}/refunds), /payments, /payments/{id}/status, POST /payment-providers.
  • Removed: the unsigned /webhooks/payments. Payment gateway webhooks are not served under /v1.

Freight

Locations

Discounts

  • /discount-codes, /discount-codes/{id} and /discount-codes/validate → /stores/{store_id}/discount-codes/...; store_id leaves the body and query. Old paths still answer.
  • Discount codes: Page; create answers 201; expires_at is a date-time (bad input 422, was a server error); 409 discount_code_exists, discount_code_in_use.
  • Validate answers discount_type: null (was "unknown") and a reason enum.
  • Automatic discounts: Page; kind and condition_payment_method are enums.
  • Explicit null clears nullable fields on update, for codes and automatic discounts.

Sales channels

  • /sales-channels and /sales-channels/{id} → /stores/{store_id}/sales-channels/.... Old paths still answer.
  • Page; create answers 201; channel_type is an enum.
  • 409 codes: web_channel_exists, web_channel_protected (was a bare 409), sales_channel_in_use.
  • Generate API key answers {key, prefix}.

Reviews

Customers

  • GET /stores/{store_id}/customers: Page (was {customers, total, page, per_page, vip_threshold_cents}); page and per_page are gone; lifecycle is an enum filter.
  • Customer search also matches full name, company, document and phone digits.
  • GET .../customers/{customer_id} drops recent_orders; orders holds the 50 newest.
  • Create answers 201; a duplicate e-mail is 409 customer_email_exists; unknown ids are 404 customer_not_found.
  • Interactions: Page; 404 for a customer of another store.
  • /customer-addresses and /customer-addresses/{id} are removed (they failed on every call); use GET .../customers/{customer_id}/addresses (Page).
  • Page: timeline (was {events}), duplicates (was {groups}).
  • Merge answers {into, removed, tables} (no ok); merging a customer into itself is 422 same_customer.
  • Erase answers {erased, skipped} (no ok).
  • The CRM endpoints (graph, consent, merge, timeline, duplicates, export, erase) answer 403 missing_permission (was an empty 403).
  • E-mail activity of an unknown customer is 404 (was {summary: null, events: []}).

Conversations

  • GET /stores/{store_id}/conversations: Page (was {conversations, counts}); counts moved to GET .../conversations/counts.
  • The list's q → search and channel → channel_kind; status, priority and sort are enums (all is no longer accepted); open conversations are no longer listed first.
  • Messages: Page, newest first; before takes a message id (was a timestamp).
  • Sending a message answers 201; 422 empty_message, message_too_long, whatsapp_window_closed (were 400 or a later failure).
  • Notes answer 201; deleting an unknown note is 404 (was 204).
  • Socket ticket answers 201 {ticket, expires_in}.
  • Bulk update answers {updated} (no ok); an unknown action or value is 422.
  • GET .../cart returns the cart or 404 no_cart (was {cart}); item endpoints return the cart (were {cart} or {cart, conversation}).
  • PUT .../cart/customer returns the state only; reset answers 204; send and PIX answer 201.
  • Cart errors carry codes: no_cart, empty_cart, insufficient_stock, variant_unavailable, missing_info, checkout_failed, pix_failed, freight_quote_failed, freight_option_invalid.
  • Cart search: q → search; Page (was {variants}).
  • A WhatsApp message accepted by Meta is sent (was delivered).
  • Security: PATCH refuses another store's customer and assignees outside the store's team (422); typing events are limited to the store's conversations.

Conversation settings

Knowledge base

  • Page: collections, articles; q → search.
  • Creates answer 201; deleting an unknown collection or article is 404 (was 204).
  • Security: an article refuses another store's collection.

Email

  • Page: templates, broadcasts, broadcast recipients.
  • Segments: Page (was {segments, fields}); fields moved to GET .../email/segments/fields.
  • Segment members: Page (was {count, members}, capped at 500).
  • Page: forms (was {forms}), products (was {products}).
  • Product subscriptions: Page (was {summary, subscriptions}); counts moved to .../summary.
  • Template test send answers 204 and fails with 502 email_send_failed (was {ok}).
  • Send broadcast answers 202 with the campaign; cancel answers 200 with it.
  • provider, inbox_mode, mode and match are enums: unknown values are 422 (were ignored).
  • New 409 codes: broadcast_in_flight, broadcast_sending, broadcast_not_sendable, broadcast_not_cancellable, email_form_slug_taken.
  • New 422 codes: no_unopened_recipients, csv_missing_file, csv_empty, csv_missing_email_column. Assistant features: 503 assistant_unavailable, 502 assistant_error.
  • Deleting an unknown template, form or segment is 404 (was 204); an unknown broadcast is 404 (was 409).
  • Form sign-ups and CSV imports of new e-mails were silently lost; they are stored now.
  • Security: broadcasts refuse another store's template, and forms another store's welcome template.
  • Segment CSV export neutralizes spreadsheet formulas.

Transactional email

Email deliverability

Ads

  • Audiences: Page (was {audiences, meta_configured}); the flag moved to GET .../ads/connection.
  • Create answers 201 with the audience (was {id, name, status}); sync returns the audience (was {ok, count}) and fails with 502 meta_sync_failed.
  • Deleting an unknown audience is 404.
  • Security: performance errors no longer include the Meta access token.

Workflows

  • Page: workflows, a workflow's runs, versions, secrets.
  • Store runs: Page (was {runs, total, limit, offset}).
  • Fire and retry answer 202 with the run (were {run_id}).
  • Saving a secret returns its summary (was {name}).
  • kind and the settable status are enums (unknown values 422, were 400).
  • Publish with errors is 422 workflow_invalid with details.problems (was {problems}).
  • New codes: 409 workflow_not_published (resume), run_not_active (cancel), run_active (retry), run_not_awaiting_approval (approval); 422 run_not_retryable.
  • GET .../runs/{run_id} is 404 for a run of another workflow.
  • Restoring a version works (it always failed with a 500).
  • Security: fire and test node refuse another store's customer_id (422).

Assistant

  • /assistant/chat, /assistant/conversations (list), /assistant/prefs, /assistant/usage, /assistant/actions, /assistant/actions/{id}/undo, /assistant/phones, /assistant/phones/verify-start and /assistant/phones/verify-complete → /stores/{store_id}/assistant/.... Old paths still answer. store_id leaves the chat body.
  • Page: conversations (?search=; ?q= still accepted), actions (was the 100 newest), phones.
  • 429 assistant_cap_reached (was plain text); 503 assistant_unavailable; 403 not_a_member or missing_permission.
  • Undo: 409 action_already_undone, 422 undo_not_supported (were text).
  • Renaming a conversation returns it (was 204); stop always answers 204 (was 202 when nothing was running).
  • Verify start answers {phone_e164, expires_in_seconds} and fails with 502 whatsapp_send_failed (was 200 {ok: false, message}).
  • Verify complete returns the linked phone (was {verified: true}); a wrong code is 422 invalid_verification_code (was 400).

Notifications

  • Page: channels, logs and inbox (were bare arrays capped at 200 or 100; the inbox takes ?unread=).
  • Alert subscriptions: Page (was {subscriptions}).
  • Channel test answers 204 (was an empty 200) and fails with 502 notification_send_failed (was plain text).
  • PUT .../alert-subscriptions/{user_id} returns the subscription (was {ok: true}); 404 for a non-member; unknown events are 422 (were dropped).
  • channel_type, alert events and alert channel types are enums (unknown values 422); empty alert destinations are 422 (were skipped).
  • Marking read an unknown log is 404 (was 204).
  • Security: channel secrets read as "__stored__" (they were returned to marketing.read); sending that value back keeps the stored secret.

Brand

  • Errors carry codes (were plain text or bare statuses): 503 assistant_unavailable; 422 brand_sources_missing, brand_system_missing, file_missing, invalid_package, invalid_asset; 404 brand_job_not_found; 403 missing_permission.
  • Export without a brand system is 404 brand_system_missing.
  • Asset upload answers 201 (was 200).
  • Job records: kind and status are enums; started_at is a timestamp; already_running is omitted when false.
  • PUT .../brand-kit answers {kit, font_presets} like GET (was {kit}); import fails with 422 brand_import_failed.

Storefront

  • GET /stores/{store_id}/pages: Page (was a bare array); create answers 201 (was 200).
  • Pages: a duplicate slug is 409 page_slug_taken; blank or too long fields are 422 (were 500).
  • Security: pages require cms.read or cms.write (any signed-in user could read and edit any store's pages).
  • Deployments: Page (was the 20 newest as a bare array); status and triggered_by are enums.
  • Promote: 409 deployment_not_ready or storefront_repo_not_connected (were plain text).
  • POST /preview-auth/grant: expires_at is RFC 3339 (was a Unix timestamp).
  • Domains: Page; add answers 201 and no longer fails with a 500; 422 invalid_hostname or reserved_suffix (were 400).
  • Setting a primary domain that is not verified is 409 domain_not_verified.
  • GitHub repos: Page.

Content

Analytics

  • GET /dashboard/stats?store_id= → GET /stores/{store_id}/dashboard/stats; GET /dashboard/attention?store_id= → GET /stores/{store_id}/dashboard/attention. Old paths still answer.
  • Dashboard stats: revenue_this_month (a float) → revenue_this_month_cents, itself now deprecated in favour of revenue_cents and revenue_change_pct.
  • Dashboard revenue counts paid orders only: revenue_series no longer includes unpaid orders.
  • Dashboard days follow the store's time zone (returned as timezone): 7d, 30d and 90d are whole local days ending today.
  • Dashboard stats and every tracking report answer 500 on a database error (they returned zeros or empty lists); the tracking config PATCH no longer reports success when the write failed.
  • Page: top pages, top referrers, top events (were bare arrays with ?limit up to 200).
  • Recent events: Page with a cursor (was a bare array paged by ?before=).
  • Visitors: Page; search is ?search= (?q= still accepted). The list was always empty (a query error); it now returns visitors.
  • Conversions: Page (was {deliveries, total, limit, offset}); capi_meta_status and google_ads_status are enums (sending, sent, failed, skipped), and the skip reason moved to google_ads_skip_reason.
  • Insights: touch is an enum; unknown values are 400 (were read as last_touch).
  • An unknown visitor is 404 visitor_not_found (was an empty 200).
  • Resend: 422 order_not_captured (was {"error": "not_captured", ...}).
  • Catalog sync: 422 meta_catalog_not_configured, 502 meta_catalog_error; result is null when nothing was pushed (was the text "no active variants").

Storefront API

  • Shopper endpoints keep their paths, requests and responses; deployed themes work unchanged. /v1 also answers on store domains.
  • Security: POST /storefront/orders accepts only the channel's own store's products and variants.
  • The retired theme checkout POST /storefront-api/{store_slug}/checkout (always 410) is not served under /v1.

On this page