Vendor API V1
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 endpointsPurchase 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 endpointsVendor-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 |
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
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 |