Skip to content

Resources

All paths are relative to https://crm.h-profit.com/bapi-v2. Every endpoint needs the Authorization header and the matching scope.

Many list endpoints support ?include= to embed related objects — see the relations table. To fetch several resources at once, use the composite /query.


Reference

Everything an integrator needs to build related objects (accounts, currencies, categories, brands, warehouses, channels, delivery services) in one call.

Method Path Scope
GET /reference reference:read

?types=accounts,currencies,warehouses narrows the response to specific sections: accounts, currencies, product_categories, expense_categories, brands, warehouses, sales_channels, carrier_accounts, write_off_categories.

carrier_accounts are the account's connected delivery services; id is the account_id for Delivery lookups:

{ "id": 427, "carrier": "meest", "name": "Main sender" }

carrier is nova_poshta, ukrposhta or meest. A Nova Poshta account also lists its senders — the senders with a city and a branch set up in HugeProfit; id is the sender_id for creating a waybill:

{ "id": 431, "carrier": "nova_poshta", "name": "Shop",
  "senders": [ { "id": "3f6c1a20-…_3f71b5c8-…", "name": "Ivan Petrenko",
                 "warehouse_name": "Kyiv, Branch №1" } ] }

Warehouses

Method Path Scope
GET /warehouses warehouses:read
{ "id": 8549, "name": "Main", "type": "main", "position": 0,
  "created_at": "2024-11-24T15:59:10" }

type is one of main, dropshipping, negative_stock.


Suppliers

Method Path Scope
GET /suppliers suppliers:read
POST /suppliers suppliers:write

POST body: { "name", "phone"?, "address"?, "comment"? } (creating a supplier with an existing name returns the existing one). unpaid is the outstanding debt to the supplier.

{ "id": 30, "name": "Acme Ltd", "phone": "380...", "address": "…",
  "comment": null, "unpaid": "69665.00", "created_at": "2023-09-11T10:45:23" }

Products

Method Path Scope Notes
GET /products products:read list
GET /products/{id} products:read single (always embeds images)
GET /products/{id}/similar products:read ids of similar products
POST /products products:write bulk create, up to 500
PUT /products products:write bulk partial update, up to 500: { "products": [{ "id": 1, ... }] }
PUT /products/{id} products:write partial update of one product
DELETE /products products:write body { "ids": [...] }
DELETE /products/{id} products:write single
POST /product_stock products:write quick stock/price/cost updates

GET filters: product_id, pids (csv), sku (csv), barcode (csv), category_id, warehouse_id, modify_start/modify_end (unix). ?count=1 returns just the total. ?include=images embeds image URLs. An image URL ends with ?c=<version>, which changes when the photo is replaced — keep it, so caches pick up the new image.

Search by SKU or barcode: ?sku=TS-001 and ?barcode=4820000000017 find products by your own codes — an exact match, case-insensitive. Up to 500 comma-separated values at once (more is a 422), so a catalog can be matched in batches. Combined with each other or with pids/product_id, they narrow the result; nothing found — an empty list. In /query: "filter": { "sku": ["TS-001", "TS-002"] }.

In stock, per warehouse: in_stock is what is available for sale (reserved items excluded), quantity is what is physically there.

Promotions: stock[].promotion is the promotion HugeProfit fills in on its sale form for this product in this warehouse, or null: the highest-priority one among promotions on the main price, or, if there are none, among promotions on additional prices. A promotion on an additional price is computed from that price's value in the product; if the product has no such price, there is no promotion either. A promotion appears here only if it lowers the price; promotion changes show up with a delay of up to a minute. To count a sale towards the promotion, send its id in the sale — see Sale with a promotion.

Field Description
id, name The promotion
discount_type, discount_value percent — percentage discount, fixed — fixed amount off, new_price — new price
price_type Which price the promotion is based on: null — the main price, otherwise the additional price name ("Wholesale")
price_before Price without the promotion: the main price or that additional price of the product
price Promotion price: show it to the buyer and send it in the sale
ends_at Promotion end (UTC); null — no end date

sale_price is the discounted price: a separately set sale price, otherwise the price of a promotion on the main price; null — no discount.

type is simple, variation (one row of a variable product — group them by parent_id), bundle or service. A bundle also has bundle_items — its composition per warehouse: for the bundle on bundle_warehouse_id, quantity of product_id is taken from warehouse_id.

