Catalog Products

2 endpoints
GET /vapi/v1/catalog_products

List the vendor's catalog products

Returns the authenticated vendor's own catalog (every Core::Product it owns), independent of which retailers stock it. This is the endpoint that resolves catalog product ids for PATCH /vapi/v1/catalog_products/:id; the ids are the same core_product_id values referenced by the inventories, manifests and purchase order endpoints. Contrast with GET /vapi/v1/retailers/:retailer_id/products, which returns only the subset one retailer currently stocks. Response envelope: { catalog_products: [ {...} ], meta: {...} }. Supports incremental sync via q[updated_at_gt] / q[updated_at_lt] against core_products.updated_at. Requires the products_access permission on the integrator/vendor connection. Money fields are integer CENTS. The brand, strain and product_type keys are OMITTED entirely (not null) when the product has no such association.

Parameters

Name Type In Required Description
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
include_inactive boolean query No Include catalog products with active=false (default false)
q[updated_at_gt] datestring query No Only products changed after this timestamp (ISO 8601, e.g. 2026-01-01 or 2026-01-01T00:00:00Z)
q[updated_at_lt] datestring query No Only products changed before this timestamp
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/catalog_products" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing products_access permission
422 Invalid updated_at or include_inactive filter

Response Fields

Field Type Required Description
catalog_products
array (object) Yes Catalog products
id
integer Yes Catalog product id (the core_product_id referenced by the inventories, manifests and purchase order endpoints)
name
string No Product name
display_name
string No Display name
description
string No Product description
instructions
string No Usage instructions
ingredients
string No Ingredients
allergens
string No Allergens
flower_type
string No Lineage / flower type
pack_size
integer No Pack size (number of units per pack)
sku_reference
string No The vendor's own SKU reference (read only)
weight
float No Unit weight, in weight_unit
weight_unit
string No Unit for weight (e.g. "gm")
cost
integer No Wholesale cost in CENTS (read only)
min_reorder_qty
integer No Minimum reorder quantity (read only)
active
boolean No Whether the product is active in the vendor's catalog (read only)
brand
object No Brand. KEY OMITTED when the product has no brand
id
integer Yes Record id
name
string No Display name
strain
object No Strain. KEY OMITTED when the product has no strain
id
integer Yes Record id
name
string No Display name
product_type
object No Product type. KEY OMITTED when the product has no type
id
integer Yes Record id
name
string No Display name
thc
float No THC value
cbd
float No CBD value
cover_image_url
string No URL of the cover image, null when the product has no images
images
array (object) No All attached images, cover first
id
integer Yes Image id
url
string No Full size URL
thumb_url
string No Thumbnail URL
created_at
datetime No When the catalog record was created (UTC, ISO 8601)
updated_at
datetime No When the catalog record was last changed (UTC, ISO 8601)
meta
object Yes Pagination state
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected
PATCH /vapi/v1/catalog_products/{id}

Update one catalog product's copy and images

Updates marketing copy and media on a product in the authenticated vendor's catalog. This is the only write endpoint in VAPI. REQUIRES the catalog_write_access permission on the integrator/vendor connection. That permission is OFF by default on every connection and must be enabled per connection by POSaBIT. A connection without it receives 401 "No permission" even though it can read this same collection. Writable attributes -- anything else in the payload is ignored, not rejected: name, display_name, description, instructions, ingredients, allergens, flower_type, pack_size, thc, cbd, weight, weight_unit Costs, stock levels, the active flag, association ids and sku_reference are NOT writable. sku_reference in particular is the key POSaBIT matches incoming deliveries against, and that match is permanent once made. IMAGES ARE PROCESSED ASYNCHRONOUSLY. The URLs are validated and queued during the request, but downloading and attaching them happens afterwards, so the images in the response body reflect the state BEFORE this call. When image URLs (or an empty image_urls list) were supplied the response carries "images_pending": true. Poll this endpoint's index to observe the result. Two image semantics, chosen by which key you send: image_urls REPLACES the product's entire image set. The first entry (or cover_image_url, if also given) becomes the cover. Send this when you are declaring the authoritative media set for the product. An empty list (image_urls: []) removes every image; a null image_urls is ignored. cover_image_url alone is ADDITIVE: the image is added if not already present and promoted to cover; existing images are kept. A URL already known to POSaBIT is de-duplicated rather than re-downloaded, so re-sending an unchanged media kit is cheap and does not churn the images. Changing `name` changes the displayed name only. It does not change the normalized key POSaBIT matches inventory against, so a rename will not re-link or un-link a product from retailer SKUs. A description or image change propagates to retailer-level products for retailers whose merchant has catalog sync enabled for this vendor. Retailers who have manually overridden a field keep their override.

Parameters

Name Type In Required Description
id integer path Yes Catalog product id
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
catalog_product[description] string form No Product description
catalog_product[name] string form No Product name (display only; see notes)
catalog_product[cover_image_url] string form No http/https URL of the cover image
catalog_product[image_urls] array form No http/https URLs; REPLACES the image set (max 10); an empty list removes every image

Example

curl -X PATCH "https://app.posabit.com/vapi/v1/catalog_products/{id}" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{...}'

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing catalog_write_access permission
404 No such product in this vendor's catalog
422 Validation failure, or a malformed/unsupported image URL

