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.
| Scope | Permitted operations |
|---|---|
core:read | All Core reads |
core:write | All supported Core writes |
master_data:read | Brands and reference master-data reads |
master_data:write | Master-data writes, initially brand creation |
organisational_units:read | Company, warehouse, retail-store and eCom-store reads |
customers:read | Read B2B and B2C customers |
suppliers:read | Read external suppliers |
transactions:read | Sales-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.
| Resource | Supported methods | Required domain scope |
|---|---|---|
/brands | GET, POST | master_data:read / master_data:write |
/retail_chains | GET | master_data:read |
/brands/{id} | GET | master_data:read |
/product_categories | GET | master_data:read |
/main_product_groups | GET | master_data:read |
/sub_product_groups | GET | master_data:read |
/product_concepts | GET | master_data:read |
/product_lifecycle_states | GET | master_data:read |
/sizes | GET | master_data:read |
/sales_order_types | GET | master_data:read |
/reason_codes | GET | master_data:read |
/resources | GET | master_data:read |
/dynamic_attributes | GET | master_data:read |
/vat_groups | GET | master_data:read |
/loyalty_programs | GET | master_data:read |
/organisational_units/companies | GET | organisational_units:read |
/organisational_units/warehouses | GET | organisational_units:read |
/organisational_units/retail_stores | GET | organisational_units:read |
/organisational_units/ecom_stores | GET | organisational_units:read |
/customers/b2b | GET | customers:read |
/customers/b2c | GET | customers:read |
/suppliers/external | GET | suppliers:read |
/transactions/sales_orders | GET | transactions:read |
/transactions/invoices | GET | transactions: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:
| Parameter | Meaning |
|---|---|
since | Optional inclusive ISO 8601 timestamp with an offset; omit to start a full synchronization |
cursor | Opaque continuation token from next_cursor; mutually exclusive with since |
limit | Maximum 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
- Start without
sincefor a full synchronization. For later runs, subtract your configured overlap window from the last completedsync_throughand send that value assince. - Apply every item on the page. Only then persist
next_cursoras your in-progress position. - If
next_cursoris non-null, request the same collection withcursor=NEXT_CURSOR, using URL encoding. Do not sendsincewith it. You may changelimitbetween pages. - When
next_cursoris null, the run is complete. Savesync_throughas the collection checkpoint. - On the next poll, send
since = checkpoint - overlapand 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 / code | Meaning |
|---|---|
400 INVALID_REQUEST | since and cursor supplied together |
400 INVALID_LIMIT | limit is not an integer in 1–500 |
400 INVALID_TIMESTAMP | since is not an ISO 8601 timestamp with an offset |
400 INVALID_CURSOR | Cursor is malformed or uses an unsupported format |
403 FORBIDDEN | Token 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_type | Core collection | Read scope |
|---|---|---|
B2B | /customers/b2b | customers:read |
B2C | /customers/b2c | customers:read |
COMPANY | /organisational_units/companies | organisational_units:read |
RETAIL_STORE | /organisational_units/retail_stores | organisational_units:read |
ECOM_STORE | /organisational_units/ecom_stores | organisational_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.
| Field | Meaning |
|---|---|
fulfillment.status | PENDING, PARTIALLY_FULFILLED, FULFILLED or CANCELLED |
fulfillment.fulfilled_quantity | Quantity already handed over or dispatched |
fulfillment.pending_quantity | Quantity still pending fulfillment |
fulfillment.cancelled_quantity | Remaining quantity closed without fulfillment |
invoicing.status | NOT_INVOICED, PARTIALLY_INVOICED or INVOICED |
invoicing.invoiced_quantity | Signed 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.
| Field | Meaning |
|---|---|
status | PENDING, DELIVERED or CANCELLED |
configured_gross_amount | Current cumulative target; null after it has been cleared |
collected_gross_amount | Completed prepayment collections |
refunded_gross_amount | Completed prepayment-cancellation refunds, expressed positively |
applied_gross_amount | Collected prepayment applied to completed handover |
unapplied_gross_amount | Collected 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.