GET /products/{id}/similar returns { "product_ids": [...] } — the other products of the same "similar products" group set up in HugeProfit; an empty list if the product is in no group.

Product object
{
  "id": 111237, "name": "T-shirt", "sku": "TS-001", "barcode": "7836...",
  "unit": "units", "type": "simple", "parent_id": null,
  "brand": null, "category_id": 18726,
  "category_path": [ { "id": 18726, "name": "Apparel" } ],
  "attributes": [], "dimensions": null, "min_stock": 0, "uktzed": null,
  "extra": {},
  "stock": [
    { "warehouse_id": 8645, "warehouse_name": "Main", "in_stock": 4,
      "quantity": 4, "cost_price": "5.00", "price": "6.00", "sale_price": "5.40",
      "promotion": { "id": 12, "name": "Autumn sale", "discount_type": "percent",
                     "discount_value": 10, "price_type": null, "price_before": "6.00", "price": "5.40",
                     "ends_at": "2026-10-31T21:59:59" },
      "sku": "TS-001" }
  ]
}

Creating products

POST /products with { "products": [...] } only creates. Send name (at least 2 characters) and any of sku, barcode, description, category (a name or a path ["Apparel", "T-shirts"]), brand, unit, images (URLs), extra (see below) and stock — [{ "warehouse_id", "quantity", "price", "cost_price" }]. Without stock, or without warehouse_id in a row, the product is created in the first warehouse of GET /warehouses.

  • id in the body — 422: an existing product is changed with PUT.
  • A product with the same sku or barcode already exists (without sku — also one with the same name) — 409 with details.product_id. The same product twice in one request — 422.
  • An error names the product by its position: details.index. Nothing is created then.

The answer is 201 with the created products. If some of them could not be saved, meta.failed is their count.

Updating products

PUT /products/{id} (or PUT /products with { "products": [{ "id": 1, ... }] }) changes only the fields you send; everything else stays as it is. To change only the description:

{ "description": "New description" }
Field Description
name At least 2 characters
description, unit As in the product object
category A name or a path ["Apparel", "T-shirts"]; missing categories are created
brand Brand name; null or "" removes the brand
barcode Up to 30 characters; if another product has it — 409 with details.product_id
extra Additional fields, merged into the product's fields, see below
stock [{ "warehouse_id", "quantity", "price", "cost_price", "comment" }] — a value you omit is not changed

extra — { "Material": "cotton" }: a key matches an existing additional field regardless of case, a new key adds a field, null or "" removes it; fields you don't send stay. Additional prices go in extra.custom_price — { "custom_price": { "Wholesale": 120 } }, numbers only.

stock — { "warehouse_id": 8645, "price": 199 } changes only the price there: quantity and cost price stay. comment is a note for the stock history. A warehouse where the product isn't yet is added. cost_price must be above 0 and can't be set while HugeProfit calculates it from shipments or cost components (422).

For a variation, name, description, category and brand change for the whole group; an extra key equal to the name of a variation attribute is a 422. sku, attributes and images are not changed by PUT: these and any other fields are a 422.

The whole request is checked before saving: on any error nothing is saved, and details.id names the product. An unknown or deleted id is a 404 with details.ids. The answer is the updated products.

Quick stock updates

POST /product_stock takes an array of { product_id, warehouse_id, quantity?, price?, cost_price?, comment? } — the same values as stock in PUT, and an omitted value is not changed either: [{ "product_id": 111237, "warehouse_id": 8645, "price": 199 }] changes only the price. Each item is applied on its own; the answer is { "updated": [ids], "errors": [{ "product_id", "error" }] }.


Product ↔ marketplace sync

Binds a local product_id to a marketplace remote_id under an integration, so stock/price sync knows which remote listing a product maps to.

Method Path Scope
GET /product_sync product_sync:read
POST /product_sync product_sync:write
DELETE /product_sync product_sync:write

GET filters: integration_id, product_id (each applied only when given). POST/DELETE body: { "bulk": [ ... ] } — POST items need product_id+remote_id+integration_id; DELETE items need integration_id+remote_id. POST returns per-item results plus stats.

{ "product_id": 111230, "integration_id": 359, "remote_id": "SKU-1",
  "created_at": "2026-07-08T08:20:10" }

Categories

Method Path Scope
GET /categories categories:read
GET /categories/{id} categories:read
POST /categories categories:write
DELETE /categories categories:write
DELETE /categories/{id} categories:write