Response Fields

Field Type Required Description
catalog_product
object Yes The updated product
id
integer Yes Catalog product id (the core_product_id referenced by the inventories, manifests and purchase order endpoints)
name
string No Product name
display_name
string No Display name
description
string No Product description
instructions
string No Usage instructions
ingredients
string No Ingredients
allergens
string No Allergens
flower_type
string No Lineage / flower type
pack_size
integer No Pack size (number of units per pack)
sku_reference
string No The vendor's own SKU reference (read only)
weight
float No Unit weight, in weight_unit
weight_unit
string No Unit for weight (e.g. "gm")
cost
integer No Wholesale cost in CENTS (read only)
min_reorder_qty
integer No Minimum reorder quantity (read only)
active
boolean No Whether the product is active in the vendor's catalog (read only)
brand
object No Brand. KEY OMITTED when the product has no brand
id
integer Yes Record id
name
string No Display name
strain
object No Strain. KEY OMITTED when the product has no strain
id
integer Yes Record id
name
string No Display name
product_type
object No Product type. KEY OMITTED when the product has no type
id
integer Yes Record id
name
string No Display name
thc
float No THC value
cbd
float No CBD value
cover_image_url
string No URL of the cover image, null when the product has no images
images
array (object) No All attached images, cover first
id
integer Yes Image id
url
string No Full size URL
thumb_url
string No Thumbnail URL
created_at
datetime No When the catalog record was created (UTC, ISO 8601)
updated_at
datetime No When the catalog record was last changed (UTC, ISO 8601)
images_pending
boolean No Present and true when image work was queued (URLs, or an empty image_urls clearing the set); the images in this payload are the pre-update set

Customer Sales

1 endpoints
GET /vapi/v1/retailers/{retailer_id}/customer_sales

Get item-level sales with customer demographics for a retailer

Returns one row per sold line item from Redshift, each with the anonymized customer demographics shown on the monthly vendor Sales Export. Accepts a custom date range via start_date/end_date (ISO 8601, e.g. 2026-01-01). The range must not exceed 31 days. Defaults to the last 30 days. customer.id is an HMAC digest scoped to the authenticated vendor: stable across this vendor's rows, but not reversible and not comparable against any other feed. Full date of birth is never returned -- birth_year and the age at time of order are. Money values are USD dollars. Only settled orders (COMPLETE, DELIVERED) are included, so counts will not tie out exactly against the monthly CSV export.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
start_date string query No Start date (ISO 8601, e.g. 2026-01-01)
end_date string query No End date (ISO 8601, e.g. 2026-01-31)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/customer_sales" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Unauthorized
404 Not Found
422 Invalid or out-of-range dates

Inventories

1 endpoints
GET /vapi/v1/retailers/{retailer_id}/inventories

Get VMI inventory data for a retailer

Returns the vendor's inventory position at one retailer, computed from the nightly warehouse rollup. Response envelope: { meta: {...}, inventory: [ {...} ] }. Each element matches `meta.venue_license_number` is the venue's STS license number — the recommended key for matching venues across systems. VendorInventoryItem; meta matches VendorInventoryMeta. Rows are ordered by the retailer SKU's updated_at, most recently touched first. The underlying rollup is capped at 3000 SKUs per retailer, so a vendor above that ceiling loses its stalest SKUs rather than an arbitrary slice. RECOMMENDED: pass exclude_inactive=true. Most integrations want the SKUs the retailer actually carries, and that is what this returns -- dropping archived SKUs, SKUs the retailer deactivated, and SKUs with no inventory activity in 90 days. It applies the same definition as the "Inactive" filter in the vendor portal, so the API and the portal agree on what is live. The default is false only because it predates the filter and changing it would alter existing integrations without warning. Left off, the response also includes SKUs the retailer stopped carrying long ago, which still carry a genuine quantity_on_hand and cost -- expect a materially larger payload and a lower-quality one. Every row carries the three fields the filter decides on, so a client that leaves it off can still classify each SKU itself. A SKU is inactive when ANY of the following holds: archived == true sku_is_active == false updated_at older than 90 days, or absent Use exclude_inactive=false when you specifically want the full historical set -- reconciling against a catalog the retailer has since pruned, or applying your own staleness window to updated_at instead of the 90 days used here. Supports incremental sync via q[updated_at_gt], which matches inventory_skus.updated_at -- the retailer SKU's stock position. Receiving, selling, or adjusting a SKU advances it. IMPORTANT: unlike the other VAPI collections, the rows here are computed, not stored, so this filter is narrower than "everything that changed since :gt". The window-derived fields (velocity, days_supply, weeks_of_supply, pct_days_in_stock, and the sold_* totals) are recalculated against a window that ends today, so they move as the window slides even when the SKU itself was untouched. An integrator that needs those figures current has to re-pull the collection on its own schedule; polling with q[updated_at_gt] alone will not surface that drift. Requires the inventories_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". RATE LIMIT: 10 requests per minute per integrator token, counted against this endpoint separately from the sales endpoint. Exceeding it returns 429 with { "error": "Rate limit exceeded" } and a Retry-After: 60 header. Note that the 429 body uses an "error" key, while every other VAPI error uses "message". Money fields are integer CENTS unless the field description says otherwise. Quantities are decimals.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
sales_range_in_weeks integer query No Sales lookback in weeks (default 4)
q[updated_at_gt] datestring query No Only SKUs whose stock position changed after this timestamp (ISO 8601, e.g. 2026-01-01 or 2026-01-01T00:00:00Z). Matches inventory_skus.updated_at
exclude_inactive boolean query No RECOMMENDED: set to true. Drops SKUs the retailer has stopped carrying -- archived SKUs, SKUs the retailer deactivated, and SKUs with no inventory activity in the last 90 days -- leaving what the retailer actually stocks. Defaults to false for backward compatibility, which returns long-dead SKUs indistinguishable from live ones
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/inventories" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing inventories_access permission
404 Retailer not found, or not accessible to this vendor
422 Invalid updated_at filter, or exclude_inactive was not a boolean
429 Rate limit exceeded (10/minute per integrator token)

