Documentation Core

Core API

The Core API exposes tenant business resources for initial extraction, incremental synchronization, and supported writes. Use it to keep reporting systems and integrations updated without direct database access. Read Get started for client credentials and shared conventions.

Access and scopes

A tenant administrator creates an API client and explicitly grants its permissions. Existing clients receive no Core access automatically. Every Core operation is limited to the client's tenant.

ScopePermitted operations
core:readAll Core reads
core:writeAll supported Core writes
master_data:readBrands and reference master-data reads
master_data:writeMaster-data writes, initially brand creation
organisational_units:readCompany, warehouse, retail-store and eCom-store reads
customers:readRead B2B and B2C customers
suppliers:readRead external suppliers
transactions:readSales-order and invoice reads

An endpoint accepts its domain scope or the matching core:read / core:write umbrella. Read and write permissions are independent: core:write does not permit GET. To grant both reads and writes across Core, enable both umbrella scopes. Domain and umbrella grants include future collections that logically belong to them; they do not grant access to e-commerce or demand-planning endpoints.

Exchange credentials at the canonical OAuth endpoint. Tokens can request several space-separated scopes, all of which must be explicitly granted to the client:

Reference: POST /oauth2/token.

curl --request POST 'https://api.snowbee.no/oauth2/token' \
  --user 'CLIENT_ID:CLIENT_SECRET' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'tenant_id=TENANT_ID' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'scope=master_data:read organisational_units:read transactions:read customers:read suppliers:read'

Omit resource. Requests containing only Core scopes retain the audience https://api.snowbee.no/tenants/TENANT_ID/core. Core scopes can share a token with ecom:read, ecom:write, loyalty_programs:read and demand_planning when each requested scope is granted; these mixed requests use the audience https://api.snowbee.no/tenants/TENANT_ID. A client granted core:read requests core:read, rather than ungranted domain scopes. Unsupported or ungranted scopes return OAuth invalid_scope for the entire request. Previously issued tokens remain accepted until expiry. See Get started for resource claims and legacy single-resource tokens.

Resources

All paths below are relative to https://api.snowbee.no/v1/tenants/TENANT_ID/core.

ResourceSupported methodsRequired domain scope
/brandsGET, POSTmaster_data:read / master_data:write
/retail_chainsGETmaster_data:read
/brands/{id}GETmaster_data:read
/product_categoriesGETmaster_data:read
/main_product_groupsGETmaster_data:read
/sub_product_groupsGETmaster_data:read
/product_conceptsGETmaster_data:read
/product_lifecycle_statesGETmaster_data:read
/sizesGETmaster_data:read
/sales_order_typesGETmaster_data:read
/reason_codesGETmaster_data:read
/resourcesGETmaster_data:read
/dynamic_attributesGETmaster_data:read
/vat_groupsGETmaster_data:read
/loyalty_programsGETmaster_data:read
/organisational_units/companiesGETorganisational_units:read
/organisational_units/warehousesGETorganisational_units:read
/organisational_units/retail_storesGETorganisational_units:read
/organisational_units/ecom_storesGETorganisational_units:read
/customers/b2bGETcustomers:read
/customers/b2cGETcustomers:read
/suppliers/externalGETsuppliers:read
/transactions/sales_ordersGETtransactions:read
/transactions/invoicesGETtransactions:read

All reference master-data collections above support GET only. No new permissions are needed: an existing master_data:read or core:read grant includes them.

Campaigns, sales prices, products, inventory resources and purchase-order collections are outside this release. Customer, supplier, store, sales-order and invoice creation are also unsupported. See the API reference for full field definitions.

Collection GET and incremental synchronization

Every Core collection GET uses the same query parameters and response envelope:

ParameterMeaning
sinceOptional inclusive ISO 8601 timestamp with an offset; omit to start a full synchronization
cursorOpaque continuation token from next_cursor; mutually exclusive with since
limitMaximum combined entity and deletion items; default 100, allowed range 1–500

Reference: GET /brands.

GET /v1/tenants/TENANT_ID/core/brands?limit=100
Authorization: Bearer YOUR_ACCESS_TOKEN
{
  "items": [
    {
      "type": "entity",
      "changed_at": "2026-10-06T09:03:00.123456Z",
      "data": {
        "id": "019c1234-5678-7abc-8def-123456789012",
        "code": "EXAMPLE",
        "name": "Example brand",
        "status": "ACTIVE",
        "created_at": "2026-10-01T08:00:00Z",
        "modified_at": "2026-10-06T09:03:00.123Z",
        "version": 2
      }
    },
    {
      "type": "deleted",
      "changed_at": "2026-10-06T09:04:00.123456Z",
      "id": "019c1234-5678-7abc-8def-123456789013",
      "deleted_at": "2026-10-06T09:04:00.123456Z"
    }
  ],
  "next_cursor": "OPAQUE_CONTINUATION_TOKEN",
  "sync_through": "2026-10-06T09:05:00.123456Z"
}