?category_id= filters to one; ?tree=1 nests children. POST body: { "categories": [ { "name", "parent_id"?, "id"? } ] } (an id updates instead of creating). A name is unique among one parent's children, case-insensitive ("Hoodies" may exist in both "Men" and "Women"): creating a taken name returns the existing id; updating into a taken name or under its own subcategory fails with 400.

{ "id": 17497, "name": "Toys", "parent_id": null, "created_at": "2024-09-02T06:42:20" }

Clients

Method Path Scope
GET /clients clients:read
GET /clients/{id} clients:read
POST /clients clients:write
DELETE /clients clients:write
DELETE /clients/{id} clients:write
GET/POST/PUT/DELETE /clients/tags clients:read/clients:write

GET filters: id, ids (csv), phone, phone_contains, updated_at (unix — returns clients changed after it), with_debt=1 (embeds outstanding debt), with_chats=telegram (embeds messenger chats). POST body: { "clients": [ ... ] } (a phone is required to create; an id updates). Tags come back as a nested tags array.

Search by phone: phone finds clients by the whole number in any notation — 380671234567, +380671234567, 0671234567 and +38 (067) 123-45-67 all find the same client. phone_contains finds clients whose number contains the given digits (the last digits or any other part, e.g. ?phone_contains=4567); it needs at least 3 digits, fewer is a 422. Both can be combined. Several clients may share one number, so the result is always a list. In /query pass them as "filter": { "phone": "0671234567" } or "filter": { "phone_contains": "4567" }.

Messenger chats (GET /clients and GET /clients/{id}): with_chats takes a comma-separated list of messenger types — only telegram is supported for now, anything else is a 422. Each client then gets a chats array (empty if none), newest first: type, integration_id (the connected bot the chat belongs to) and chat_id (a string; for Telegram — the Bot API chat_id). A bot can only message a client who has written to that very bot, so pair chat_id with its integration_id. In /query pass it as "filter": { "with_chats": "telegram" } (or ["telegram"]).

Client object
{
  "id": 105424, "is_active": true, "type": "person",
  "first_name": "Svitlana", "last_name": "P.", "phone": "380...",
  "email": null, "company": null, "company_code": null,
  "discount": 0, "balance": "0.00", "total_amount": "0.00",
  "birth_day": "1995-12-16T00:00:00", "city": null, "region": null,
  "extra": {}, "tags": [ { "id": 5, "name": "VIP", "color": "#ff0000" } ],
  "chats": [ { "type": "telegram", "integration_id": 512, "chat_id": "123456789" } ],
  "created_at": "2023-06-17T05:59:19", "updated_at": "2026-04-23T10:21:58"
}