Response Fields

Field Type Required Description
meta
object Yes Pagination state plus the report window and retailer identity
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected
sales_range_in_weeks
integer Yes Sales lookback applied to velocity and sold_* fields
period_start
string Yes First day of the sales window (ISO 8601 date)
period_end
string Yes Last day of the sales window (ISO 8601 date)
retailer_name
string No Retailer name
venue_name
string No Venue name (null if the retailer has no linked venue)
inventory
array (object) Yes Inventory rows
core_product_id
integer No Vendor catalog product id (matches id from the products endpoint)
sku_reference
string No Retailer's SKU barcode
sku_id
integer No Retailer's inventory SKU id
core_product_name
string No Product name in the vendor's catalog
retailer_product_name
string No Product name as the retailer has it
product_type_name
string No Product type / category name
product_type_color
string No Product type display color (hex)
brand_name
string No Brand name
strain_name
string No Strain name
lineage
string No Strain lineage (e.g. indica, sativa, hybrid)
quantity_on_hand
float No Units on hand at the retailer
sellable_quantity
float No Units available to sell
cost
float No Cost in CENTS, from the vendor catalog
sku_cost
float No Cost in CENTS the retailer recorded for this SKU
velocity
float No Average units sold per day over the sales window
days_supply
float No Days of stock remaining at current velocity. NULL when velocity is zero (supply is unbounded)
weeks_of_supply
float No days_supply / 7, rounded to 1 decimal. NULL when days_supply is null
pct_days_in_stock
float No Percentage of days in the window the SKU had stock on hand (0-100)
avg_retail_price_per_unit
float No Average retail price per unit sold, in CENTS. 0.0 when nothing sold
min_reorder_qty
integer No Minimum reorder quantity from the vendor catalog
optimum_level
integer No Retailer's target stock level
reorder_threshold
integer No Retailer's reorder trigger level
last_received_date
datetime No When the retailer last received this SKU
last_received_quantity
float No Units received on that last receipt
last_order_at
datetime No When this SKU last sold
units_sold
float No Units sold during the sales window
sold_cost_amount
float No Cost of goods sold during the window, in CENTS
sold_price_amount
float No Gross retail sales during the window, in CENTS
sold_discount_amount
float No Discounts applied during the window, in CENTS
updated_at
datetime No When the retailer's SKU last changed (UTC, ISO 8601) -- receiving, selling, or adjusting it advances this. Feed the value back as q[updated_at_gt] to poll forward. Does NOT move when only the window-derived metrics change
sku_is_active
boolean No False once the retailer stops ordering this SKU
archived
boolean No True once the retailer archives the underlying product

Manifests

2 endpoints
GET /vapi/v1/retailers/{retailer_id}/manifests

Get manifests for a retailer

Returns the vendor's inbound transfer manifests to this retailer, newest first. Response envelope: { manifests: [ {...} ], meta: {...} }. Each element matches VendorManifestSummary; meta matches PaginationMeta. Accepted manifests with a zero accepted_cost are treated as samples and excluded from this collection. Supports incremental sync via q[updated_at_gt] / q[updated_at_lt]. manifests.updated_at advances on the changes an integrator cares about (acceptance, state, costs, transfer date); a manifest_item saved on its own does not touch the header, so poll #show for line-item detail. Requires the manifests_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". Money fields are integer CENTS.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
q[updated_at_gt] datestring query No Only manifests updated after this timestamp (ISO 8601, e.g. 2026-01-01 or 2026-01-01T00:00:00Z)
q[updated_at_lt] datestring query No Only manifests updated before this timestamp (ISO 8601)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/manifests" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing manifests_access permission
404 Retailer not found, or not accessible to this vendor
422 Invalid updated_at filter

Response Fields

Field Type Required Description
manifests
array (object) Yes Manifests
id
integer Yes Manifest id
reference_no
string No Manifest reference number
state
string No Workflow state (e.g. pending, incoming, accepted)
transfer_date
string No Date of transfer (ISO 8601 date)
delivery_time
datetime No Scheduled delivery time
accepted_at
datetime No When the retailer accepted the manifest. NULL until accepted
accepted_quantity
float No Units the retailer accepted
accepted_cost
integer No Accepted value in CENTS
supplier_name
string No Supplier name. NULL if the manifest has no linked supplier
supplier_license
string No Supplier license number. NULL if the manifest has no linked supplier
created_at
datetime No When the manifest was created (UTC, ISO 8601). Results are ordered by this field, descending
updated_at
datetime No When the manifest header last changed (UTC, ISO 8601). Line-item-only edits do not advance it
meta
object Yes Pagination state
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected
GET /vapi/v1/retailers/{retailer_id}/manifests/{id}