For each entity item, upsert the complete data representation using data.id. For each deleted item, remove the entity using id; an already absent entity needs no action. Deletion items contain no previous entity payload. Archived or inactive entities remain entity items with their status.

Results are ordered by (changed_at, id), with full timestamp precision retained. Equal timestamps can span pages safely. The page limit counts both variants: a limit of 100 may return 98 entities and 2 deletions. A short or empty page is valid. There is no has_more field.

Checkpoints and pagination

  1. Start without since for a full synchronization. For later runs, subtract your configured overlap window from the last completed sync_through and send that value as since.
  2. Apply every item on the page. Only then persist next_cursor as your in-progress position.
  3. If next_cursor is non-null, request the same collection with cursor=NEXT_CURSOR, using URL encoding. Do not send since with it. You may change limit between pages.
  4. When next_cursor is null, the run is complete. Save sync_through as the collection checkpoint.
  5. On the next poll, send since = checkpoint - overlap and repeat.

Keep independent checkpoints for each tenant and collection. Save timestamps exactly as returned; do not truncate them to milliseconds or replace them with your local clock. A fresh since request is inclusive, so it may replay items on the boundary. Applying upserts and deletions must be idempotent. Store the applied data and checkpoint atomically where possible.

Treat continuation tokens as opaque and pass them back unchanged for the same tenant and collection. An invalid cursor returns 400 INVALID_CURSOR. Restart from the last completed checkpoint minus the overlap window, or omit since if no run has completed.

An empty terminal response still includes a sync_through checkpoint. Do not advance your completed checkpoint after a failed page or while continuation pages remain.

Timestamps and overlap windows

Collection GET reads current database entities directly and includes retained tombstones. For an entity, changed_at is its database modification timestamp. For a deletion, changed_at equals deleted_at. Changes to embedded children also update the owning entity's modification timestamp. An invoice's invoice_date is a business date, separate from synchronization timestamps.

sync_through is the database cutoff chosen for the first page and kept fixed in subsequent continuation tokens. It is a useful checkpoint, but does not guarantee that every transaction with an earlier modification timestamp has committed and become visible. Database timestamps are not commit-order markers: a transaction can start before the cutoff and commit after the page is read. Read-replica lag and clock adjustments can also affect visibility.

Use an overlap window on every incremental run. Start with 60 seconds as a configurable value, then tune it to cover your transaction durations, replica lag and clock skew. For example, after completing a run with sync_through = 2026-10-06T09:05:00Z, a 60-second overlap means the next run starts with since=2026-10-06T09:04:00Z. Inclusive replay is expected; make upserts and deletes idempotent.

A transaction that commits outside the overlap window can still be missed. No finite overlap gives a completeness guarantee without a bound on transaction duration and visibility delays. Schedule periodic full refreshes or reconcile the destination's IDs and values against a full collection read. Do not interpret sync_through as an exact commit-consistent snapshot.

What a synchronization represents

The endpoint returns current representations and retained deletions, rather than a history of every edit. Several edits between reads can result in one current representation.

If an entity changes during pagination, its position can move. A later poll with an overlap window can pick up recent changes, but a run is not an exact snapshot at a point in time and separate collections do not share a snapshot. Treat this API as incremental current-data synchronization.

Full synchronization includes tombstones, even without since. Tombstones are retained indefinitely for each collection from deployment of its deletion-capture migration, regardless of whether any client has Core permissions. Deletions before deployment cannot be reconstructed. When migrating an existing database extraction, start from an empty destination or reconcile its existing IDs once; old rows deleted before deployment will not have tombstones.

Errors

Status / codeMeaning
400 INVALID_REQUESTsince and cursor supplied together
400 INVALID_LIMITlimit is not an integer in 1–500
400 INVALID_TIMESTAMPsince is not an ISO 8601 timestamp with an offset
400 INVALID_CURSORCursor is malformed or uses an unsupported format
403 FORBIDDENToken does not permit this Core operation in this tenant

Honor the shared 429 rate limit and Retry-After behavior. Fetch continuation pages sequentially for each collection and use backoff on retryable failures.

Entity boundaries

Brands contain their business identity and status. The reference master-data collections cover product categories, main and sub product groups, product concepts, product lifecycle states, sizes, sales-order types, reason codes, resources and retail chains. Independent relationships are referenced by ID.

Dynamic attributes embed their options, and VAT groups embed their country-specific rates. Loyalty programs expose their business name and provider without credentials or integration secrets.