Tags (/clients/tags): GET lists tags (or, with ?client_id=, a client's tags); POST { name, color } creates; PUT binds { client_id, tag_id } or edits { tag_id, name?, color? }; DELETE unbinds { client_id, tag_id } or deletes { tag_id }.


Sales

A sale is one object with a nested items array. Writes go through the real sale engine (stock, money, fiscal receipts) — the fiscal receipt + new balance come back under meta.

Method Path Scope
GET /sales sales:read
GET /sales/{id} sales:read
POST /sales sales:write
PUT /sales sales:write
DELETE /sales or /sales/{id} sales:write
POST /sales/{id}/waybill delivery:write + sales:write
GET /sales/{id}/waybill/label delivery:read + sales:read

GET filters: date_from/date_to (unix; default = last 90 days), client_id, warehouse_id, status, refund=1 (return receipts only), external_id (your order number, see below), updated_from/updated_to (unix, by the last change), after_id (with updated_from, see below). ?include=client embeds the client.

Polling for changes: ?updated_from=<unix> returns the sales changed since then — new ones, edited ones and ones whose status changed (delivery tracking included), oldest change first. Remember the updated_at (as unix) and id of the last sale you got and pass them next time as updated_from and after_id: the list goes on right after that sale, even when many sales changed in the same second. Without after_id the boundary is inclusive. With updated_from or external_id and no date_from, the 90-day window does not apply. A deleted sale drops out of the list — use the order_delete webhook for those.

Sale object
{
  "id": 4605, "external_id": "site-1001", "order_number": "20072602", "status": "saled",
  "sold_at": "2026-07-20T08:28:59", "amount": "100.00", "sale_amount": "100.00",
  "prepaid": "20.00", "discount": "0.00", "currency": "2", "fee": "5.00",
  "account_id": 2, "client_id": null, "channel_ids": [867],
  "expense_ids": [],
  "items": [
    { "id": 5436, "product_id": 107720, "warehouse_id": 8562,
      "sku": null, "barcode": "7739...", "product_name": "Item",
      "quantity": 1, "price": "100.00", "cost_price": "1.00",
      "amount": "100.00", "profit": "94.00", "is_refund": false }
  ]
}

Create/update body carries the sale header plus an items array. Required: account_id (the account the money goes to) and, for each item, product_id, warehouse_id, quantity (> 0), price. The response meta may carry the fiscal receipt and the new account balance.

{ "external_id": "site-1001", "account_id": 2, "channel_ids": [867], "discount": 10,
  "items": [{ "product_id": 107720, "warehouse_id": 8562, "quantity": 2, "price": "100.00" }] }

PUT takes the same body plus the sale id and replaces the sale as a whole, like saving the sale form: an omitted field is reset (no status — the sale becomes saled). Only external_id, order_number and the expenses already on the sale stay.

All request fields
Field Type Required Description
id int PUT only The sale being edited (oid is accepted too)
items array yes Sale items, see items[] below
account_id int yes Account that receives the money (accounts in Reference); the sale currency is the currency of this account. It gets sale_amount minus the prepayment and the account commission, whatever the status
external_id string no Your order number, up to 100 characters; POST only — see Repeating a request safely
status string no Sale status, up to 20 characters, see the statuses below. Default — saled
custom_status string no Only with status: "delivery": the sale gets this status instead of delivery
client_id int no Client (Clients). Omit — a sale without a client
channel_ids int[] no Sales channels (sales_channels in Reference); a single channel_id: 867 is accepted too. Omit — the technical channel 1
amount number no Sum of quantity × price of the items; counted for you, see below
discount number no Discount on the sale. Default 0
discount_type string no % (default) — percent, stable — a fixed sum in the sale currency
sale_amount number no What the client pays. Default — amount minus the discount
prepaid number no Prepayment. Default 0
prepaid_account_id int with prepaid Account that receives the prepayment. Without it the prepayment is only recorded, and the whole sale_amount goes to account_id
fee number no Marketplace / channel commission for this sale, in the sale currency; reduces the profit. It is not calculated from the channel settings — send the amount yourself
cash_from_client number no Cash received from the buyer; the change on the fiscal receipt is calculated from it
comment string no Comment, up to 300 characters
extra object no Additional sale fields: { "Source": "Instagram" }. Keys are the names of the sale additional fields from your settings
delivery object no The client's Nova Poshta address — a branch or a courier address, see the delivery fields below. Needs client_id
client_delivery_id int no An already saved address of the client instead of delivery; an unknown id is ignored
delivery_info string no Delivery service, up to 200 characters: 1 — Nova Poshta, 3 — Ukrposhta, 6 — Rozetka Delivery, 24 — Meest, justin, delivery_auto, pickup, city-delivery, outcity-delivery, international; outside Ukraine — UPS, DHL, post office, other. A note can follow a colon: "1: Branch 5"
tracking_number string no A waybill created outside HugeProfit, up to 20 characters. It is linked to the carrier only if delivery_info holds just the code ("1"), without a note
expenses array no Expenses created together with the sale, see expenses[] below

items[]

Field Type Required Description
product_id int yes Product
warehouse_id int yes Warehouse the goods are written off from (warehouses in Reference). One product from two warehouses — two items
quantity number yes Quantity, greater than 0, fractional allowed (0.5)
price number yes, except an item with promotion_id Unit price the item is sold at
promotion_id int no Promotion the item is sold under, see Sale with a promotion
discount number no Item discount in percent, for display: the item is sold at price anyway. The sale % discount does not apply to such an item
default_price number no Unit price before the item discount, shown next to discount. Default — price
price_type string no Price label, e.g. an additional price name ("Wholesale"), up to 255 characters. With promotion_id — the promotion name by default
uktzed string no UKTZED code of the item for the fiscal receipt

expenses[] — each expense reduces the sale profit. On PUT send only new expenses.

Field Type Required Description
amount number yes Expense amount
category int no Expense category (expense_categories in Reference)
account_id int no Account the expense is paid from. Default — account_id of the sale
expense_name string no Expense name
mid int no Warehouse the expense belongs to

Statuses

status What happens
saled (default) Completed sale: stock is written off
reserve_sale Reserve: in_stock decreases, quantity stays. Stock is not checked
delivery Sale in delivery; with custom_status the sale gets that status
any other string Saved as is: delivering, cod, arrived or your own status

Amounts are counted the same way as in the HugeProfit sale form:

Field Description
amount Sale total — the sum of quantity × price of the items. Omit it and it is counted for you; if you pass it, it must equal that sum to the cent, otherwise 422 with the right sum in details.expected
discount, discount_type Discount on the sale: % (default) or stable — a fixed sum. Items with their own discount are not part of the % base
sale_amount What the client pays. Default — amount minus the discount

Money and quantities may be numbers or strings ("100.50", "100,50"); anything else, a negative number, nan or inf — 422.

If an item is short on a warehouse that doesn't allow negative stock, the sale is not created and the answer is 422 with what is missing, as for a production run:

{ "success": false, "error": { "code": "validation_error", "message": "not enough stock",
  "details": { "shortages": [ { "product_id": 107720, "warehouse_id": 8562,
                                "required": 2, "in_stock": 1 } ] } } }

An edit gives the sale a new id

PUT /sales (and editing the sale in HugeProfit) re-creates it under a new id; the response carries the new one, and GET /sales/{old id} is a 404 from then on. Keep external_id to find the sale, or take the new id from the order_update webhook — it has previous_id.

items[].warehouse_id is the warehouse the goods are written off from. Delivery fields (all optional):

Field Description
status Sale status: saled (default), reserve_sale, delivery, delivering, cod, arrived or your own status
delivery The client's Nova Poshta address — a branch { "carrier": "nova_poshta", "warehouse_ref": "…" } (ref from /delivery/warehouses) or a courier address { "carrier": "nova_poshta", "settlement_ref": "…", "street": "Київська", "house": "14", "flat": "285" } (flat is optional; settlement_ref and street from /delivery/settlements and /delivery/streets). Needs client_id; the address is saved to the client (an existing one is reused) and the sale gets delivery_info: "1"
client_delivery_id An already saved address of the client instead of delivery
delivery_info Delivery service code: 1 — Nova Poshta, 3 — Ukrposhta, 24 — Meest, …
tracking_number A waybill created outside HugeProfit. While the sale is in a delivery status (delivery, delivering, arrived), the parcel status is tracked automatically and moves the sale on

Sale with a promotion

To count a sale towards a promotion — its limits, budget and statistics in HugeProfit — send promotion_id in the item: the product's stock[].promotion.id or another promotion applied to this product in this warehouse (as in the sale form's price list). HugeProfit computes the price — from the main price or, for a promotion on an additional price, from that price:

  • price may be omitted — we fill in promotion.price;
  • a price you send must equal the promotion price to the cent, otherwise 422 promotion_price_mismatch with the correct price in details.expected;
  • compute amount (if you send it) from the promotion price.
{ "account_id": 2, "items": [{ "product_id": 107720, "warehouse_id": 8562, "quantity": 2, "promotion_id": 12 }] }