Get manifest details with line items

Returns one manifest with its line items. Response envelope: { manifest: {...} }, matching VendorManifestDetail. Use this action rather than #index to observe line-item changes, which do not advance the manifest header's updated_at. Requires the manifests_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". Money fields are integer CENTS.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
id integer path Yes Manifest ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/manifests/{id}" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing manifests_access permission
404 Manifest or retailer not found, or not accessible to this vendor

Response Fields

Field Type Required Description
manifest
object Yes The manifest
id
integer Yes Manifest id
reference_no
string No Manifest reference number
state
string No Workflow state (e.g. pending, incoming, accepted)
transfer_date
string No Date of transfer (ISO 8601 date)
delivery_time
datetime No Scheduled delivery time
pickup_time
datetime No Scheduled pickup time
accepted_at
datetime No When the retailer accepted the manifest. NULL until accepted
accepted_by
string No Who accepted the manifest
accepted_quantity
float No Units the retailer accepted
accepted_cost
integer No Accepted value in CENTS
received_quantity
float No Units received
received_cost
integer No Received value in CENTS
discount
integer No Discount applied in CENTS
notes
string No Free-text notes
supplier_name
string No Supplier name. NULL if the manifest has no linked supplier
supplier_license
string No Supplier license number. NULL if the manifest has no linked supplier
driver_name
string No Transport driver name
license_plate
string No Transport vehicle license plate
vehicle_make
string No Transport vehicle make
vehicle_model
string No Transport vehicle model
created_at
datetime No When the manifest was created (UTC, ISO 8601)
updated_at
datetime No When the manifest header last changed (UTC, ISO 8601)
items
array (object) No Line items
id
integer Yes Line item id
name
string No Product name as it appears on the manifest
barcode
string No Retailer SKU barcode. NULL if the item is not linked to an inventory SKU
quantity
float No Units on the manifest
received_quantity
float No Units received
cost
integer No Unit cost in CENTS
lot_number
string No Lot / batch number
expires_on
string No Expiry date (ISO 8601 date)
core_product_id
integer No Vendor catalog product id (matches id from the products endpoint). NULL if the item is not linked through to a catalog product

Products

1 endpoints
GET /vapi/v1/retailers/{retailer_id}/products

Get core products carried by a retailer

Returns the vendor's catalog products that this retailer currently stocks on at least one active SKU. Response envelope: { products: [ {...} ], meta: {...} }. Each element matches VendorCatalogProduct; meta matches PaginationMeta. Supports incremental sync via q[updated_at_gt]. The filter matches a change to either the catalog product or the retailer's stocking of it, so a product the retailer began carrying since the last poll is returned even though the catalog record itself did not change. Requires the products_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". Each product includes last_sale_date: the UTC date of its most recent completed sale across all POSaBIT retailers (not just the requested retailer), null if it has never sold. The brand, strain and product_type keys are OMITTED entirely (not null) when the product has no such association. Money fields are integer CENTS.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
q[updated_at_gt] datestring query No Only products whose catalog record or retailer SKU was updated after this timestamp (ISO 8601, e.g. 2026-01-01 or 2026-01-01T00:00:00Z)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/products" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing products_access permission
404 Retailer not found, or not accessible to this vendor
422 Invalid updated_at filter

Response Fields

Field Type Required Description
products
array (object) Yes Catalog products
id
integer Yes Catalog product id (the core_product_id referenced by the inventories, manifests and purchase order endpoints)
name
string No Product name
description
string No Product description
weight
float No Unit weight, in weight_unit
weight_unit
string No Unit for weight (e.g. "gm")
cost
integer No Wholesale cost in CENTS
min_reorder_qty
integer No Minimum reorder quantity
active
boolean No Whether the product is active in the vendor's catalog
brand
object No Brand. KEY OMITTED when the product has no brand
id
integer Yes Record id
name
string No Display name
strain
object No Strain. KEY OMITTED when the product has no strain
id
integer Yes Record id
name
string No Display name
product_type
object No Product type. KEY OMITTED when the product has no type
id
integer Yes Record id
name
string No Display name
thc
float No THC value from the catalog record
cbd
float No CBD value from the catalog record
created_at
datetime No When the catalog record was created (UTC, ISO 8601)
updated_at
datetime No When the catalog record was last changed (UTC, ISO 8601). Does NOT move when only the retailer's SKU changes -- use the q[updated_at_gt] filter, which covers both
meta
object Yes Pagination state
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected

Purchase Orders

1 endpoints
Purchase Orders allow vendors to manage and track orders from their retailers. View all incoming purchase orders, update their status, and communicate directly with retailers through the POSaBIT portal.
GET /vapi/v1/retailers/{retailer_id}/purchase_orders

Get purchase orders for a retailer

