Skip to content
openapi.json (opens in a new tab)

OAuth Scope Catalog

The scope families a plugin or store API key works with day to day on the Orbit Commerce public API (api.myorbitcommerce.net/v1/*, plus the /oauth/* token handshake). Most scopes are requestable by a third-party plugin; the remainder are labelled with the credential that can hold them. When a merchant installs your plugin they consent to the scopes you request; the granted set becomes the scopes claim on every token your plugin receives, and each endpoint checks for the scope it needs.

The same scope strings govern store API keys: the merchant picks the key's scopes at creation instead of consenting at install. Every resource scope below is grantable to a key, with two exceptions, each labelled where it appears. The API refuses them at key creation with 400, and the dashboard's key picker, which lists exactly what the API accepts, never offers them:

  • Plugin-only (billing:*, email:send, settings:*, customer-events:create): these only make sense for an installed plugin and are rejected for keys.
  • Consent screen only (page:*, store-settings:*, tracking-script:*, import:*, plus families not yet on this page: theme:*, template:*, currency:*, domain:*, plugin:*, global-overlay:*, team:*, custom-field-definition:*, site-verification:update): writes that change what every shopper sees, rewrite the catalogue in bulk, or change who controls the shop. They are only ever granted on a screen where the merchant approves them by name (a plugin install, or connecting an AI assistant), never to a store key. See Consent-screen scopes for pages and store settings, Tracking scripts and Product import.

This page is the public contract for the families it covers: every scope string here is exactly the string the API enforces, so copy them verbatim. It is not yet exhaustive: /v1 enforces further families (inventory, shipping, discounts, tags, gift cards, returns, quotes, suppliers and more) that this page has still to document. For how scopes are requested, consented to, and checked at runtime, see the Scopes guide; for the endpoint-by-endpoint detail, including the families not yet listed here, see the API Reference.

Naming convention. Scopes are <resource>:<action>. The resource is singular and kebab-case (product, customer, tracking-script, never products or trackingScripts). The action is usually one of list, read, create, update, delete; a small number of scopes use a purpose verb where a CRUD action would be misleading (order:fulfill, email:send, cart:adjust), and content resources with a draft/live split add publish (page:publish, blog-post:publish). So product:write is invalid (write is not an action: use update), and products:read is invalid (the resource must be singular).

A call made without a token, or with an invalid or expired one, returns 401 Unauthorized. A call made with a valid token that is missing the required scope also returns 401 Unauthorized; the error body names the missing scope (Missing required scopes: …). The one exception is subscribing to a webhook topic without the topic's scope, which returns 403 Forbidden.


Products: product:*

The product catalogue: variants, prices, inventory, images, and taxonomy associations. The most-used scope set for sync plugins (marketplaces, feed generators, import/export, reviews, search).

ScopeGrants
product:listGET /v1/products (filtered, paginated), GET /v1/products/sync (lightweight sync variant; follow meta.nextCursor with ?cursor= to page a large catalogue at constant speed). Required for any plugin that iterates the catalogue. With customer:read and b2b-catalog:read, also GET /v1/customers/{customerId}/catalog/products: the products one customer may see, with what they pay (see Customer catalogue and prices).
product:readGET /v1/products/{id}, POST /v1/products/batch-lookup. Pair with product:list for plugins that fetch detail after listing. With customer:read and b2b-catalog:read, also GET /v1/customers/{customerId}/catalog/products/{productId} and POST /v1/customers/{customerId}/catalog/prices.
product:createPOST /v1/products. Used by import plugins and marketplace inbound (Amazon → Orbit, eBay → Orbit).
product:updatePATCH /v1/products/{id}, PATCH /v1/products/bulk-update. Used by sync plugins to push price, inventory, and SEO updates from external sources.

A plugin that only listens to product events through webhooks (product.created, product.updated) needs webhook:create to subscribe plus product:read for the topic. The payload carries the product id, not the product: read it with GET /v1/products/{id}.


Orders: order:*

Reads, create-on-behalf-of and status sync for marketplace plugins, plus fulfilment with tracking for shipping/3PL plugins.

ScopeGrants
order:listGET /v1/orders. Iteration for sync and reporting plugins, with filters for status, payment status, fulfilment state, created-date window, and updatedFrom for incremental sync. For company orders, filter by companyId, poNumber, costCentreId and approvalState.
order:readGET /v1/orders/{id}, GET /v1/orders/{id}/fulfillments. Order and fulfilment detail.
order:createPOST /v1/orders. Used by marketplace plugins and ERP syncs that import orders from an external platform as native Orbit orders. Delivery and billing addresses can be sent inline as shippingAddress / billingAddress (name, address lines, city, state, postcode, ISO alpha-2 countryCode, phone), for source systems that hold the address on the order rather than in a customer address book. The address is frozen onto the order as sent and is not added to the customer's saved addresses; a saved-address id (shippingAddressId / billingAddressId) wins when both are sent, and billing defaults to shipping when omitted. A company order can carry poNumber, companyLocationId, deliveryInstructions and, per item, costCentreCode. Its company comes only from customerId, so send the company user's customerId with them; see Orders, approvals and pay on account. Lines can name their product by sku alone (the create response lists any SKU that linked nothing in unmatchedSkus), and orderNumber keeps the source system's own order number, unique per store (409 ORDER_NUMBER_EXISTS). See Importing orders from your ERP.
order:updatePATCH /v1/orders/{id} (sync an order's status, paymentStatus, or notes; paymentStatus: "paid" also settles the order's open or past-due B2B invoice, which fires b2b_invoice.paid; an order awaiting approval refuses a status or paymentStatus that would change it with 409 ORDER_AWAITING_APPROVAL; set custom metadata, merged key-by-key, where null removes a key, and source, externalId and the keys the platform owns, such as payment_flow, approval_payment_flow, approval_staff_only and approval_payment_term_id, are ignored; toggle archived, idempotently), POST /v1/orders/{id}/tags (find-or-create 1 to 20 tags by name, each up to 100 characters, and attach them; overwrite: true replaces the order's tags with those. An empty list is refused with 400, so this route cannot leave an order with no tags), POST /v1/orders/{id}/approve and POST /v1/orders/{id}/decline (decide an order held for approval, with an optional reason).
order:fulfillPOST /v1/orders/{id}/fulfillments (fulfil an order fully or partially with tracking number/URL/carrier; omit items to fulfil every remaining quantity), PATCH /v1/orders/{id}/fulfillments/{fulfillmentId} (correct tracking on a fulfilled order, mark delivered, or cancel/void a shipment).

order:update is deliberately narrow: pricing, line items, currency, and addresses are not editable through it (and a paid + fulfilled order seals those fields). order:fulfill is separate from order:update because fulfilling has customer-facing side effects: creating a fulfilment with tracking sends the store's Order Shipped email/SMS unless you pass notifyCustomer: false (do that when your plugin sends its own notification). Refunds and cancellation workflows stay owned by the storefront and dashboard.


Customers: customer:*

Read and write customer records: list and detail, plus create, update and delete on behalf of a merchant (for example, mirroring a CRM into Orbit, or resolving and enriching buyer identity during order import).

ScopeGrants
customer:listGET /v1/customers. List the customer base, as marketing and CRM plugins do.
customer:readGET /v1/customers/{id}, GET /v1/customers/by-email/{email}. Customer detail (including tags), and buyer-identity lookup by email (e.g. resolving a customer during order import, or for reviews: "which customer left this review?"). Also the topic scope for the customer.* webhook topics. Together with a product scope and b2b-catalog:read it opens the customer catalogue view (/v1/customers/{customerId}/catalog/*), which includes the prices that customer pays, negotiated B2B prices included.
customer:createPOST /v1/customers. Create a customer, e.g. mirroring a CRM contact into Orbit, or registering a buyer imported from a marketplace.
customer:updatePATCH /v1/customers/{id} (update a customer's details), POST /v1/customers/{id}/tags (find-or-create up to 20 tags by name, each up to 100 characters, and attach them; overwrite: true replaces the customer's tags, and with an empty list removes every tag, which the order route refuses).
customer:invitePOST /v1/customers/{id}/send-invite. Email the customer the store's account invite: a link where they choose a password for their account on this store and are signed in. There is no recipient or message parameter; the address comes from the customer record. Refused with 409 Conflict once the account is active (CUSTOMER_ALREADY_ACTIVE), or when this store's account with the email has never confirmed it and either already holds another customer record or the store is not open for self-registration (CUSTOMER_EMAIL_UNCONFIRMED), or while the customer has closed their own account on this store and it waits to be removed for good, which takes up to 30 days (CUSTOMER_LOGIN_CLOSED); 400 Bad Request (CUSTOMER_NO_EMAIL) when the record has no email. Each store has its own shop accounts, so an account with the same email on another store or an Orbit dashboard is never touched. Sending again issues a fresh link.
customer:deleteDELETE /v1/customers/{id}. Remove a customer. This also closes their account on this store: their sessions end and the email can sign up again as a new account.

Customer timeline: customer-events:read / customer-events:create

ScopeGrants
customer-events:readGET /v1/customers/{id}/events (before, limit, types): a customer's history, newest first, over orders, returns, emails the store sent, abandoned checkouts, conversations, tickets and what plugins recorded. Takes the dashboard permission to read customers to grant, like customer:read.
customer-events:createPOST /v1/customers/{id}/events (type, title, optional summary, route, occurredAt, externalId, data): records one of your plugin's own events on the timeline, listed as plugin.<pluginId>.<type> where the store team and other plugins see it. Plugin-bound.

See the Customer timeline guide.


Taxonomies: taxonomy:*

The product classification hierarchy (Orbit calls this "taxonomy"; some platforms call it a "category tree"). Read-only on the public API: taxonomy management happens in the dashboard.

ScopeGrants
taxonomy:listGET /v1/taxonomies. Hierarchy walk for feed plugins (category mapping).
taxonomy:readGET /v1/taxonomies/{id}, GET /v1/taxonomies/by-handle/{handle}.

In Orbit's vocabulary, a taxonomy is the classification tree. What other platforms call a "category" (a curated product collection) is a separate resource with its own scope family: see Categories below.


Categories: category:*

Curated product collections (the dashboard's Categories): manual or smart, each a browsable storefront destination. Distinct from taxonomies, the classification tree.

ScopeGrants
category:listGET /v1/categories (paginated).
category:readGET /v1/categories/{id}, with its linked products.
category:createPOST /v1/categories.
category:updatePATCH /v1/categories/{id}, plus membership edits on a manual category: POST /v1/categories/{id}/products (assign products) and DELETE /v1/categories/{id}/products/{productId} (remove one). Membership edits are updates of the category, so both run under this scope rather than category:delete.
category:deleteDELETE /v1/categories/{id}. Deliberately its own scope: a token granted category:update cannot destroy a category.

A category has no separate publish step: status is writable on create and update, so a category created active is browsable by shoppers on the same request. Hold your own drafts back by creating them inactive.


Brands: brand:*

The merchant's own brands. A brand is a storefront destination, not just a label: /brands/{handle} is a real route, the brand's description, logo, and SEO fields render on it, and the brand's name appears in the storefront product filter rail. (This is unrelated to the supplier-side brand catalogue, which is not on the public API.)

ScopeGrants
brand:listGET /v1/brands (paginated, always ordered by name A-Z; filter with keyword and status).
brand:readGET /v1/brands/{id}. Includes productCount, the number of products currently carrying the brand: the number that matters before deleting it.
brand:createPOST /v1/brands. A new brand's page is reachable by shoppers as soon as the request returns; there is no draft state.
brand:updatePATCH /v1/brands/{id}. Every writable field is shopper-visible, and renaming the handle moves the brand's public URL (a redirect from the old one is created).
brand:deleteDELETE /v1/brands/{id} (soft delete). Further-reaching than it looks: every product carrying the brand goes unbranded on the storefront in the same request, the brand leaves the filter rail, and /brands/{handle} stops resolving. There is no restore route.

Carts: cart:*

On-demand read access to a live cart, plus the ability to own adjustment lines pushed onto one through the synchronous function.cart.adjustments extension point (conditional fees and surcharges, for example from a customs-duty or payment-surcharge plugin).

ScopeGrants
cart:readGET /v1/carts/{cartId} (store-tenanted; the response shape mirrors the function.cart.adjustments callout payload, so you learn one cart model across pull and push). Also the topic scope for the cart.updated webhook topic, whose payload carries ids only: pair the two so a plugin is notified, then fetches full detail.
cart:adjustNot a callable endpoint by itself. Gates whether a plugin receives the function.cart.adjustments callout and may add its own adjustment lines back onto the cart; core enforces caps and ownership, so a plugin can only ever touch the lines it added.

There is deliberately no list endpoint: enumerating a store's live carts is a privacy surface Orbit has not opened.


Custom fields: custom-field:read / custom-field:update

Custom field values on the store's own resources: the columns an import/export or sync plugin needs beyond the built-in fields. Values only, deliberately: definitions (the store's schema) are merchant-managed in the dashboard, and there is no public create/update/delete for them, so a plugin can never mutate a store's schema.

ScopeGrants
custom-field:readGET /v1/custom-field-definitions (paginated; filter by ownerType and namespace), which lists the store's definitions so you can build your column catalogue, and GET /v1/custom-fields/batch, which returns values for up to 100 owners per request (ownerIds comma-separated, plus ownerType; optional namespace filter).
custom-field:updatePOST /v1/custom-fields/set. Batch-upserts up to 25 values per call, keyed on (owner, namespace, key); each value is validated against the matching definition where one exists.

Owner types span the platform: PRODUCT, PRODUCT_VARIANT, CATEGORY, BRAND, ORDER, DRAFT_ORDER, CUSTOMER, PAGE, BLOG, BLOG_POST, and CRM_DEAL. The store is always resolved from your token; a storeId in the query or body is ignored.


Tracking scripts: tracking-script:*

The conversion-tracking pixels and custom scripts the storefront runs on every page: the well-known pixels (GA4, Meta, TikTok, Snapchat, Pinterest), the Google Ads tag, and scripts of your own. Every write is live from the next page a shopper loads. Withheld from keys: a tracking script runs in shoppers' browsers on the storefront, so a store API key cannot hold these scopes (requesting one answers 400). A plugin requests them at install, and an AI assistant connection can be granted them.

ScopeGrants
tracking-script:read:storeGET /v1/tracking-scripts/snapchat and GET /v1/tracking-scripts/google-ads: the store's Snapchat Pixel or Google Ads tag, or null.
tracking-script:update:storePUT/DELETE /v1/tracking-scripts/snapchat (set or remove the Snapchat Pixel id) and PUT /v1/tracking-scripts/google-ads (set the Google Ads conversion id and event labels).
tracking-script:readGET /v1/tracking-scripts: every script of the store, active or not, with each pixel's id and each custom script's body.
tracking-script:updatePUT/DELETE /v1/tracking-scripts/pixels/{type} (set or remove a well-known pixel: type is google_analytics, facebook_pixel, tiktok_pixel, snapchat_pixel or pinterest_tag), POST /v1/tracking-scripts/custom and PATCH/DELETE /v1/tracking-scripts/custom/{id} (custom scripts of up to 100 KB, placed in the head, at the start of the body or at its end).

The :store pair is the Snapchat and Google Ads surface; the pair without a suffix covers every pixel and custom scripts.

Not grantable to store API keys. A custom tracking script is code the storefront serves on every page, so a leaked key holding these scopes would put someone else's code on the whole shop. Plugins request them on the install consent screen instead.


Settings: settings:read:store / settings:update:store

Your plugin's own per-store configuration, as defined by its settingsSchema. Store-scoped (the :store suffix, as with tracking scripts).

ScopeGrants
settings:read:storeGET /v1/settings. Returns your plugin's settings merged with the schema defaults.
settings:update:storePUT /v1/settings. Partial-merge update; values are validated against your settingsSchema.

The SDK wraps these as orbit.settings.get(), orbit.settings.getValue(key), and orbit.settings.update({ … }); see the SDK guide.


Billing: billing:read / billing:update

Plugin monetisation, for plugins that charge merchants. Use the SDK's billing module (orbit.billing.*); these scopes gate what it can do.

ScopeGrants
billing:readRead-only billing: plans, subscription status, and the merchant's saved payment methods (orbit.billing.getPlans(), getSubscriptionStatus(), getPaymentMethods()).
billing:updateCreate charges and manage subscriptions: subscribe, change plan, cancel, restore (orbit.billing.requestPurchase(), subscribeToPlan(), cancelSubscription(), restoreSubscription()).

A plugin that only displays pricing without taking payment needs billing:read only. See the billing section of the SDK guide for the full flow.


Email: email:send

Send transactional email through the platform's email service, so your plugin's mail goes out through the store's verified sender identity (DKIM), suppression list, and warm-up handling instead of hand-rolled SMTP.

ScopeGrants
email:sendPOST /v1/emails/send (to, subject, html, optional replyTo and attachments: up to 3 items of {filename, content, contentType}, with content base64 encoded and 6 MiB (6,291,456 bytes) in total once decoded. contentType is required and must match the file name's extension: .csv text/csv, .txt text/plain, .json application/json, .xml application/xml or text/xml, .pdf application/pdf, .zip application/zip, .xlsx and .xls their Excel types). Sends from the store's verified sender when one is configured, falling back to the platform default address.

Use it for merchant-facing notifications (sync reports, alerts) and store-branded customer messages your plugin owns. It is not a bulk-campaign API: sends inherit the platform's suppression and rate handling.


Outbound webhooks: webhook:*

Subscribe to store events for delivery to your plugin's own HTTPS endpoint. Most plugins that subscribe at runtime request all four scopes so they can manage their own subscriptions through the API. To subscribe to a topic you also need that topic's resource scope (for example, product:read for product.*); see the Webhooks guide.

ScopeGrants
webhook:listGET /v1/webhooks. List your subscriptions.
webhook:readGET /v1/webhooks/{id}/deliveries. Recent delivery history for a subscription (useful for debugging).
webhook:createPOST /v1/webhooks. Subscribe to a topic at runtime.
webhook:deleteDELETE /v1/webhooks/{id}. Unsubscribe.

Storefront menus and the links inside them. One family covers the menu and its items: an item cannot exist without its menu, so there is no separate navigation-item:*. Menus have no draft state and the storefront resolves them by handle, which makes every write here live: a menu edit is read by shoppers immediately.

ScopeGrants
navigation:listGET /v1/navigations (paginated, ordered by title A-Z; filter with keyword). Menu items are not included; read one menu to get its tree.
navigation:readGET /v1/navigations/{id}: one menu with its full nested item tree.
navigation:createPOST /v1/navigations. The body may carry the whole nested items tree, so a menu can arrive fully populated. If its handle is one the theme asks for (say main-menu), it fills the storefront header on the creating request.
navigation:updatePATCH /v1/navigations/{id} (retitle, or rename the handle, which is what a theme looks a menu up by), POST /v1/navigations/{id}/items (add a link), PATCH /v1/navigations/{id}/items/{itemId} (edit one), POST /v1/navigations/{id}/items/{itemId}/move (reorder/reparent).
navigation:deleteDELETE /v1/navigations/{id} and DELETE /v1/navigations/{id}/items/{itemId}. Hard deletes: there is no soft delete or restore path for menus, deleting a menu takes every item with it, and deleting a single item takes its entire submenu. The item delete deliberately requires this scope, not navigation:update: "edit my menus" must not include "destroy a submenu".

Blogs: blog:*

The containers a store's articles are filed under. Most stores have one ("News", seeded at store creation) and only need more for genuinely separate publications. A blog is not itself a storefront page (there is no /blogs/{handle} route), but a store's blogs are listed on shopper-reachable surfaces, so creating one is visible without any post inside it. Posts are a separate scope family (blog posts below), so granting "write my articles" does not grant "delete the container they live in".

ScopeGrants
blog:listGET /v1/blogs (paginated, ordered by title A-Z; filter with keyword).
blog:readGET /v1/blogs/{id}. Includes postCount, the number of posts currently filed under the blog: the number that matters before deleting it.
blog:createPOST /v1/blogs.
blog:updatePATCH /v1/blogs/{id}. Renaming a blog changes what shoppers read: its title is printed on the card of every post filed under it in the storefront blog index.
blog:deleteDELETE /v1/blogs/{id} (soft delete). Deleting a blog silently takes every published post inside it off the storefront; the posts survive and return if the blog is restored from the dashboard. There is no hard delete on the public API.

Blog posts: blog-post:*

The articles themselves, with a full draft/publish lifecycle. A post is created as a draft, becomes shopper-visible only through the separately-scoped publish step, and (unlike a page) has no separate draft copy once published: editing a published post rewrites the live article.

ScopeGrants
blog-post:listGET /v1/blog-posts (paginated, most recently edited first; filter with blogId, status, keyword).
blog-post:readGET /v1/blog-posts/{id}, including its body content.
blog-post:createPOST /v1/blogs/{blogId}/posts. Always creates a draft: a status or publishedAt in the body is dropped, so a new post is on no page a shopper can reach until the publish step runs.
blog-post:updatePATCH /v1/blog-posts/{id}. On a published post this edits the live article in the same request; renaming the handle moves its public URL (/blog/{handle}) and creates an automatic redirect.
blog-post:publishPOST /v1/blog-posts/{id}/publish and POST /v1/blog-posts/{id}/unpublish: the only handlers on the posts controller that change what the storefront serves.
blog-post:deleteDELETE /v1/blog-posts/{id} (soft delete, restorable from the dashboard). Deliberately not reachable through blog-post:update.

Media: media:*

The store media library: the images and files the dashboard's media manager holds.

ScopeGrants
media:readGET /v1/media (paginated, newest first; filter with keyword, matched against the filename), GET /v1/media/{id}.
media:createPOST /v1/media. Multipart upload, one file per request under the field name file; the upload is validated by its content, not the Content-Type header, and accepts images, audio, video, Lottie animations, glTF/USDZ models and PDF documents. The filename you send becomes the library display name and must carry a real extension. An optional folder text field (1 to 100 characters) files the upload in that top-level library folder, made on first use; the same name always means the same folder.
media:deleteDELETE /v1/media/{id}. The dashboard's delete: the file stops serving at once and there is no restore (it answers 204). Nothing checks whether the file is in use, so check before deleting.

DELETE /v1/media/{id} needs media:delete; removing a folder stays a dashboard action.


Published files: published-file:*

Download files a plugin publishes into the store's Files (CSV or ZIP, up to 250 MB) at a CDN link that stays the same when the plugin replaces the file. The link holds a random token, so it cannot be guessed, but anyone who has it can download the file ("visibility": "public-url"): never publish private data there. Plugin scopes only: a store API key cannot hold them. See Publishing downloadable files.

ScopeGrants
published-file:listGET /v1/published-files (paginated, by slug). Only the calling plugin's files for the store.
published-file:readGET /v1/published-files/{slug}.
published-file:createPOST /v1/published-files/{slug}/uploads (start: a presigned upload URL for the exact size), then POST /v1/published-files/{slug}/uploads/{uploadId}/complete (publish at the fixed URL). Publishing to an existing slug replaces the file and keeps its URL. Files count towards the store's storage allowance. POST /v1/published-files/{slug}/rotate-url moves a file to a new unguessable URL and kills the old one.
published-file:deleteDELETE /v1/published-files/{slug}. The file goes to the store's trash and its link stops working.

SEO: seo:read

A read-only health overview of the store's search-engine surface.

ScopeGrants
seo:readGET /v1/seo/overview: sitemap entry counts and robots status.

Redirects are their own resource family (next section), not part of seo:read.


Redirects: redirect:*

URL redirects, the tool for moving or retiring storefront paths without stranding saved links. There is deliberately no redirect:update: a redirect is small enough that the correction path is delete and recreate.

ScopeGrants
redirect:listGET /v1/redirects (newest first, capped; meta.hasMore tells you when the store has more redirects than one response returns).
redirect:createPOST /v1/redirects. A redirect takes effect immediately; there is no draft or publish step. Paths are normalized before storage (lowercased, query string and fragment stripped, trailing slash removed) and chains are flattened: creating /a to /b while /b to /c exists stores /a to /c and repoints existing redirects that targeted /a. Read the response (the row as stored) rather than assuming your input was kept, and list first if anything depends on where existing redirects point.
redirect:deleteDELETE /v1/redirects/{id}. Re-breaks whatever link the redirect was rescuing.

A redirect takes precedence over a live page at the same path, and nothing checks for that collision (the dashboard does not either), so confirm the source path is genuinely retired before creating one.


Analytics: analytics:read

Read-only reporting, mirroring the dashboard's analytics payloads.

ScopeGrants
analytics:readGET /v1/analytics/overview (sales overview metrics with the returning-customer rate merged in), GET /v1/analytics/sales-over-time (time series), GET /v1/analytics/top-products (by revenue; limit defaults to 10). All three take a required startDate/endDate range and an optional comparison (default previous_period).

Staff directory: staff:read

The store's staff directory, read only: who is on the team, so a plugin that assigns work to people (a CRM's deal owners and task assignees, a help desk's agents) can show a name, keep an id and notify an email.

ScopeGrants
staff:readGET /v1/staff (paged, ?search= on name or email, at most 100 a page) and GET /v1/staff/{userId}: id, firstName, lastName, name, email, role (the role's display name), active (false until an invited person has confirmed their address) and isPrimaryOwner (whether the person is the store's primary owner). Also the topic team_member.removed, sent when somebody leaves the team, so you can clear what you assigned to them.

Nothing about what a person may do: no permissions, no rank, no invitations, no seats. /v1/team, which has those and writes who can get into the shop, is not available to plugins. A user id that is not on this store's team answers 404 with the same words as an id nobody has. On a dashboard session token the scope is present only for a user who may see the team themselves (Settings > Team); for a staff member who may not, /v1/staff answers 401 naming the missing scope, so handle that rather than treating it as an empty team.


Companies: company:*

B2B buyer accounts: the company record itself, its users (buyers who sign in and place orders on the company's behalf), pending invitations, locations (billing/shipping addresses, each with its own assigned catalogue and payment term), and cost centres (the budget codes its order lines are charged to). Company users, invitations, locations and cost centres have no independent tenancy: a contact, an invitation, a location or a cost centre cannot outlive its company, so they read and write under company:* rather than a scope family of their own, the same way navigation items live under navigation:*.

PATCH /v1/companies/{id} also carries the company's credit, approval and checkout rules: see Companies, buyers and locations.

Every /v1/companies request also requires the store to hold the B2B feature. Without it, reads still work (an ERP that pulls after a downgrade keeps working), but writes are refused with 403 Forbidden and body { "code": "FEATURE_FROZEN", "feature": "b2b" }.

ScopeGrants
company:listGET /v1/companies (paginated; filter by status, exact-match externalId, free-text keyword, and updatedAtMin/updatedAtMax for incremental sync; includeDeleted=true adds deleted companies with their deletedAt; sort with sortBy (name, status, createdAt or updatedAt) and sortDir). With company:read, GET /v1/company-users: the users of every company added or changed in a window, paged, so a sync catches up on membership without reading each company. Removed users are listed only with includeRemoved=true, with removedAt set.
company:readGET /v1/companies/{id}, which returns the company with its locations and contacts, including each contact's linked customer record, projected down to exactly id, firstName, lastName and email (the buyer's phone, notes, consent flags and unsubscribe token are never included). It is still a PII-bearing scope: a buyer's name and email are personal data. Grant it only to integrations that need contact detail. Also: GET /v1/companies/{companyId}/users, GET /v1/companies/{companyId}/invitations, GET /v1/companies/{companyId}/locations, GET /v1/companies/{companyId}/locations/{locationId}, GET /v1/companies/{companyId}/cost-centres, GET /v1/companies/{companyId}/cost-centres/{costCentreId} and GET /v1/companies/{id}/statement. With company:list, GET /v1/company-users. Also the topic scope for the company.*, company_user.*, company_location.* and cost_centre.* webhook topics.
company:createPOST /v1/companies. Paired with company:update on the upsert route below.
company:updatePATCH /v1/companies/{id} (including status to suspend/activate, and mainContactId); PUT /v1/companies/by-external-id/{externalId} (with company:create, see below); and every write on a company's users, invitations, locations, and cost centres: POST .../users, PATCH/DELETE .../users/{contactId}, POST .../users/{contactId}/send-activation, POST .../invitations, POST .../invitations/{id}/resend, DELETE .../invitations/{id}, POST /v1/companies/{companyId}/locations, PATCH/DELETE /v1/companies/{companyId}/locations/{locationId}, PUT .../locations/by-external-ref/{externalRef}, POST .../cost-centres, PATCH/DELETE .../cost-centres/{costCentreId}, PUT .../cost-centres and PUT .../cost-centres/by-external-ref/{externalRef}. Removing a company user is a DELETE that still requires company:update, not a separate delete scope, by the same subordinate-resource reasoning as company:read above.
company:deleteDELETE /v1/companies/{id} (soft delete).

The two assignment routes under a location are the exception to company:update: PATCH .../locations/{locationId}/catalog is gated on b2b-catalog:update alone and PATCH .../locations/{locationId}/payment-term on payment-term:update alone, so neither also requires company:update.

PUT /v1/companies/by-external-id/{externalId} is an idempotent upsert keyed on your own ERP id: it requires both company:create and company:update, since it may do either. The path externalId always wins over one in the request body; it matches ignoring case and surrounding spaces, and a matched company takes the path's spelling. It returns { "company": { … }, "created": true } on insert or "created": false on update. See the B2B/ERP integration guide for the full sync pattern.

Company users land pending-activation: POST .../users never accepts a password, and sendActivationEmail defaults to false so a bulk ERP import never surprise-emails a buyer. Set a role preset (admin, buyer, or viewer) for a quick grant, or send an explicit permissions object for the 11 individual flags; an explicit permissions always wins over role. Pass mainContact: true to also make the new user the company's main contact. To send (or resend) the set-password email later, once you're ready for the buyer to sign in, call POST .../users/{contactId}/send-activation. Each company user gets an account on this store's shop only, separate from any account the same email has on another store or an Orbit dashboard; an email this store already has an account for answers 409 Conflict with errorCode: "B2B_EMAIL_HAS_LOGIN" (invite that person instead). An email whose account on this store was closed by its owner answers 409 Conflict with errorCode: "B2B_EMAIL_LOGIN_DELETED" and availableFrom, the time from which the email can be used again (up to 30 days after the account was closed).


Catalogues: b2b-catalog:*

Company-specific price books: per-product/variant price overrides, group pricing rules keyed on category, tag, brand, custom product type, or an individual product, and, for a restricted catalogue, the products a company is allowed to see and buy.

Every /v1/catalogs request also requires the store to hold the B2B feature; the same read-always, write-refused-when-frozen split as company:* applies (403 Forbidden, { "code": "FEATURE_FROZEN", "feature": "b2b" }).

ScopeGrants
b2b-catalog:listGET /v1/catalogs (paginated; filter by status, free-text keyword).
b2b-catalog:readGET /v1/catalogs/rule-targets/custom-types (the distinct merchant-defined product types a custom_type rule can target), GET /v1/catalogs/{id} (with its price overrides), GET /v1/catalogs/{id}/prices, GET /v1/catalogs/{id}/products, GET /v1/catalogs/{id}/taxonomies and GET /v1/catalogs/{id}/categories (the products, taxonomy branches and categories a restricted catalogue shows), GET /v1/catalogs/{id}/rules. Also the topic scope for the b2b_catalog.created / b2b_catalog.updated / b2b_catalog.deleted webhook topics. With a product scope and customer:read, also every /v1/customers/{customerId}/catalog/* route: what one customer may see and the trade prices they pay (see Customer catalogue and prices). Those routes do not need the B2B feature.
b2b-catalog:createPOST /v1/catalogs.
b2b-catalog:updatePATCH /v1/catalogs/{id} (name, currency, status and isRestricted); PUT /v1/catalogs/{id}/prices (create or replace one product/variant price override) and DELETE /v1/catalogs/{id}/prices/{priceId}; POST /v1/catalogs/{id}/products, DELETE /v1/catalogs/{id}/products/{productId}, PUT/DELETE /v1/catalogs/{id}/taxonomies/{taxonomyId} and PUT/DELETE /v1/catalogs/{id}/categories/{categoryId} (what a restricted catalogue shows; both PUTs are idempotent); POST /v1/catalogs/{id}/rules, PATCH /v1/catalogs/{id}/rules/{ruleId}, DELETE /v1/catalogs/{id}/rules/{ruleId} (group pricing rules); and PATCH /v1/companies/{companyId}/locations/{locationId}/catalog (assigning a catalogue to a location; this scope alone gates the route, and company:update is not additionally required).
b2b-catalog:deleteDELETE /v1/catalogs/{id}.

isRestricted: true on create means the company sees and buys only the catalogue's products: those added with POST /v1/catalogs/{id}/products, every product filed under a taxonomy added with PUT /v1/catalogs/{id}/taxonomies/{taxonomyId} or under any of its descendants, and every product of a category added with PUT /v1/catalogs/{id}/categories/{categoryId}. false (the default) means every store product is visible, at whatever price the catalogue's overrides and rules resolve to. The vendor dashboard's price-preview/price-check tooling is deliberately not exposed on the public surface.


Payment terms: payment-term:*

Net-terms definitions (Net 30, a fixed due date and so on) that a company location can be assigned. The assignment drives the due date on every B2B invoice an order under that location generates.

Every /v1/payment-terms request also requires the store to hold the B2B feature; same read-always, write-refused-when-frozen split (403 Forbidden, { "code": "FEATURE_FROZEN", "feature": "b2b" }).

ScopeGrants
payment-term:listGET /v1/payment-terms (paginated; filter by status, free-text keyword).
payment-term:readGET /v1/payment-terms/{id}. Also the topic scope for the payment_term.created / payment_term.updated / payment_term.deleted webhook topics.
payment-term:createPOST /v1/payment-terms with a name, unique in the store ignoring case (another term's name answers 409), and a type. net and external (an account the store invoices from its own accounts system) require netDays: an invoice is due that many days after it is issued. fixed requires dueDate, the day every invoice on it is due. receipt (due when issued) and fulfillment (due once the order is fully fulfilled) take neither; a netDays or dueDate a type does not use is dropped.
payment-term:updatePATCH /v1/payment-terms/{id}, and PATCH /v1/companies/{companyId}/locations/{locationId}/payment-term (assigning a term to a location; this scope alone gates the route, and company:update is not additionally required).
payment-term:deleteDELETE /v1/payment-terms/{id}.

B2B invoices: b2b-invoice:list / b2b-invoice:read

Read-only accounts-receivable view of a store's B2B invoices: one is written for each order placed on payment terms, with the term's termType, and for each order placed on account, with termType: "external". There is no public create, update, record-payment, or void route, so ERPs pull an invoice's state through this surface (or through the b2b_invoice.* webhook topics) rather than minting one, and recording a payment or voiding stays a vendor-dashboard action.

Reads work regardless of B2B entitlement, the same as every other company:*-family surface: there simply is no write route here for the entitlement guard to refuse.

ScopeGrants
b2b-invoice:listGET /v1/b2b-invoices (paginated; filter by status, companyId, and updatedAtMin/updatedAtMax for incremental sync).
b2b-invoice:readGET /v1/b2b-invoices/{id}. Also the topic scope for the b2b_invoice.created / b2b_invoice.paid / b2b_invoice.voided webhook topics.

The response never includes stripeInvoiceId: that id must never leave the server. It does include hostedInvoiceUrl, the Stripe-hosted invoice page you can hand a buyer, but only when the invoice was sent through Stripe. An on-account invoice never is, so its hostedInvoiceUrl is always null; a payment-terms invoice has one once Stripe has issued it, which needs the store's Stripe account, and a fulfilment-term invoice waits until the order is fully fulfilled. When Orbit settles or voids the invoice, it closes the hosted invoice, so the link can no longer be paid.


The two families below gate live /v1 endpoints that a store API key cannot hold. Publishing a page or renaming the shop changes what every shopper sees, so these scopes are granted only on a screen where the merchant approves them by name: a plugin that requests them in its oauth.scopes gets them on the install consent screen, and an AI assistant gets them when the merchant connects it (with its own tiered read / safe-write / live-write approval). The dashboard's key scope picker does not offer them, and requesting one on a key is rejected with 400.

Pages: page:*

Storefront pages built with the Orbit page builder: draft/publish lifecycle, revision history, and the element catalog the page content is validated against.

ScopeGrants
page:listNo endpoint enforces this scope today: listing pages requires page:read (below), so page:list currently gates nothing. The string is kept in the catalog for the day pages grow a separate list route; do not request it.
page:readGET /v1/pages (meta only, no content; filter with status/keyword), GET /v1/pages/{id} (with its draft content), GET /v1/pages/{id}/revisions, plus the element catalog: GET /v1/page-builder/elements (compact index) and GET /v1/page-builder/elements/{id} (full prop schema for one element).
page:createPOST /v1/pages. Always creates a draft custom page: a status in the body is dropped, so the result is invisible to shoppers until the separately-scoped publish step runs.
page:updatePATCH /v1/pages/{id} (title, handle, and SEO metadata; note this edits a published page's live metadata, and blanking a live page's handle would unroute it), PATCH /v1/pages/{id}/content (replace the draft content; validated against the element catalog), POST /v1/pages/{id}/revisions/{revId}/restore (restore a revision onto the draft).
page:publishPOST /v1/pages/{id}/publish, POST /v1/pages/{id}/unpublish and POST /v1/pages/{id}/set-homepage (make the page the storefront's homepage): the handlers on the pages controller that change what the storefront serves.
page:deleteDELETE /v1/pages/{id} (soft delete; the page and its revision history are restorable from the dashboard). A live page is deleted in one step and leaves the storefront immediately. Built-in template pages, the homepage, and the default page for a type answer 409.

Store settings: store-settings:*

Store-level configuration: what the store is, as opposed to settings:*:store above, which is a plugin's own per-store settings surface.

ScopeGrants
store-settings:readGET /v1/store-settings, one grouped read: general (order numbering, timezone, unit system), information (name, description, domain), branding (logo, favicon), and currency (primary plus enabled currencies).
store-settings:updatePATCH /v1/store-settings. Deliberately narrower than the read: it may write information.name, information.description, and the general keys (order-number prefix/suffix/format, timezone, unit system, weight and dimension units). The domain, both branding fields, the whole currency group, and the next order number are refused with 400.

Product import: import:*

The /v1/import surface imports products from a CSV file in three steps: stage the file, check it, then apply it. Each step has its own scope, so a merchant can let a connection check a file without letting it change the catalogue. A store API key cannot hold any of them: the key scope picker does not offer them and the API rejects them with 400. A plugin can request them, and the merchant consents at install; an AI assistant connection is granted them through the consent flow a merchant completes when connecting the assistant; see MCP for merchants.

ScopeGrants
import:dry-runPOST /v1/import/uploads (stage a CSV file and get an id for it; nothing in the catalogue changes) and POST /v1/import/products/dry-run (check a staged file, or a few rows sent inline, and report the problems row by row; nothing is written).
import:applyPOST /v1/import/products (import a staged file or inline rows; it runs as a background job and answers a job id).
import:readGET /v1/import/jobs/{id} (the progress and result of an import the caller started).

The file uses the columns in CSV imports and exports.


Using this page

  1. Find the endpoint you need in the API Reference.
  2. Pick the matching scope from the tables above.
  3. Declare it in your plugin's oauth.scopes (in the partner-dashboard plugin form, or via JSON import).
  4. When a merchant installs your plugin, they see those scopes on the consent screen.
  5. After install, your token's scopes claim contains everything you declared, and the API enforces that set on every call.

Request only the scopes you actually use: a smaller set is easier for merchants to approve and limits exposure if a token leaks. If you need something that isn't listed here, it isn't available on the public API yet; reach the team at developers@orbitcommerce.net rather than guessing a string, since any scope not in this catalog will never be granted.


Versioning

These scope strings are version-independent: /v1 and any future version share the same catalog. The OAuth handshake (/oauth/*) is rooted at the host (deliberately not under /vN), so your token flow never changes when the data API version bumps. New scopes are only ever added alongside the endpoints they protect; plugins that don't request a new scope are unaffected.


Last updated: 2026-10-13.