Without promotion_id the item is sold at price and the promotion does not count it, even if the price matches the promotion price. A refusal is 422 with one of the codes below; when the error belongs to an item, details holds its product_id, warehouse_id and promotion_id.

code Cause What to do
promotion_inactive The promotion was turned off, has ended or has not started yet Re-read the product and show the buyer the current price
promotion_not_applicable The promotion does not apply to this product in this warehouse, the product has no such additional price, or it does not lower the price Same
promotion_price_mismatch price does not equal the promotion price Take the price from details.expected
promotion_limit_usage The promotion usage limit is reached Sell without the promotion
promotion_limit_client Client client_id has used up their promotion limit Sell without the promotion

PUT /sales checks the promotion the same way: if it has already ended, send the item without promotion_id — the price is kept, but the sale leaves the promotion's accounting.

Repeating a request safely (external_id)

If a POST /sales response is lost (a timeout, a dropped connection), you can't tell whether the sale was created, and a blind repeat would create a second sale and write the stock off twice. Pass your own order number as external_id (a string up to 100 characters):

  • the first request creates the sale, and external_id comes back in it;
  • a repeat with the same external_id creates nothing and answers 409 with the sale that already exists — take it with GET /sales/{details.sale_id} and go on:
{ "success": false, "error": { "code": "conflict", "message": "a sale with this external_id already exists",
  "details": { "sale_id": 4605, "external_id": "site-1001" } } }