Returns purchase orders the retailer has raised against this vendor, newest first. Response envelope: { purchase_orders: [ {...} ], meta: {...} }. Each element matches VendorPurchaseOrder; meta matches PaginationMeta. Supports incremental sync via q[updated_at_gt] / q[updated_at_lt]. Note that purchase_orders.updated_at tracks the order header (state, totals, ref_number); an edit confined to a line item does not advance it. Requires the purchase_orders_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". Money fields are integer CENTS.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
q[updated_at_gt] datestring query No Only purchase orders updated after this timestamp (ISO 8601, e.g. 2026-01-01 or 2026-01-01T00:00:00Z)
q[updated_at_lt] datestring query No Only purchase orders updated before this timestamp (ISO 8601)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/purchase_orders" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing purchase_orders_access permission
404 Retailer not found, or not accessible to this vendor
422 Invalid updated_at filter

Response Fields

Field Type Required Description
purchase_orders
array (object) Yes Purchase orders
id
integer Yes Purchase order id
ref_number
string No Retailer-facing reference number
state
string No Workflow state (e.g. draft, submitted, accepted)
items_count
integer No Number of line items
created_at
datetime No When the order was created (UTC, ISO 8601). Results are ordered by this field, descending
updated_at
datetime No When the order header last changed (UTC, ISO 8601). Line-item-only edits do not advance it
items
array (object) No Line items
id
integer Yes Line item id
core_product_id
integer No Vendor catalog product id (matches id from the products endpoint)
quantity
integer No Units ordered
cost
integer No Unit cost in CENTS
meta
object Yes Pagination state
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected

Reports

4 endpoints
Vendor-level reports aggregate across all of your connected retailers rather than a single store, so they are not nested under a retailer id. They read from a nightly rollup, which means figures are complete through yesterday. Money values are USD dollars and share values are 0-1 ratios; format them as percentages client-side. Heavier sections are opt-in through the include parameter, so the default response stays small: you get the totals plus one row per retailer, each with a single blended share. To get a breakdown by product family, pass include=families — for example GET /vapi/v1/reports/sales_share_by_retailer?start_date=2026-05-01&end_date=2026-06-30&include=families — which adds a families array to every retailer row, one entry per product family (Flower, Preroll, Concentrate, …) carrying that family's vendor_cog and its share of the retailer's sales in that family. A null share means the retailer had no sales at all in that family for the period, so there was no denominator to divide by; render it as "no data" rather than 0%. include=trend adds the month-by-month vendor and total series, and the two combine comma-separated (include=families,trend) to return both in one call — that combination is what the Response Example below shows.
GET /vapi/v1/reports/inventory_lots

Run the vendor Custom Inventory Lots report

Returns inventory lot metrics from Redshift, grouped by the requested dimensions. group_by is required; an invalid value returns 422 with the list of valid fields. Accepts a custom date range via start_date/end_date (ISO 8601); defaults to the last 30 days. Optional filters: core_brand_ids[], core_strain_ids[], core_product_type_ids[], core_product_flower_types[], product_family_ids[], core_product_name_cont, venue_ids[], merchant_ids[], lot_statuses[], lot_age_gt/lot_age_lt, lot_qty_gt/lot_qty_lt, lot_weight_in_gm_gt/lot_weight_in_gm_lt, sku_no_order_since, row_limit (max 3000).

Parameters

Name Type In Required Description
group_by string query Yes Report dimensions, comma-separated or group_by[]= (e.g. core_brand_name,lot_status)
start_date string query No Start date (ISO 8601, e.g. 2026-01-01)
end_date string query No End date (ISO 8601, e.g. 2026-01-31)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000)

Example

curl -X GET "https://app.posabit.com/vapi/v1/reports/inventory_lots" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Unauthorized
422 Invalid group_by fields or dates, or a filter this report does not support
GET /vapi/v1/reports/sales_discounts

Run the vendor Custom Sales Discounts report

Returns discount/coupon metrics from Redshift, grouped by the requested dimensions. group_by is required; an invalid value returns 422 with the list of valid fields. Accepts a custom date range via start_date/end_date (ISO 8601); defaults to the last 30 days. Optional filters: core_brand_ids[], core_strain_ids[], core_product_type_ids[], core_product_flower_types[], product_family_ids[], core_product_name_cont, venue_ids[], merchant_ids[], doh_compliant, order_types[], sources[], origins[], payments[], row_limit (max 3000).

Parameters

Name Type In Required Description
group_by string query Yes Report dimensions, comma-separated or group_by[]= (e.g. discount_name,order_date)
start_date string query No Start date (ISO 8601, e.g. 2026-01-01)
end_date string query No End date (ISO 8601, e.g. 2026-01-31)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000)

Example

curl -X GET "https://app.posabit.com/vapi/v1/reports/sales_discounts" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Unauthorized
422 Invalid group_by fields or dates, or a filter this report does not support
GET /vapi/v1/reports/sales_items

Run the vendor Custom Sales Items report

Returns sales item metrics from Redshift, grouped by the requested dimensions. group_by is required; an invalid value returns 422 with the list of valid fields. Accepts a custom date range via start_date/end_date (ISO 8601); defaults to the last 30 days. Optional filters: core_brand_ids[], core_strain_ids[], core_supplier_ids[], core_product_type_ids[], core_product_flower_types[], product_family_ids[], core_product_name_cont, venue_ids[], merchant_ids[], doh_compliant, order_types[], sources[], origins[], payments[], row_limit (max 3000).