Companies include business identity, company type, country, currency, language, contact details and owned addresses. Bank details and shipping-provider configuration are excluded. Warehouses include identity, warehouse type, currency, contact details, planning settings, owned addresses and company_ids for their associated companies. Warehouse zones, bins and inventory are outside this export. Retail chains are reference master data and use master_data:read.

Retail stores include organizational identity, status, relationship IDs, contact fields and owned addresses. eCom stores include organizational identity, status, store type and provider, and relationship IDs. These exports exclude integration credentials and internal operational configuration.

Invoices embed all invoice lines and the recorded invoice address. Lines expose their type, quantities, explicitly named net unit prices, discounts, VAT and net/gross line amounts, and source transaction references. Monetary values and quantities are decimal strings. net excludes VAT; gross includes VAT. Invoice descriptions and address values come from the recorded invoice, rather than live customer or product objects. Related entities are generally referenced by ID.

Inserting, updating or deleting an owned child updates its parent's source modification timestamp. Moving a child between parents updates both parents. For example, changing a dynamic attribute option makes its attribute appear as an entity item; moving an option makes both attributes appear as changed. Removing a VAT rate returns the updated VAT group with that rate absent.

Deleting a child returns its updated parent, rather than a child tombstone. The same rule applies to company, warehouse and retail-store addresses, warehouse company relationships and invoice lines. Deleting the root entity returns a tombstone for that entity.

Customers, suppliers and typed references

Core exposes concrete entities: B2B customers, B2C customers, external suppliers, companies, retail stores and eCom stores. There is no aggregated customer or supplier collection. Keep their original SnowBee IDs when storing these entities in your integration.

B2B customers include business identity, contact details, relationship IDs and addresses. B2C customers include names, contact details, receipt preferences, addresses and loyalty_memberships. Memberships reference the Core loyalty program by loyalty_program_id, and include the external reference, when known, and registration retail store. These relationships do not establish current membership status. Provider credentials and internal phone-reconciliation evidence are excluded. External suppliers include business identity, contact details, currency_for_tenant_id, standard lead time, service-centre status, addresses and contact_people. Supplier bank details and customer/supplier trading terms are outside these exports.

customer_status and supplier_status describe the shared trading role (ACTIVE or INACTIVE). For B2B customers and external suppliers, status separately describes the concrete entity's lifecycle (PLANNED, RELEASED or ARCHIVED). Inactive roles and archived entities remain readable. B2C customers have customer_status without a separate lifecycle status.

Companies, retail stores and eCom stores expose is_customer and is_supplier. These describe active roles on that exact entity ID, rather than roles inherited from its owning company. For a company, its role must also be configured. Retail stores always have both intercompany roles. An eCom store can have its own roles; a sale made through a store can still use the owning company as the supplier. Resolve a transaction reference using its type and ID, rather than inferring the party from the store where it originated.

Disabling a role updates the entity or organisation flag; it does not produce a tombstone. Deleting the concrete root produces a tombstone in its own collection. Changes to customer or supplier addresses, supplier contact people and consumer loyalty memberships update their root's modified_at, including removals and moves. Changing the shared role status or a company's role configuration also updates the concrete root's timestamp. The same overlap-window guidance applies.

Sales-order and invoice exports include customer_id and customer_type. The ID is the concrete entity ID; the discriminator tells you which collection to use:

customer_typeCore collectionRead scope
B2B/customers/b2bcustomers:read
B2C/customers/b2ccustomers:read
COMPANY/organisational_units/companiesorganisational_units:read
RETAIL_STORE/organisational_units/retail_storesorganisational_units:read
ECOM_STORE/organisational_units/ecom_storesorganisational_units:read

An existing customer or supplier identity cannot change its concrete type. Turning a role off preserves that identity, so a historical invoice reference keeps the same type. Invoice access requires transactions:read; reading its related customer requires the corresponding scope above (or core:read). The invoice contains its recorded address, not an embedded live customer.

If you already synchronize organisations or invoices, run a full synchronization when adopting these added fields. Adding a response field does not change existing rows' modification timestamps.

Purchase-order exports remain deferred. Their references can follow the same pattern with supplier_id plus supplier_type (EXTERNAL, COMPANY, RETAIL_STORE or ECOM_STORE). Typed customer references are derived from existing identities; no extra customer-type column is stored on sales orders.

Sales orders and progress

GET /transactions/sales_orders requires transactions:read or core:read. It uses the same cursor, checkpoint and deletion protocol as other Core collections. Each entity contains the complete current sales order with its embedded lines, discount_coupons, recorded addresses and typed customer reference. Lines are ordered by position, then ID. Related products, resources, gift cards and service orders are referenced by their own IDs; their current names and configuration are not embedded.

Retail classification and online references