external_id is unique per API key: two keys (e.g. two sites) may each send their own 1001. It stays with the sale when the sale is edited, comes in the sale object and in the order_* webhooks, and GET /sales?external_id=site-1001 finds the sale by it. Once the sale is deleted, the number is free again. It is set on creation only — PUT does not change it.

Nova Poshta waybill

POST /sales/{id}/waybill creates a Nova Poshta waybill for the sale through the connected Nova Poshta account — the same as Create waybill in the sale card. The waybill number is saved to the sale, the sale moves to delivering, and HugeProfit then tracks the parcel status. The sale needs a client_id (with first name, last name and phone) and a Nova Poshta address (delivery when creating the sale): to a branch, or to a courier address — then the parcel goes from the sender's branch to the recipient's door.

Field Required Description
carrier_account_id yes Nova Poshta account: id from carrier_accounts in Reference
sender_id if the account has several senders senders[].id of that account
cod_amount no Cash on delivery the recipient pays. Omit — no cash on delivery
cod_type no cash_on_delivery (default) or payment_control (Nova Poshta "Payment control", needs a contract with Nova Poshta)
declared_value no Declared value. Default — sale_amount
payer no Who pays for the delivery: recipient (default) or sender
payment_method no cash (default) or non_cash
weight, length, width, height no Parcel weight in kg and size in cm. Default — 1 kg, 10×10×10
seats no Several parcels: [{ "weight", "length", "width", "height" }, …] instead of the fields above
description no Parcel description, up to 120 characters. Default — names of the sale items
additional_info no Additional information, up to 100 characters
{ "carrier_account_id": 431, "cod_amount": "185.00", "weight": 0.5 }

Response 201:

{ "tracking_number": "20450000000001", "sale_id": 4605, "carrier": "nova_poshta",
  "carrier_account_id": 431, "sender_id": "3f6c1a20-…_3f71b5c8-…",
  "cost": "70", "estimated_delivery_date": "2026-10-18" }

cost is the delivery price calculated by Nova Poshta. Errors:

  • 404 — no such sale, or it is a return receipt.
  • 409 — the sale already has a waybill, is refunded, or a waybill for it is being created right now: repeating a request does not create a second waybill.
  • 422 validation_error — no client or Nova Poshta address on the sale, the account or sender is not found or not set up, a field is invalid.
  • 422 carrier_error — Nova Poshta rejected the waybill; message carries its text.
  • 500 with details.tracking_number — the waybill is created at Nova Poshta but not saved to the sale. Do not repeat the request (it would create another waybill) — contact support.

Waybill label

GET /sales/{id}/waybill/label returns the waybill label (100×100 marking) of the sale as a PDF (Content-Type: application/pdf) — the same as Print in HugeProfit. It works for any waybill of the sale made through a connected delivery account, not only for ones created through the API. Errors come in the usual JSON envelope:

  • 404 — no such sale, or it has no waybill;
  • 422 carrier_error — the delivery service returned no label: the waybill is not from a connected account.
curl https://crm.h-profit.com/bapi-v2/sales/4605/waybill/label \
  -H "Authorization: hp_live_your_key_here" -o label.pdf

Orders (marketplace)

Incoming orders from external channels (remote_orders) before they become a sale. sale_id links to the sale once created (is_linked).

Method Path Scope
GET /orders orders:read
GET /orders/{id} orders:read
POST /orders orders:write
PUT /orders/{id} orders:write

GET filters: order_id, integration_id, status, local_status, start/end (unix), linked (unlinked [default] | linked | all). ?include=client,sale embeds the client and, if linked, the sale. POST accepts an external order and reserves stock.

GET /orders/{id} also returns tracking_number and sale_status — the waybill number and status of the sale the order became (null until it is linked).

PUT /orders/{id} changes an order created by the same key (any other order — 404):

  • { "status": "cancelled" } cancels it and releases the reserved stock. Repeating it is safe — the stock is not released twice. A sale already created from the order is not affected.
  • { "order_text": "1024" } sets the order number shown in the HugeProfit order list (stored in info.order_text).

Both return the updated order object; any other status or an empty body — 422.


Delivery

City and branch lookups of Nova Poshta and Meest — e.g. to pick a city and a branch for a delivery address — and Nova Poshta settlement and street lookups for a courier address.