Parameters

Name Type In Required Description
group_by string query Yes Report dimensions, comma-separated or group_by[]= (e.g. core_brand_name,order_date)
start_date string query No Start date (ISO 8601, e.g. 2026-01-01)
end_date string query No End date (ISO 8601, e.g. 2026-01-31)
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000)

Example

curl -X GET "https://app.posabit.com/vapi/v1/reports/sales_items" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Unauthorized
422 Invalid group_by fields or dates, or a filter this report does not support
GET /vapi/v1/reports/sales_share_by_retailer

Sales Share by Retailer report for the authenticated vendor

Returns the vendor's cost-of-goods share of each retailer's total sales, computed from the nightly sales rollup (data through yesterday). Accepts a custom date range via start_date/end_date (ISO 8601, e.g. 2026-01-01); the range must not exceed 366 days and defaults to the last 30 days. Dates are interpreted in the VENDOR's local timezone (falling back to Pacific Time), not UTC and not the retailer's timezone. Pass include=families,trend to opt into the per-family breakdown and monthly trend. Both are omitted from the response unless requested. Money values on this endpoint are USD DOLLARS, rounded to 2 decimals -- unlike the retailer collections (inventories, sales, products, purchase orders, manifests), which return integer CENTS. Share values are 0-1 ratios. Pagination applies to the data array only; totals covers every retailer matching the filters. Pagination state is reported under meta (consistent with the other VAPI collections) and, for backwards compatibility, is duplicated under the original top-level pagination key. Prefer meta; treat pagination as deprecated. Requires the sales_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission".

Parameters

Name Type In Required Description
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
start_date string query No Start date (ISO 8601, e.g. 2026-01-01). Interpreted in the vendor's local timezone. Required if end_date is given
end_date string query No End date (ISO 8601, e.g. 2026-01-31). Interpreted in the vendor's local timezone. Required if start_date is given
merchant_ids string query No Comma-separated merchant IDs to filter
retailer_ids string query No Comma-separated retailer IDs to filter (the ids returned by /vapi/v1/retailers)
product_family_ids string query No Comma-separated product family IDs to filter
brand_ids string query No Comma-separated core brand IDs to filter (vendor side only)
include string query No Optional sections: families, trend (comma-separated)
page integer query No Page number (default 1)
per_page integer query No Retailer rows per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/reports/sales_share_by_retailer" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing sales_access permission
422 Invalid or out-of-range dates

Response Fields

Field Type Required Description
meta
object Yes Report identity and window
report
string Yes Always "sales_share_by_retailer"
vendor_id
integer Yes Authenticated vendor id
period
object Yes Window the report covers
start_date
string Yes First day of the window (ISO 8601 date)
end_date
string Yes Last day of the window (ISO 8601 date)
currency
string Yes Always "USD"
generated_at
string Yes When this response was produced (UTC, ISO 8601)
page
integer No Current page number (1-based). Mirrors pagination.page
per_page
integer No Rows per page. Mirrors pagination.per_page
total
integer No Total retailer rows across all pages. Mirrors pagination.total
total_pages
integer No Total number of pages. Mirrors pagination.total_pages
max_per_page
integer No Ceiling on per_page. Mirrors pagination.max_per_page
totals
object Yes Roll-up across every retailer matching the filters
vendor_cog
float Yes The vendor's cost of goods sold, in DOLLARS
total_cog
float Yes All vendors' cost of goods sold at these retailers, in DOLLARS
vendor_share
float No vendor_cog / total_cog as a 0-1 ratio. NULL when total_cog is zero
retailers_with_sales
integer No Retailers that recorded any sales in the window
vendor_share_prior_period
float No Same ratio for the immediately preceding window of equal length. NULL when unavailable
vendor_share_change_pct
float No Percentage-point change in share against the prior period. NULL when unavailable
data
array (object) Yes One row per retailer, for the requested page
retailer
object Yes Retailer reference
id
integer Yes Retailer id (matches /vapi/v1/retailers)
name
string No Retailer name
merchant
object Yes Merchant that operates the retailer
id
integer No Merchant id. NULL when the retailer's venue has no merchant on record
name
string No Merchant name
vendor_cog
float Yes The vendor's cost of goods sold at this retailer, in DOLLARS
total_cog
float Yes All vendors' cost of goods sold at this retailer, in DOLLARS
vendor_share
float No vendor_cog / total_cog as a 0-1 ratio. NULL when total_cog is zero
families
array (object) No Per product family breakdown. PRESENT ONLY when include=families was requested
product_family
string Yes Product family name
vendor_cog
float Yes The vendor's cost of goods sold in this family, in DOLLARS
share
float No This family's share of the retailer's total, as a 0-1 ratio. NULL when the denominator is zero
trend
object No Monthly trend. PRESENT ONLY when include=trend was requested
months
array Yes Month labels, oldest first
families
array No Product family names present in the trend
vendor
array No The vendor's cost of goods sold per month, in DOLLARS
total
array No All vendors' cost of goods sold per month, in DOLLARS
pagination
object Yes Pagination state
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected

Response Example