is_retail_order is true exactly when organisational_unit_type is RETAIL_STORE and customer_type is B2C. This includes manually entered sales orders, even when they have no retail checkout subtype. Retail-store orders to B2B customers or other stores are false. The classification does not imply that payment has been collected or fulfillment completed. Cash-register information and a separate creation-source classification are not exposed.

Where present, click_and_collect_cart_id identifies the reservation cart converted into the retail order, and ecom_store_id identifies the originating eCom store. eCom orders also expose recorded external order IDs/numbers, sold_at and freight. An external order number alone is not used to infer a sales channel or platform.

Fulfillment and invoicing

Each line has separate fulfillment and invoicing objects, and the order has summary statuses. These values are derived by SnowBee from its fulfillment and payment records. They are independent of the stored order status and line_status: CLOSED means no further fulfillment, and does not mean fully invoiced.

FieldMeaning
fulfillment.statusPENDING, PARTIALLY_FULFILLED, FULFILLED or CANCELLED
fulfillment.fulfilled_quantityQuantity already handed over or dispatched
fulfillment.pending_quantityQuantity still pending fulfillment
fulfillment.cancelled_quantityRemaining quantity closed without fulfillment
invoicing.statusNOT_INVOICED, PARTIALLY_INVOICED or INVOICED
invoicing.invoiced_quantitySigned invoiced quantity for this sales order line

Fulfillment quantities are positive magnitudes. order_quantity and invoiced_quantity retain their sign, including returns. Use sales_process and returned_sales_order_line_id to identify returns and their original sales order line. Do not add quantities with different units or line types to infer the order summary; use the supplied summary status instead.

For standard fulfillment, shipment records provide dispatched quantity. Invoicing also provides completion evidence for resource lines and closed product lines fulfilled without shipments. Retail checkout uses completed handover payments and its associated invoices; merely belonging to a retail store does not switch a manually entered order to this flow. A pending prepayment is still pending fulfillment, even if the stored line is closed. Gift-card and service-order lines use their typed invoice associations within the owning retail order.

FULFILLED describes handover or dispatch recorded in SnowBee. It is not proof of receipt by a carrier's recipient. Picking, shipment and invoice ID lists allow integrations to follow the underlying documents where corresponding exports are available. Partial fulfillment with a cancelled remainder is represented by separate quantities; no further fulfillment is pending.

Retail prepayments

A product line's prepayment is present when it has a configured target or recorded prepayment history. It remains present after delivery or cancellation, when the current configured target has been cleared. Amounts are gross amounts in the order currency; they are distinct from the full commercial gross line amount.

FieldMeaning
statusPENDING, DELIVERED or CANCELLED
configured_gross_amountCurrent cumulative target; null after it has been cleared
collected_gross_amountCompleted prepayment collections
refunded_gross_amountCompleted prepayment-cancellation refunds, expressed positively
applied_gross_amountCollected prepayment applied to completed handover
unapplied_gross_amountCollected minus refunded minus applied

Only allocations belonging to PAID retail payments contribute to these amounts. Pending, failed and aborted payment attempts do not. Delivery may collect an additional payment; that additional collection is not counted again as a prepayment or as the deposit applied. Paying the configured target does not necessarily pay the full gross line amount.

Net/gross line amounts include persisted line discounts and allocated order discounts. Header totals include recorded eCom freight once. An unpriced line has null commercial amounts; unknown prices are not replaced with zero. Values are decimal strings as elsewhere in Core. Sales prices and commercial amounts use the order currency; unit_cost uses the owning company's currency.

Changes to embedded lines, coupons, subtype details, fulfillment progress, invoice references, prepayment state or click-and-collect references update the sales order's synchronization timestamp. A removed line re-emits its order with that line absent; deleting the order emits a sales-order tombstone. The same overlap-window and periodic-reconciliation guidance applies.

Creating a brand

POST requires master_data:write or core:write:

Reference: POST /brands.

POST /v1/tenants/TENANT_ID/core/brands
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
 
{
  "code": "EXAMPLE",
  "name": "Example brand",
  "status": "ACTIVE"
}

code and name are required strings. status is optional, defaults to ACTIVE, and accepts only ACTIVE or INACTIVE. SnowBee assigns the ID. Unknown request fields, malformed values or missing required fields return 400 INVALID_REQUEST. Duplicate codes or names return 409 CONFLICT. Business validation errors return 422.

Success returns 201 Created with the complete brand and a Location header pointing to /v1/tenants/TENANT_ID/core/brands/{id}. That single-resource GET reads the current brand directly and requires a read scope. Collection GET reads the stored brand directly; allow for read-replica lag when synchronizing immediately after creation.

Brand creation has no general idempotency key. After an uncertain POST result, a retry with the same code or name can return 409; reconcile the existing brand before treating the operation as failed. PATCH and DELETE are not supported in this initial release. Deletions performed through SnowBee are still reported by collection GET.