Method Path Scope
GET /delivery/cities delivery:read
GET /delivery/warehouses delivery:read
GET /delivery/settlements delivery:read
GET /delivery/streets delivery:read
  • GET /delivery/cities?carrier=nova_poshta&q=Київ — up to 20 cities { ref, name } whose name contains q; without q — the first 20 alphabetically. Settlements with the same name carry the region or district in the name: Олександрівка (Миколаївська обл.).
  • GET /delivery/warehouses?carrier=nova_poshta&city_ref=… — branches of the city { ref, number, name } sorted by number: number=5 finds branch №5 exactly (not №50), q filters by a part of the name, limit — up to 500 (default 50).
  • Meest — the same two calls with carrier=meest and account_id of the connected Meest account (from carrier_accounts in Reference); they are searched live through that account. GET /delivery/cities?carrier=meest&account_id=…&q=Київ returns { ref, name }, GET /delivery/warehouses?carrier=meest&account_id=…&city_ref=… — { ref, number: null, name }. An account_id that is not your Meest account gives an empty list.

An unknown carrier, a missing city_ref, or no account_id for Meest — 422.

{ "ref": "8d5a980d-391c-11dd-90d9-001a92567626", "name": "Київ" }
{ "ref": "1ec09d88-e1c2-11e3-8c4a-0050568002cf", "number": 1,
  "name": "Відділення №1: вул. Пирогівський шлях, 135" }

Courier address (Nova Poshta only, searched live at Nova Poshta, limit up to 500, default 20):

  • GET /delivery/settlements?carrier=nova_poshta&q=Кременчук — settlements { ref, name, city_ref, address_delivery }, villages without a branch included. ref goes to delivery.settlement_ref of the sale; address_delivery: false — Nova Poshta does not deliver to addresses there; city_ref works for /delivery/warehouses.
  • GET /delivery/streets?carrier=nova_poshta&settlement_ref=…&q=Київ — streets { name, street }: pass street to delivery.street, name (with the street type) is for display.

A missing q or settlement_ref, or an unknown settlement_ref — 422; Nova Poshta did not answer — 502 carrier_error, retry later.

{ "ref": "e71a26ea-4b33-11e4-ab6d-005056801329", "name": "м. Кременчук, Кременчуцький р-н, Полтавська обл.",
  "city_ref": "8d5a9813-391c-11dd-90d9-001a92567626", "address_delivery": true }
{ "name": "вул. Київська", "street": "Київська" }

Media (image hosting)

Upload images to HugeProfit and get public links to use anywhere.

Method Path Scope
GET /media media:read
POST /media media:write

POST /media takes multipart/form-data with up to 5 files; only PNG, JPEG, WebP and GIF (the type is checked by content, not by extension; SVG is not accepted). Returns 201 with an array of the uploaded files. Another file type, more than 5 files or no file — 422. Files count towards the account's storage limit.

GET /media lists the account's files (including ones uploaded in HugeProfit), newest first; limit/offset, the total is in meta.total.

{ "id": 5121, "url": "https://…/bfiles/public/…", "name": "banner", "ext": "png",
  "size": 48213, "created_at": "2026-10-06 12:00:00" }
Order object
{
  "id": 3510949, "order_id": "9231", "integration_id": 621,
  "status": "completed", "name": "Order ID: 9231", "price": "3101.00",
  "currency": "UAH", "created_at": "2026-07-07T10:19:55",
  "first_name": "Lina", "phone": "+380...", "email": "...",
  "address": { "city": "…", "address_1": "…" },
  "items": [ { "id": 7759, "name": "…", "quantity": 1, "price": 518 } ],
  "client_id": 36247, "sale_id": null, "is_linked": false
}

Shipments

Read-only. A supply/receiving is one object with a nested items array.

Method Path Scope
GET /shipments shipments:read
GET /shipments/{id} shipments:read

GET filters: date_from/date_to (unix, by delivered date), status, warehouse_id. ?include=supplier,expenses embeds the supplier and linked expense rows.

payments lists every payment for the goods, oldest first — including deferred and partial ones (the delivery payment is not included, see delivery_expense_id). expense_id points to Expenses; all of them are also available via GET /expenses?shipment_id=….