{
  "meta": {
    "report": "sales_share_by_retailer",
    "vendor_id": 1,
    "period": {
      "start_date": "2026-05-01",
      "end_date": "2026-06-30"
    },
    "currency": "USD",
    "generated_at": "2026-07-01T18:22:04Z"
  },
  "totals": {
    "vendor_cog": 47916.04,
    "total_cog": 125166.63,
    "vendor_share": 0.3828,
    "retailers_with_sales": 124,
    "vendor_share_prior_period": 0.4098,
    "vendor_share_change_pct": -6.6
  },
  "data": [
    {
      "retailer": {
        "id": 3221,
        "name": "The Bong Marche"
      },
      "merchant": {
        "id": 5,
        "name": "POSaBIT Demo"
      },
      "vendor_cog": 4227.0,
      "total_cog": 9675.0,
      "vendor_share": 0.4369,
      "families": [
        {
          "product_family": "Flower",
          "vendor_cog": 1709.0,
          "share": 0.425
        },
        {
          "product_family": "Preroll",
          "vendor_cog": 2518.0,
          "share": 0.602
        },
        {
          "product_family": "Concentrate",
          "vendor_cog": 0.0,
          "share": null
        },
        {
          "product_family": "Uncategorized",
          "vendor_cog": 0.0,
          "share": 0.0
        }
      ]
    },
    {
      "retailer": {
        "id": 4102,
        "name": "Green Vault"
      },
      "merchant": {
        "id": 12,
        "name": "Cascade Holdings"
      },
      "vendor_cog": 2891.5,
      "total_cog": 14350.25,
      "vendor_share": 0.2015,
      "families": [
        {
          "product_family": "Flower",
          "vendor_cog": 1420.0,
          "share": 0.231
        },
        {
          "product_family": "Preroll",
          "vendor_cog": 1105.5,
          "share": 0.348
        },
        {
          "product_family": "Concentrate",
          "vendor_cog": 366.0,
          "share": 0.091
        },
        {
          "product_family": "Uncategorized",
          "vendor_cog": 0.0,
          "share": 0.0
        }
      ]
    }
  ],
  "trend": {
    "months": [
      "May 2026",
      "June 2026"
    ],
    "families": [
      "Flower",
      "Preroll",
      "Concentrate",
      "Uncategorized"
    ],
    "vendor": {
      "Flower": [
        11240.5,
        11890.25
      ],
      "Preroll": [
        9875.0,
        10420.6
      ],
      "Concentrate": [
        2180.44,
        2309.25
      ],
      "Uncategorized": [
        0.0,
        0.0
      ]
    },
    "total": {
      "Flower": [
        29500.0,
        30880.13
      ],
      "Preroll": [
        21400.0,
        22990.5
      ],
      "Concentrate": [
        9800.0,
        10120.0
      ],
      "Uncategorized": [
        240.0,
        236.0
      ]
    }
  },
  "pagination": {
    "page": 1,
    "per_page": 100,
    "total": 124,
    "total_pages": 2
  }
}

Retailers

2 endpoints
GET /vapi/v1/retailers

List all active retailers for the vendor

Returns every retailer the vendor has active, access-granted, live-venue access to. Response envelope: { meta: {...}, retailers: [ {...} ] }, each retailer element matching VendorRetailerSummary. The id returned here is the retailer_id path segment for the inventories, sales, products, purchase orders and manifests endpoints, and the value accepted by the sales share report's retailer_ids filter. `name` is the venue's name and `license_number` is the venue's STS license number, the recommended key for matching venues across systems. Requires the retailers_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". Not paginated: all accessible retailers are returned in one response, sorted by name (case-insensitive). meta.total is the number of rows in this response. meta.total_connections counts every retailer connection on record for the vendor, including any currently hidden because the retailer is inactive, retailer access is off, or the venue is inactive. A gap between the two means access was withdrawn, not that the connection was deleted.

Parameters

Name Type In Required Description
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing retailers_access permission

Response Fields

Field Type Required Description
meta
object Yes Row and connection totals
total
integer Yes Retailers in this response
total_connections
integer Yes Every retailer connection on record for the vendor, visible or not
retailers
array (object) Yes Retailers
id
integer Yes Retailer id -- the retailer_id path segment used by the other endpoints
name
string No Retailer name -- the venue's name
venue_id
integer No Underlying POSaBIT venue id
license_number
string No Venue STS license number -- the recommended cross-system match key
GET /vapi/v1/retailers/{id}

Get retailer details with inventory metrics

Returns one retailer with a snapshot of the vendor's inventory position there. Response envelope: { retailer: {...} }, matching VendorRetailerDetail. `name` mirrors the venue's name; `venue.license_number` is the venue's STS license number — the recommended key for matching venues across systems. The metrics object is computed on demand. When no metrics are available for the pairing, every metric is returned as NULL rather than the object being omitted or zero-filled. Requires the retailers_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". Money fields are integer CENTS.

Parameters

Name Type In Required Description
id integer path Yes Retailer ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{id}" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing retailers_access permission
404 Retailer not found, inactive, or not accessible to this vendor

Response Fields