Shipment object
{
  "id": 11191, "name": "Supply #…", "status": "complete",
  "warehouse_id": 8549, "warehouse_name": "Main", "supplier_id": null,
  "amount": "100.00", "quantity": 10, "currency": "2",
  "delivered_date": "2026-05-21T07:42:00", "expense_id": 1115,
  "payments": [
    { "expense_id": 1115, "date": "2026-06-16T09:10:00", "amount": "100.00",
      "account_id": 14459, "category_id": 11, "name": "Supply payment" }
  ],
  "items": [
    { "id": 5020, "product_id": 104641, "product_name": "…", "sku": "c2b-6i3",
      "quantity_shipped": 10, "quantity_received": 10, "amount": "100.00",
      "cost_price": "10.00" }
  ]
}

Expenses

Method Path Scope
GET /expenses expenses:read
GET /expenses/{id} expenses:read
POST /expenses expenses:write

GET filters: date_from/date_to (unix; default = last 31 days), category_id, shipment_id (payments of a shipment; without dates — all of them, regardless of date). ?include=warehouse embeds the warehouse. POST body: { account_id, category_id, amount, name, comment?, warehouse_id?, product_id?, shipment_id?, order_id?, type?, date? }; the new account balance returns in meta.

{ "id": 1115, "account_id": 14459, "category_id": 2, "category_name": "Delivery",
  "warehouse_id": 8549, "name": "…", "amount": "30.00", "balance": "116721.08",
  "type": "onetime", "created_at": "2026-06-29T08:32:56" }

Productions

A production is a recipe: components (consumed products) + expenses → results (produced products).

Method Path Scope
GET /productions productions:read
GET /productions/{id} productions:read
POST / PUT /productions productions:write
DELETE /productions or /productions/{id} productions:write
POST /productions/{id}/run productions:write
GET /productions/history productions:read
GET/POST/DELETE /productions/categories productions:read/productions:write

GET filters: start/end (unix), category_id, is_deleted. ?include=category embeds the category.

Production object
{
  "id": 49, "name": "Recipe 787", "status": "created", "category_id": null,
  "amount": "0.00", "quantity": 50, "result_product_id": 107511,
  "result_warehouse_id": 8614, "use_real_cost": true,
  "components": [ { "id": 115, "product_id": 107513, "warehouse_id": 8614,
                    "name": "Raw", "quantity": 1, "cost": "0.00" } ],
  "results": [], "expenses": []
}

Running a recipe

POST /productions/{id}/run with { "quantity": 2.5, "account_id": 14459 } runs the recipe quantity times, exactly as the Produce button in HugeProfit does: writes the components off their warehouses, adds the results to stock and records the recipe's expenses.

  • quantity — how many times to run the recipe (fractions allowed, > 0).
  • account_id — the account to pay the recipe's expenses from (from accounts in Reference); required only when the recipe has expenses.

Returns 201 with the run (the same object as in /productions/history):

{ "id": 812, "production_id": 49, "product_id": 107511, "warehouse_id": 8614, "category_id": null,
  "quantity": 2.5, "net_price": "20.00", "products_prices": "50.00", "expenses_amount": "0.00",
  "total": "50.00", "type": "production", "created_at": "2026-10-06T12:00:00" }

If a component is short on a warehouse that doesn't allow negative stock, nothing is run and the answer is 422 with what is missing:

{ "success": false, "error": { "code": "validation_error", "message": "not enough stock",
  "details": { "shortages": [ { "product_id": 107513, "warehouse_id": 8614,
                                "required": 120, "in_stock": 100 } ] } } }

Other 422: quantity missing or not positive, no account_id for a recipe with expenses, a recipe without results. An unknown or deleted recipe — 404.


Write-offs

Write goods off a warehouse (damage, losses, own use) — like the Write-off document in HugeProfit, created and completed in one call.

Method Path Scope
POST /write_offs write_offs:write
{ "warehouse_id": 8614, "category_id": 2, "comment": "Damaged in transit",
  "items": [ { "product_id": 107513, "quantity": 3 } ] }

category_id is a write-off category (write_off_categories in Reference); each product appears once. The amount is counted at the warehouse cost price. Returns 201:

{ "id": 77, "warehouse_id": 8614, "category_id": 2, "status": "completed", "amount": "30.00",
  "quantity": 3, "comment": "Damaged in transit", "created_at": "2026-10-06T12:00:00",
  "items": [ { "product_id": 107513, "quantity": 3, "quantity_was": 100, "quantity_became": 97 } ] }

Not enough stock (on a warehouse without negative stock) — 422 with details.shortages, as for a production run, and nothing is written off. Empty items, a repeated product or a quantity that isn't positive — 422; an unknown category — 404.