Field Type Required Description
retailer
object Yes The retailer
id
integer Yes Retailer id
name
string No Retailer name
active
boolean No Whether the vendor/retailer link is active
venue_id
integer No Underlying POSaBIT venue id
venue
object No Venue reference. KEY OMITTED when the retailer has no linked venue
id
integer Yes Venue id
name
string No Venue name
metrics
object Yes Inventory metrics. All members are NULL when no metrics are available
active_lot_count
integer No Lots currently in stock
samples_count
integer No Sample lots received
samples_on_hand
float No Sample units still on hand
skus_count
integer No Distinct SKUs of the vendor's the retailer carries
inventory_value
integer No Value of the vendor's inventory at the retailer, in CENTS
unmapped_products
integer No Retailer products not linked to a vendor catalog product
active_lots
integer No Lots with stock on hand
pending_lots
integer No Lots on manifests not yet accepted
aged_lots
integer No Lots held beyond the aging threshold
recent_products
integer No Products first stocked recently

Sales

1 endpoints
GET /vapi/v1/retailers/{retailer_id}/sales

Get sales data for a retailer

Returns the vendor's daily sales at one retailer, aggregated by product type. Response envelope: { meta: {...}, sales: [ {...} ] }. Each element matches VendorSalesRow; meta matches PaginationMeta. Accepts a custom date range via start_date/end_date (ISO 8601, e.g. 2025-01-01). The range must not exceed 31 days and defaults to the last 30 days. Dates are interpreted in the RETAILER VENUE's local timezone (falling back to America/Los_Angeles), not UTC and not the vendor's timezone. Days with no sales are returned as zero-filled rows rather than omitted, so the row count is (days in range x product types), not the number of days that had sales. Requires the sales_access permission on the integrator/vendor connection. A connection without it receives 401 "No permission". RATE LIMIT: 10 requests per minute per integrator token, counted against this endpoint separately from the inventories endpoint. Exceeding it returns 429 with { "error": "Rate limit exceeded" } and a Retry-After: 60 header. Note that the 429 body uses an "error" key, while every other VAPI error uses "message". Money fields are integer CENTS.

Parameters

Name Type In Required Description
retailer_id integer path Yes Retailer ID
vendor_id integer query No Vendor ID (alternative to supplying vendor_token in the auth header)
start_date string query No Start date (ISO 8601, e.g. 2025-01-01). Required if end_date is given
end_date string query No End date (ISO 8601, e.g. 2025-01-31). Required if start_date is given
page integer query No Page number (default 1)
per_page integer query No Results per page (default 100, max 1000; larger values are clamped, not rejected)

Example

curl -X GET "https://app.posabit.com/vapi/v1/retailers/{retailer_id}/sales" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid integrator token, invalid vendor, or missing sales_access permission
404 Retailer not found, or not accessible to this vendor
422 Invalid or out-of-range dates
429 Rate limit exceeded (10/minute per integrator token)

Response Fields

Field Type Required Description
meta
object Yes Pagination state
page
integer Yes Current page number (1-based)
per_page
integer Yes Rows per page
total
integer Yes Total rows matching the request across all pages
total_pages
integer Yes Total number of pages
max_per_page
integer Yes Ceiling on per_page. A larger requested value is clamped to this rather than rejected
sales
array (object) Yes Sales rows
order_date
string Yes Day of the sale (ISO 8601 date, in the retailer venue's timezone)
iso_week
integer No ISO week number of order_date
iso_year
integer No ISO year of order_date
product_type_name
string No Product type / category name
product_type_color
string No Product type display color (hex)
total_quantity
float No Units sold
total_price
float No Gross retail sales in CENTS
total_profit
float No Retailer margin in CENTS (price less credits, discounts and cost)
total_cost
float No Cost of goods sold in CENTS

Vendors

1 endpoints
GET /vapi/v1/vendors

List vendors connected to the integrator

Returns every active vendor connection for the integrator. Response envelope: { vendors: [ {...} ] }, each element matching ConnectedVendor. This is the discovery endpoint: unlike every other VAPI endpoint it needs only the integrator token (send the header with an empty vendor half, base64("<integrator_token>:")), and it requires no data_access permission. Each row includes that connection's vendor token. TREAT THE RESPONSE AS CREDENTIAL MATERIAL -- the token values are what authenticate every other endpoint. Not paginated: all active connections are returned in one response.

Example

curl -X GET "https://app.posabit.com/vapi/v1/vendors" \
  -H "Authorization: Basic BASE64(INTEGRATOR_TOKEN:VENDOR_TOKEN)" \
  -H "Accept: application/json"

Responses

Status Description
200 Success
401 Invalid or inactive integrator token

Response Fields

Field Type Required Description
vendors
array (object) Yes Connected vendors
id
integer Yes Vendor id (pass as the vendor_id query param on other endpoints)
name
string No Vendor name
status
string No Vendor account status
email
string No Contact email
phone
string No Contact phone
contact_name
string No Primary contact name
address
string No Street address
city
string No City
state
string No State
zip
string No Postal code
website
string No Website URL
description
string No Vendor description
logo
string No Logo URL. NULL when no logo is attached
territory
string No Sales territory
timezone
string No Vendor's local timezone (the zone the sales share report's dates are interpreted in)
retailers_count
integer No Number of retailers linked to the vendor
suppliers_count
integer No Number of suppliers linked to the vendor
token
string Yes CREDENTIAL. This connection's vendor token -- the vendor half of the Basic auth header on every other endpoint