{"routes":[{"method":"GET","path":"/v1/me","scope":"read","status":200,"summary":"The merchant this key belongs to. `mode` is the key's (live or test), not the merchant's.","params":[],"responseExample":{"id":"mer_7Hq2kLx9Pz3RtV8wYb1N","name":"Acme Labs","slug":"acme-labs","status":"active","platform":"stonegate","packaging_tier":"labrat","per_order_fee_cents":1050,"mode":"live"},"errors":[]},{"method":"GET","path":"/v1/skus","scope":"read","status":200,"summary":"Every SKU you have registered, with counts. Lots are on GET /v1/inventory.","params":[{"name":"active","in":"query","type":"1 | 0","required":false,"description":"active=1 lists only active SKUs, active=0 only inactive ones. Default: all."}],"responseExample":{"data":[{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":20,"active":true,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0}}],"next_cursor":null},"errors":["validation_error"]},{"method":"POST","path":"/v1/skus","scope":"inventory:write","status":201,"summary":"Create a SKU, or update it when the sku string already exists (upsert by sku). 201 on create, 200 on update.","params":[{"name":"sku","in":"body","type":"string","required":true,"description":"Your own sku string, up to 64 characters. Case-insensitive: \"bpc-5\" and \"BPC-5\" are the same SKU."},{"name":"name","in":"body","type":"string","required":true,"description":"Required on create. Up to 160 characters."},{"name":"catalog_code","in":"body","type":"string | null","required":false,"description":"A Veriti catalog code from GET /v1/catalog, or null to unmap."},{"name":"size_label","in":"body","type":"string","required":false,"description":"What the label says, e.g. \"5mg\"."},{"name":"unit","in":"body","type":"vial | kit | other","required":false,"description":"Default vial."},{"name":"barcode","in":"body","type":"string","required":false,"description":"Optional barcode the warehouse can scan."},{"name":"weight_oz","in":"body","type":"number | null","required":false,"description":"Unit weight in ounces, used for postage estimates."},{"name":"low_stock_threshold","in":"body","type":"integer | null","required":false,"description":"inventory.low fires when available drops to or below this. null = the warehouse default."},{"name":"active","in":"body","type":"boolean","required":false,"description":"false stops new orders for this SKU."}],"requestExample":{"sku":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","weight_oz":0.5,"low_stock_threshold":20},"responseExample":{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":20,"active":true,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0}},"errors":["validation_error"]},{"method":"GET","path":"/v1/skus/:sku","scope":"read","status":200,"summary":"One SKU by its sku string.","params":[{"name":"sku","in":"path","type":"string","required":true,"description":"Your sku string (URL-encode it)."}],"responseExample":{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":20,"active":true,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0}},"errors":["not_found"]},{"method":"PATCH","path":"/v1/skus/:sku","scope":"inventory:write","status":200,"summary":"Change any field of a SKU except the sku string itself. Only the fields you send change.","params":[{"name":"sku","in":"path","type":"string","required":true,"description":"Your sku string."},{"name":"name","in":"body","type":"string","required":false,"description":""},{"name":"catalog_code","in":"body","type":"string | null","required":false,"description":""},{"name":"size_label","in":"body","type":"string","required":false,"description":""},{"name":"unit","in":"body","type":"vial | kit | other","required":false,"description":""},{"name":"barcode","in":"body","type":"string","required":false,"description":""},{"name":"weight_oz","in":"body","type":"number | null","required":false,"description":""},{"name":"low_stock_threshold","in":"body","type":"integer | null","required":false,"description":""},{"name":"active","in":"body","type":"boolean","required":false,"description":"true reactivates a SKU that DELETE deactivated."}],"requestExample":{"low_stock_threshold":30},"responseExample":{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":30,"active":true,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0}},"errors":["not_found","validation_error"]},{"method":"DELETE","path":"/v1/skus/:sku","scope":"inventory:write","status":200,"summary":"Deactivate a SKU. Nothing is deleted: stock, orders and the ledger keep pointing at it, and PATCH {active:true} brings it back.","params":[{"name":"sku","in":"path","type":"string","required":true,"description":"Your sku string."}],"responseExample":{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":20,"active":false,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0}},"errors":["not_found"]},{"method":"GET","path":"/v1/inventory","scope":"read","status":200,"summary":"Every SKU with its counts and its lots (lot number, expiry, COA link). available = on_hand - reserved.","params":[{"name":"active","in":"query","type":"1 | 0","required":false,"description":"Same filter as GET /v1/skus. Default: all."},{"name":"limit","in":"query","type":"integer","required":false,"description":"Page size, 1 to 200. Default 50."},{"name":"cursor","in":"query","type":"string","required":false,"description":"The next_cursor from the previous page. Opaque; do not build one by hand."}],"responseExample":{"data":[{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":20,"active":true,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0},"lots":[{"lot_number":"L2409A","expires_at":"2027-06-30","on_hand":120,"reserved":6,"available":114,"coa_url":"https://fulfill.example.com/files/m1-coa-L2409A.pdf","expired":false}]}],"next_cursor":null},"errors":["validation_error"]},{"method":"GET","path":"/v1/inventory/movements","scope":"read","status":200,"summary":"The stock ledger, newest first. Every change to on-hand stock is one row with the balance after it; reservations are not rows.","params":[{"name":"sku","in":"query","type":"string","required":false,"description":"Only this SKU."},{"name":"since","in":"query","type":"ISO-8601","required":false,"description":"Only rows created at or after this instant."},{"name":"limit","in":"query","type":"integer","required":false,"description":"Page size, 1 to 200. Default 50."},{"name":"cursor","in":"query","type":"string","required":false,"description":"The next_cursor from the previous page. Opaque; do not build one by hand."}],"responseExample":{"data":[{"id":418,"sku":"BPC-5","lot_number":"L2409A","delta":-2,"reason":"ship","ref_type":"order","ref_id":"ord_3kQ9zT7yW2mL5nR8pV1c","balance_after":118,"note":"","by":"console","created_at":"2026-09-04T16:20:11.000Z"}],"next_cursor":"418"},"errors":["validation_error"]},{"method":"GET","path":"/v1/inventory/:sku","scope":"read","status":200,"summary":"One SKU with counts and lots.","params":[{"name":"sku","in":"path","type":"string","required":true,"description":"Your sku string."}],"responseExample":{"id":"BPC-5","name":"BPC-157 5mg","catalog_code":"VC-BPC-5","size_label":"5mg","unit":"vial","barcode":"","weight_oz":0.5,"low_stock_threshold":20,"active":true,"inventory":{"on_hand":120,"reserved":6,"available":114,"low_stock":false,"expired":0},"lots":[{"lot_number":"L2409A","expires_at":"2027-06-30","on_hand":120,"reserved":6,"available":114,"coa_url":"https://fulfill.example.com/files/m1-coa-L2409A.pdf","expired":false}]},"errors":["not_found"]},{"method":"POST","path":"/v1/inbound","scope":"inventory:write","status":201,"summary":"Announce a shipment to the warehouse (an ASN). The dock matches the box to `reference`, receives it line by line, and you get inbound.received or inbound.discrepancy.","params":[{"name":"reference","in":"body","type":"string","required":true,"description":"Your reference for the box (a PO number). Write it on the box."},{"name":"carrier","in":"body","type":"string","required":false,"description":""},{"name":"tracking","in":"body","type":"string","required":false,"description":""},{"name":"expected_at","in":"body","type":"YYYY-MM-DD | ISO-8601","required":false,"description":"When it should arrive."},{"name":"items","in":"body","type":"array","required":true,"description":"[{sku, qty, lot_number?, expires_at?}]. Every sku must exist and be active."},{"name":"notes","in":"body","type":"string","required":false,"description":"Anything the dock should know."}],"requestExample":{"reference":"PO-2211","carrier":"UPS","tracking":"1Z999AA10123456784","expected_at":"2026-09-10","items":[{"sku":"BPC-5","qty":100,"lot_number":"L2409A","expires_at":"2027-06-30"}]},"responseExample":{"id":"inb_5cB8nM2xK7qT1wZ4jP9r","reference":"PO-2211","carrier":"UPS","tracking":"1Z999AA10123456784","status":"expected","expected_at":"2026-09-10T00:00:00.000Z","received_at":null,"items":[{"sku":"BPC-5","qty_expected":100,"qty_received":0,"lot_number":"L2409A","expires_at":"2027-06-30"}]},"errors":["validation_error"]},{"method":"GET","path":"/v1/inbound","scope":"read","status":200,"summary":"Your inbound shipments, newest first.","params":[{"name":"status","in":"query","type":"expected | received | discrepancy | cancelled","required":false,"description":""},{"name":"limit","in":"query","type":"integer","required":false,"description":"Page size, 1 to 200. Default 50."},{"name":"cursor","in":"query","type":"string","required":false,"description":"The next_cursor from the previous page. Opaque; do not build one by hand."}],"responseExample":{"data":[{"id":"inb_5cB8nM2xK7qT1wZ4jP9r","reference":"PO-2211","carrier":"UPS","tracking":"1Z999AA10123456784","status":"expected","expected_at":"2026-09-10T00:00:00.000Z","received_at":null,"items":[{"sku":"BPC-5","qty_expected":100,"qty_received":0,"lot_number":"L2409A","expires_at":"2027-06-30"}]}],"next_cursor":null},"errors":["validation_error"]},{"method":"GET","path":"/v1/inbound/:id","scope":"read","status":200,"summary":"One inbound shipment with what was expected and what was received per line.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The inb_ id."}],"responseExample":{"id":"inb_5cB8nM2xK7qT1wZ4jP9r","reference":"PO-2211","carrier":"UPS","tracking":"1Z999AA10123456784","status":"expected","expected_at":"2026-09-10T00:00:00.000Z","received_at":null,"items":[{"sku":"BPC-5","qty_expected":100,"qty_received":0,"lot_number":"L2409A","expires_at":"2027-06-30"}]},"errors":["not_found"]},{"method":"POST","path":"/v1/inbound/:id/cancel","scope":"inventory:write","status":200,"summary":"Cancel an announced shipment. Only while it is still expected and nothing on it has been received.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The inb_ id."}],"responseExample":{"id":"inb_5cB8nM2xK7qT1wZ4jP9r","reference":"PO-2211","carrier":"UPS","tracking":"1Z999AA10123456784","status":"cancelled","expected_at":"2026-09-10T00:00:00.000Z","received_at":null,"items":[{"sku":"BPC-5","qty_expected":100,"qty_received":0,"lot_number":"L2409A","expires_at":"2027-06-30"}]},"errors":["not_found","conflict"]},{"method":"POST","path":"/v1/orders","scope":"orders:write","status":201,"summary":"Create an order. Stock is reserved FEFO at once; a short line puts the order on_hold (backorder) until stock arrives. Send an Idempotency-Key so a retry can never create two.","params":[{"name":"external_id","in":"body","type":"string","required":true,"description":"Your order id. Unique per merchant: a second POST with the same value answers 409 with the existing order in `order`."},{"name":"customer","in":"body","type":"object","required":true,"description":"{name (required), email?, phone?}"},{"name":"address","in":"body","type":"object","required":true,"description":"{address1, address2?, city, state, zip, country}. country is a 2-letter code; a US state is required and normalized to its 2-letter code; a US zip is 5 digits or ZIP+4."},{"name":"items","in":"body","type":"array","required":true,"description":"[{sku, qty}]. Every sku must exist and be active; qty is a whole number of at least 1."},{"name":"shipping_service","in":"body","type":"string","required":false,"description":"The service your customer chose at checkout, as text. The warehouse picks the matching rate."},{"name":"notes","in":"body","type":"string","required":false,"description":"For the packer."},{"name":"gift_message","in":"body","type":"string","required":false,"description":"Printed and put in the box."},{"name":"hold","in":"body","type":"boolean","required":false,"description":"true creates the order on_hold; release it with PATCH {hold:false}."},{"name":"requested_ship_at","in":"body","type":"YYYY-MM-DD | ISO-8601","required":false,"description":"Do not ship before this."},{"name":"Idempotency-Key","in":"header","type":"string","required":false,"description":"Any string up to 200 characters. See Idempotency."}],"requestExample":{"external_id":"SHOP-1001","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","qty":2}],"shipping_service":"USPS Priority"},"responseExample":{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"received","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"","carrier":"","tracking_number":"","tracking_url":null,"label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":null,"delivered_at":null,"cancelled_at":null,"mode":"live"},"errors":["validation_error","conflict","idempotency_mismatch"]},{"method":"GET","path":"/v1/orders","scope":"read","status":200,"summary":"Your orders, newest first. A live key lists live orders; a test key lists test orders.","params":[{"name":"status","in":"query","type":"received | on_hold | picking | packed | shipped | delivered | cancelled | returned | exception","required":false,"description":""},{"name":"since","in":"query","type":"ISO-8601","required":false,"description":"Only orders created at or after this instant."},{"name":"external_id","in":"query","type":"string","required":false,"description":"Exactly this external id."},{"name":"limit","in":"query","type":"integer","required":false,"description":"Page size, 1 to 200. Default 50."},{"name":"cursor","in":"query","type":"string","required":false,"description":"The next_cursor from the previous page. Opaque; do not build one by hand."}],"responseExample":{"data":[{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"shipped","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"USPS|usps_priority","carrier":"usps","tracking_number":"9400111899223197428490","tracking_url":"https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490","label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":"2026-09-05T18:41:02.000Z","delivered_at":null,"cancelled_at":null,"mode":"live"},{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"received","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"","carrier":"","tracking_number":"","tracking_url":null,"label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":null,"delivered_at":null,"cancelled_at":null,"mode":"live"}],"next_cursor":"4171"},"errors":["validation_error"]},{"method":"GET","path":"/v1/orders/:id","scope":"read","status":200,"summary":"One order. `:id` is the ord_ id, or ext_<external_id> to look it up by your own id (URL-encode it: ext_%231001 for \"#1001\"). A live key sees live orders, a test key test orders.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"ord_... or ext_<external_id>"}],"responseExample":{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"shipped","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"USPS|usps_priority","carrier":"usps","tracking_number":"9400111899223197428490","tracking_url":"https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490","label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":"2026-09-05T18:41:02.000Z","delivered_at":null,"cancelled_at":null,"mode":"live"},"errors":["not_found"]},{"method":"PATCH","path":"/v1/orders/:id","scope":"orders:write","status":200,"summary":"Edit the customer, address, notes, gift message or shipping service while the order is received or on_hold; {hold:true|false} pauses or releases it.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"ord_... or ext_<external_id>"},{"name":"customer","in":"body","type":"object","required":false,"description":"Fields you leave out keep their value."},{"name":"address","in":"body","type":"object","required":false,"description":"Fields you leave out keep their value; the result is validated as a whole."},{"name":"notes","in":"body","type":"string","required":false,"description":""},{"name":"gift_message","in":"body","type":"string","required":false,"description":""},{"name":"shipping_service","in":"body","type":"string","required":false,"description":""},{"name":"hold","in":"body","type":"boolean","required":false,"description":"true = hold (keeps the reservation), false = release. A released order still short on stock stays on_hold as a backorder."}],"requestExample":{"address":{"address2":"Apt 12"},"hold":false},"responseExample":{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"received","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"","carrier":"","tracking_number":"","tracking_url":null,"label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":null,"delivered_at":null,"cancelled_at":null,"mode":"live"},"errors":["not_found","validation_error","conflict"]},{"method":"POST","path":"/v1/orders/:id/cancel","scope":"orders:write","status":200,"summary":"Cancel while received or on_hold. Releases the reservation. Once the warehouse is picking, ask them (409).","params":[{"name":"id","in":"path","type":"string","required":true,"description":"ord_... or ext_<external_id>"},{"name":"reason","in":"body","type":"string","required":false,"description":"Recorded on the order's timeline."}],"requestExample":{"reason":"Customer changed their mind"},"responseExample":{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"cancelled","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"","carrier":"","tracking_number":"","tracking_url":null,"label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":null,"delivered_at":null,"cancelled_at":"2026-09-04T17:10:00.000Z","mode":"live"},"errors":["not_found","conflict"]},{"method":"GET","path":"/v1/orders/:id/events","scope":"read","status":200,"summary":"The order's timeline, oldest first: created, hold, release, picking, packed, label.bought, shipped, and so on.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"ord_... or ext_<external_id>"}],"responseExample":{"data":[{"type":"created","detail":"2 unit(s) on 1 line(s), live mode, via api","created_at":"2026-09-04T16:02:33.000Z"},{"type":"picking","detail":"","created_at":"2026-09-05T15:02:00.000Z"}],"next_cursor":null},"errors":["not_found"]},{"method":"GET","path":"/v1/webhooks","scope":"read","status":200,"summary":"Your endpoints with their health. The secret is never listed.","params":[],"responseExample":{"data":[{"id":"whk_9pL2xN7vB4kM1qR6tW3s","url":"https://example.com/hooks/veriti","events":["order.shipped","order.delivered"],"active":true,"created_at":"2026-09-04T15:00:00.000Z","mode":"live","failures_in_a_row":0,"last_ok_at":"2026-09-05T18:41:03.000Z","flagged":false,"created_by":"ada@acme-labs.com"}],"next_cursor":null},"errors":[]},{"method":"POST","path":"/v1/webhooks","scope":"webhooks:write","status":201,"summary":"Register an endpoint. The response is the ONLY time the signing secret is shown: store it.","params":[{"name":"url","in":"body","type":"string","required":true,"description":"https in production. No localhost or private addresses."},{"name":"events","in":"body","type":"array","required":false,"description":"Event types to receive, or [\"*\"] for all (the default). Types: order.received, order.on_hold, order.picking, order.packed, order.shipped, order.delivered, order.cancelled, order.exception, order.returned, inventory.low, inventory.received, inbound.received, inbound.discrepancy, invoice.finalized, ping, connector.unmapped."}],"requestExample":{"url":"https://example.com/hooks/veriti","events":["order.shipped","order.delivered"]},"responseExample":{"id":"whk_9pL2xN7vB4kM1qR6tW3s","url":"https://example.com/hooks/veriti","events":["order.shipped","order.delivered"],"active":true,"created_at":"2026-09-04T15:00:00.000Z","mode":"live","secret":"whsec_4kP9nM2xQ7zT1wL5vB8rY3cD6fG0hJ2kL4mN6pQ8sT"},"errors":["validation_error"]},{"method":"DELETE","path":"/v1/webhooks/:id","scope":"webhooks:write","status":200,"summary":"Remove an endpoint and its delivery log.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The whk_ id."}],"responseExample":{"deleted":true,"id":"whk_9pL2xN7vB4kM1qR6tW3s"},"errors":["not_found"]},{"method":"POST","path":"/v1/webhooks/:id/test","scope":"webhooks:write","status":202,"summary":"Queue a signed `ping` delivery to one endpoint so you can verify your receiver. It arrives within the worker interval (15s).","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The whk_ id."}],"responseExample":{"queued":true,"event_id":"evt_8xN2kP7mQ4zT1wL9vB5r","delivery_id":91,"url":"https://example.com/hooks/veriti"},"errors":["not_found"]},{"method":"GET","path":"/v1/events","scope":"read","status":200,"summary":"Poll alternative to webhooks: your events oldest first. Hand the last id back as since_id. The same evt_ ids ride on webhook deliveries, so you can dedupe across both.","params":[{"name":"since_id","in":"query","type":"string","required":false,"description":"Only events after this evt_ id. Omit to start from the beginning."},{"name":"types","in":"query","type":"string","required":false,"description":"Comma-separated event types to include."},{"name":"limit","in":"query","type":"integer","required":false,"description":"Page size, 1 to 200. Default 50."}],"responseExample":{"data":[{"id":"evt_2wT8kP4nM6xQ1zL9vR5b","type":"order.shipped","created_at":"2026-09-05T18:41:02.000Z","data":{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"shipped","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"USPS|usps_priority","carrier":"usps","tracking_number":"9400111899223197428490","tracking_url":"https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490","label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":"2026-09-05T18:41:02.000Z","delivered_at":null,"cancelled_at":null,"mode":"live"}}],"next_cursor":"evt_2wT8kP4nM6xQ1zL9vR5b"},"errors":["validation_error"]},{"method":"GET","path":"/v1/invoices","scope":"read","status":200,"summary":"Your finalized invoices (open and paid), newest period first. Drafts are the warehouse's until finalized.","params":[],"responseExample":{"data":[{"id":"inv_4nK7mP2xQ9zT1wL5vB8r","period_start":"2026-08-01","period_end":"2026-08-31","status":"open","orders_count":42,"subtotal_cents":46350,"total_cents":46350,"lines":[{"kind":"fulfillment","description":"ACMELABS-3-00042 (Lab Rat)","qty":1,"unit_cents":1050,"total_cents":1050,"order_id":"ord_3kQ9zT7yW2mL5nR8pV1c"},{"kind":"postage","description":"ACMELABS-3-00042 USPS Priority","qty":1,"unit_cents":812,"total_cents":812,"order_id":"ord_3kQ9zT7yW2mL5nR8pV1c"}]}],"next_cursor":null},"errors":[]},{"method":"GET","path":"/v1/invoices/:id","scope":"read","status":200,"summary":"One invoice with its lines. Lines that belong to an order carry its ord_ id.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The inv_ id."}],"responseExample":{"id":"inv_4nK7mP2xQ9zT1wL5vB8r","period_start":"2026-08-01","period_end":"2026-08-31","status":"open","orders_count":42,"subtotal_cents":46350,"total_cents":46350,"lines":[{"kind":"fulfillment","description":"ACMELABS-3-00042 (Lab Rat)","qty":1,"unit_cents":1050,"total_cents":1050,"order_id":"ord_3kQ9zT7yW2mL5nR8pV1c"},{"kind":"postage","description":"ACMELABS-3-00042 USPS Priority","qty":1,"unit_cents":812,"total_cents":812,"order_id":"ord_3kQ9zT7yW2mL5nR8pV1c"}]},"errors":["not_found"]},{"method":"GET","path":"/v1/catalog","scope":"read","status":200,"summary":"The Veriti catalog codes, so you can map your SKUs (POST /v1/skus catalog_code) to what is on the shelf.","params":[],"responseExample":{"data":[{"code":"VC-BPC-5","name":"BPC-157","strength":"5mg"},{"code":"VC-GHK-50","name":"GHK-Cu","strength":"50mg"}],"next_cursor":null},"errors":[]}],"meta":{"baseUrl":"/v1","overview":{"intro":["This page is for the developer connecting a store to the warehouse. The shape of every integration is the same: you register your SKUs once, you create an order when a customer has paid, the warehouse picks, packs and ships it, and the tracking number comes back to you by webhook or by polling. Stock counts flow the same way, so your store can stop selling what the shelf does not have.","Anything that can make an HTTPS request can integrate, and the common stores need no code at all. [Shopify](#platform-shopify) and [WooCommerce](#platform-woocommerce) post their order webhooks to a connector address on this warehouse; a [Stonegate](#platform-stonegate) store connects with two settings; any store with an export uploads a [CSV](#platform-csv) from the portal; and the [three calls](#platform-custom) a custom cart needs are at the bottom of this page. Whichever door an order comes through, [the warehouse does the same thing with it](#floor)."],"facts":[["Format","JSON in, JSON out. Send `Content-Type: application/json` on POST and PATCH. A body is at most 1 MB."],["Time","Every timestamp is ISO-8601 UTC, e.g. `2026-09-04T16:02:33.000Z`. Dates that are days rather than instants (an expiry, an invoice period) are `YYYY-MM-DD`."],["Money","Integer cents in fields ending `_cents`. There are no floats anywhere."],["Ids","Public ids are prefixed strings you cannot guess: `ord_`, `inb_`, `whk_`, `inv_`, `evt_`, `mer_`. A SKU is identified by the string you gave it."]],"testKeys":"Every merchant can mint two kinds of key in the portal. A **live** key (`vc_live_...`) creates real orders that the warehouse picks and bills. A **test** key (`vc_test_...`) creates orders in test mode: nothing is reserved, the warehouse never picks them, billing never counts them, and every webhook still fires. That last part is the point: build against a test key, watch the whole event sequence land on your endpoint, then swap the key. Live and test are two worlds: each key only sees orders, events and webhook endpoints of its own mode, so a test key can never read a live customer's address, and an endpoint registered with a test key only ever receives test events."},"auth":"Send your key on every request as a bearer token: `Authorization: Bearer vc_live_...` (or `vc_test_...`). Mint keys in the portal under **Settings > API keys**; the full key is shown once, and lists show only its first 12 characters afterwards.","authTable":[["No key, a malformed key, a revoked key, or the merchant account is closed","`401 unauthorized`"],["The key is fine but lacks the scope the route needs","`403 forbidden`"],["The merchant account is paused by the warehouse","`403 merchant_paused` on every call until it is resumed"]],"scopes":[["read","Every GET."],["orders:write","Create, edit, hold and cancel orders."],["inventory:write","Create and edit SKUs, announce and cancel inbound shipments."],["webhooks:write","Register, remove and test webhook endpoints."]],"scopesNote":"A key carries scopes; the default is all of them. Any `:write` scope implies `read`. Each route in the reference names the scope it needs.","errorsIntro":"Every error is JSON with one `error` object. `message` is a plain English sentence written for the developer reading it; `field` is present on a validation error and names the offending field in dot-and-bracket form (`address.state`, `items[0].sku`).","errorShape":{"error":{"code":"validation_error","message":"ZIP code is required.","field":"address.zip"}},"errors":[{"code":"unauthorized","status":401,"when":"No usable API key on the request (missing, malformed, revoked), or the merchant account is closed."},{"code":"forbidden","status":403,"when":"The key is valid but lacks the scope this route needs."},{"code":"merchant_paused","status":403,"when":"The merchant account is paused by the warehouse; every call answers this until it is resumed."},{"code":"not_found","status":404,"when":"No object with that id belongs to you (in this key's mode), or no such route."},{"code":"validation_error","status":400,"when":"A field is missing or malformed; `field` names it."},{"code":"bad_json","status":400,"when":"The request body is not valid JSON."},{"code":"payload_too_large","status":413,"when":"The body is over 1 MB."},{"code":"conflict","status":409,"when":"The request is valid but the object refuses it in its current state: a duplicate `external_id` (the body also carries the existing order as `order`), cancelling an order the warehouse is already picking, cancelling an inbound shipment that has been received."},{"code":"idempotency_mismatch","status":409,"when":"The `Idempotency-Key` was already used with a different path or body."},{"code":"rate_limited","status":429,"when":"Over the per-key budget. Wait `Retry-After` seconds."},{"code":"internal","status":500,"when":"Something failed on our side. Retry with the same `Idempotency-Key`."}],"pagination":["Every list answers `{ \"data\": [...], \"next_cursor\": \"...\" | null }`. Pass `?limit=` (1 to 200, default 50) and, for the next page, `?cursor=<next_cursor>`. The cursor is opaque; hand it back exactly as you received it. `next_cursor` is `null` on the last page.","Orders, inbound shipments and ledger rows are newest first. Events are the exception: they are oldest first and use `?since_id=` instead of a cursor, so a poller can remember the last id it saw."],"paginationExample":"GET /v1/orders?limit=100\n{ \"data\": [ ... 100 orders ... ], \"next_cursor\": \"4171\" }\n\nGET /v1/orders?limit=100&cursor=4171\n{ \"data\": [ ... ], \"next_cursor\": null }","idempotency":["On any non-GET request you can send an `Idempotency-Key` header: any string up to 200 characters (a UUID is a fine choice). A repeat of the same key with the same method, path and body within 24 hours replays the original response, same status and same body, with an `Idempotency-Replayed: true` header, and does nothing again. The same key with a different path or body answers `409 idempotency_mismatch`.","Use it on `POST /v1/orders`: a timeout on your side, followed by a retry, can then never create two orders. Derive the key from your own order id (`shopify-5512345678`, `woo-1042`) so every retry of the same order carries the same key without you storing anything. The unique `external_id` is a second net: even without a key, a duplicate is refused with the existing order in the 409 body.","Only a success (and the duplicate-`external_id` 409, which embeds the existing order) is remembered under a key. A refusal is not: a `400` for a SKU you have not registered yet re-runs on the next try, so fixing the cause and retrying with the same key works. Keys are scoped to the key's mode: an `Idempotency-Key` used with a test key is fresh again when you swap in the live key."],"rateLimit":"Each key has a budget of requests per minute (600 by default; the warehouse can change it per account). Over it, the answer is `429 rate_limited` with a `Retry-After` header in seconds. The window is a fixed UTC minute and the count is persisted, so a deploy on our side does not reset it. Back off on a 429 rather than retrying in a tight loop; a webhook subscription costs you no requests at all, which is one reason to prefer it over polling.","timestamps":"Every timestamp is ISO-8601 UTC (`2026-09-04T16:02:33.000Z`). Dates that are days, not instants (an expiry, an invoice period) are `YYYY-MM-DD`. Money is integer cents in fields ending `_cents`.","webhooks":{"intro":"Register an HTTPS endpoint with `POST /v1/webhooks` (or in the portal under **Settings > Webhooks**) and the warehouse POSTs you a signed JSON event every time something happens to your orders, stock or invoices. The signing secret is shown once, on the create response; store it. Subscribe to specific event types or to `[\"*\"]` for everything, including types added later. An endpoint receives the events of the mode it was registered in (a test key registers a test endpoint; the portal registers live ones unless you say test).","events":[{"type":"order.received","when":"An order was created with every line reserved, or came back to the queue after a hold, a backorder or an exception was cleared.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.on_hold","when":"An order is waiting: `hold_reason` is `backorder` (short stock; it releases itself when stock arrives) or `merchant` (you asked for a hold).","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.picking","when":"The warehouse started picking it.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.packed","when":"Packed and waiting for a label.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.shipped","when":"A label was bought or a tracking number entered. `data.shipping` carries `carrier`, `tracking_number` and `tracking_url`. This is the one most integrations act on.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.delivered","when":"The carrier reported delivery.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.cancelled","when":"Cancelled by you or by the warehouse; the reservation is released.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.exception","when":"The warehouse flagged a problem (`exception_reason`: an undeliverable address, damage). It goes back to `received` or to `cancelled` from there.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"order.returned","when":"A shipped parcel came back to the warehouse. Restocking is a separate warehouse action.","data":"The **Order** object, exactly as `GET /v1/orders/:id` returns it. Read its `status`, `hold_reason`, `exception_reason` and `shipping` rather than assuming from the type."},{"type":"inventory.low","when":"A SKU's available count dropped to or below its `low_stock_threshold`. Once per crossing: it fires again only after stock goes back above the line and drops again.","data":"`{ sku, name, on_hand, reserved, available, threshold }`"},{"type":"inventory.received","when":"Stock from an inbound shipment was booked into lots.","data":"`{ inbound_id, reference, items: [{ sku, qty_received, lot_number, expires_at }] }`"},{"type":"inbound.received","when":"An inbound shipment was closed with every line matching what you announced.","data":"The **Inbound** object, as `GET /v1/inbound/:id` returns it."},{"type":"inbound.discrepancy","when":"An inbound shipment was closed with counts that differ from what you announced; compare `qty_expected` and `qty_received` per line.","data":"The **Inbound** object, as `GET /v1/inbound/:id` returns it."},{"type":"invoice.finalized","when":"The warehouse finalized a monthly invoice (draft to open).","data":"The **Invoice** object with its lines."},{"type":"ping","when":"You called `POST /v1/webhooks/:id/test`.","data":"`{ endpoint_id, url, message }`"},{"type":"connector.unmapped","when":"A store connection or a CSV import brought in an order with a SKU code your account does not know. The order is `on_hold` with `hold_reason` `unmapped_sku` and nothing ships until every code on it is mapped: in the portal under Connect your store, map the code to one of your SKUs (or register the SKU first), which releases the hold. Subscribe with `[\"*\"]` or by name.","data":"`{ order, unmapped: [{ code, name, qty }], connector_id }` from a connection, `{ order, unmapped, source: \"csv\" }` from an import; `order` is the **Order** object."}],"payloadIntro":"Every delivery is a POST whose body is one event envelope. The same `evt_` id appears in `GET /v1/events`, so you can dedupe across both.","payload":{"id":"evt_2wT8kP4nM6xQ1zL9vR5b","type":"order.shipped","created_at":"2026-09-05T18:41:02.000Z","merchant_id":"mer_7Hq2kLx9Pz3RtV8wYb1N","data":{"id":"ord_3kQ9zT7yW2mL5nR8pV1c","external_id":"SHOP-1001","order_number":"ACMELABS-3-00042","status":"shipped","hold_reason":"","exception_reason":"","customer":{"name":"Pat Doe","email":"pat@example.com","phone":"303-555-0100"},"address":{"address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","state":"CO","zip":"80202","country":"US"},"items":[{"sku":"BPC-5","name":"BPC-157 5mg","qty":2,"lot_number":"L2409A"}],"shipping":{"service_requested":"USPS Priority","service":"USPS|usps_priority","carrier":"usps","tracking_number":"9400111899223197428490","tracking_url":"https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490","label_url":null},"packaging_tier":"labrat","notes":"","gift_message":"","created_at":"2026-09-04T16:02:33.000Z","shipped_at":"2026-09-05T18:41:02.000Z","delivered_at":null,"cancelled_at":null,"mode":"live"}},"headers":[["X-Veriti-Signature","`t=<unix seconds>,v1=<hex>` (see below)"],["X-Veriti-Event","The event type, so you can route before parsing"],["X-Veriti-Delivery","The delivery id; the same number the portal's delivery log shows"],["Content-Type","`application/json`"],["User-Agent","`Veriti-Webhooks/1.0`"]],"signingIntro":"`v1` is HMAC-SHA256 with your endpoint secret over the string `t + \".\" + rawBody`: the timestamp, one dot, then the request body **exactly as received**. Three rules keep it honest:","signingRules":["**Verify before you parse.** Compute the HMAC over the raw bytes. A re-serialized JSON object will not match (key order, whitespace, unicode escapes all change the bytes).","**Compare timing-safe** (`timingSafeEqual`, `hash_equals`, `hmac.compare_digest`), never with `==`.","**Refuse a stale `t`.** Reject anything more than 5 minutes from your clock; that closes replays of a captured delivery."],"signingRotation":"The header may carry more than one `v1=` value while a secret is being rotated; accept a match on any of them.","signature":"Every delivery is a POST with the JSON payload as the body and an `X-Veriti-Signature` header of the form `t=<unix seconds>,v1=<hex>`, where v1 is HMAC-SHA256 with your endpoint secret over the string `${t}.${rawBody}`. Verify over the raw bytes, compare timing-safe, and refuse a `t` more than 5 minutes from your clock. Answer any 2xx within 10 seconds; anything else (or a timeout) is retried after 1m, 5m, 30m, 2h, 6h, 12h, 24h, 24h and then marked failed. After 20 consecutive failures the endpoint is flagged in your portal (it stays active).","snippets":[{"id":"node","title":"Node","code":"import { createHmac, timingSafeEqual } from 'node:crypto';\n\n// Verify over the RAW request body bytes, never a re-serialized object.\nexport function verifyVeritiSignature(secret, header, rawBody, toleranceSec = 300) {\n  const parts = {};\n  for (const kv of String(header || '').split(',')) {\n    const i = kv.indexOf('=');\n    if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();\n  }\n  const t = parts.t || '';\n  if (!/^\\d+$/.test(t)) return false;\n  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false; // replay window\n  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');\n  const given = String(parts.v1 || '');\n  return given.length === expected.length && timingSafeEqual(Buffer.from(given, 'hex'), Buffer.from(expected, 'hex'));\n}\n\n// Express: a raw body parser on the webhook path, answer 2xx first, do the work after.\napp.post('/hooks/veriti', express.raw({ type: '*/*' }), (req, res) => {\n  if (!verifyVeritiSignature(process.env.VERITI_WEBHOOK_SECRET, req.get('x-veriti-signature'), req.body)) return res.status(403).end();\n  const event = JSON.parse(req.body.toString('utf8')); // { id: 'evt_...', type, created_at, merchant_id, data }\n  res.status(200).end();\n  handle(event);\n});"},{"id":"php","title":"PHP","code":"<?php\n// Verify over the raw body. Answer 200 first, then do the work.\n$secret = getenv('VERITI_WEBHOOK_SECRET');\n$raw = file_get_contents('php://input');\n$header = $_SERVER['HTTP_X_VERITI_SIGNATURE'] ?? '';\n$parts = [];\nforeach (explode(',', $header) as $kv) { [$k, $v] = array_pad(explode('=', $kv, 2), 2, ''); $parts[trim($k)] = trim($v); }\n$t = $parts['t'] ?? ''; $v1 = $parts['v1'] ?? '';\nif (!ctype_digit($t) || abs(time() - (int)$t) > 300) { http_response_code(403); exit; }\n$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);\nif (!hash_equals($expected, $v1)) { http_response_code(403); exit; }\nhttp_response_code(200);\n$event = json_decode($raw, true); // ['id' => 'evt_...', 'type' => ..., 'data' => [...]]"},{"id":"python","title":"Python","code":"import hmac, hashlib, time\n\ndef verify_veriti_signature(secret: str, header: str, raw_body: bytes, tolerance_sec: int = 300) -> bool:\n    parts = dict(kv.strip().split('=', 1) for kv in (header or '').split(',') if '=' in kv)\n    t = parts.get('t', '')\n    if not t.isdigit() or abs(int(time.time()) - int(t)) > tolerance_sec:\n        return False  # malformed, or outside the replay window\n    expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, parts.get('v1', ''))\n\n# Flask: verify request.get_data() (the raw bytes), answer 200, then do the work in a task.\n@app.post('/hooks/veriti')\ndef veriti_hook():\n    if not verify_veriti_signature(os.environ['VERITI_WEBHOOK_SECRET'], request.headers.get('X-Veriti-Signature', ''), request.get_data()):\n        abort(403)\n    event = request.get_json(force=True)\n    queue.enqueue(handle_event, event)\n    return '', 200"}],"nodeSnippet":"import { createHmac, timingSafeEqual } from 'node:crypto';\n\n// Verify over the RAW request body bytes, never a re-serialized object.\nexport function verifyVeritiSignature(secret, header, rawBody, toleranceSec = 300) {\n  const parts = {};\n  for (const kv of String(header || '').split(',')) {\n    const i = kv.indexOf('=');\n    if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();\n  }\n  const t = parts.t || '';\n  if (!/^\\d+$/.test(t)) return false;\n  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false; // replay window\n  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');\n  const given = String(parts.v1 || '');\n  return given.length === expected.length && timingSafeEqual(Buffer.from(given, 'hex'), Buffer.from(expected, 'hex'));\n}\n\n// Express: a raw body parser on the webhook path, answer 2xx first, do the work after.\napp.post('/hooks/veriti', express.raw({ type: '*/*' }), (req, res) => {\n  if (!verifyVeritiSignature(process.env.VERITI_WEBHOOK_SECRET, req.get('x-veriti-signature'), req.body)) return res.status(403).end();\n  const event = JSON.parse(req.body.toString('utf8')); // { id: 'evt_...', type, created_at, merchant_id, data }\n  res.status(200).end();\n  handle(event);\n});","phpSnippet":"<?php\n// Verify over the raw body. Answer 200 first, then do the work.\n$secret = getenv('VERITI_WEBHOOK_SECRET');\n$raw = file_get_contents('php://input');\n$header = $_SERVER['HTTP_X_VERITI_SIGNATURE'] ?? '';\n$parts = [];\nforeach (explode(',', $header) as $kv) { [$k, $v] = array_pad(explode('=', $kv, 2), 2, ''); $parts[trim($k)] = trim($v); }\n$t = $parts['t'] ?? ''; $v1 = $parts['v1'] ?? '';\nif (!ctype_digit($t) || abs(time() - (int)$t) > 300) { http_response_code(403); exit; }\n$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);\nif (!hash_equals($expected, $v1)) { http_response_code(403); exit; }\nhttp_response_code(200);\n$event = json_decode($raw, true); // ['id' => 'evt_...', 'type' => ..., 'data' => [...]]","retrySchedule":["1m","5m","30m","2h","6h","12h","24h","24h"],"retryScheduleMs":[60000,300000,1800000,7200000,21600000,43200000,86400000,86400000],"timeoutSec":10,"toleranceSec":300,"flagAfterFailures":20,"retriesIntro":"A delivery counts as received when your endpoint answers any 2xx within 10 seconds. Anything else, or a timeout, is retried after **1m, 5m, 30m, 2h, 6h, 12h, 24h, 24h** and then marked `failed`. After 20 consecutive failures the endpoint is flagged in your portal; it stays active and keeps receiving. The portal's delivery log shows every attempt with its status code and lets you retry a failed delivery by hand.","receiverRules":["**Answer 2xx fast, then work.** Put the event on a queue and return; a receiver that does its database writes before answering is the usual cause of timeouts and duplicate deliveries.","**Verify before parsing.** Do not `JSON.parse` a body you have not verified.","**Dedupe on `id`.** A delivery can arrive more than once (a retry after a slow 2xx, a hand retry from the portal). Keep the last few thousand `evt_` ids you have processed and skip repeats.","**Do not assume order.** Two events for the same order can arrive out of sequence. Read the object's own `status` and timestamps, and treat a transition backwards (a picking event after you already saw the shipped one) as stale.","**Test with a ping.** `POST /v1/webhooks/:id/test` queues a signed `ping` to one endpoint so you can prove your verification before an order ever exists.","**Polling is the fallback.** `GET /v1/events?since_id=` returns the same events, oldest first, for a backfill or for a platform that cannot receive HTTP."]},"testMode":"Mint a test key (`vc_test_...`) in the portal. Orders it creates are `mode: \"test\"`: no stock is reserved, the warehouse never picks them, they are never billed, and every webhook fires. The warehouse console has a **Simulate** action for a test order that walks it through picking, packed and shipped with a fake tracking number, so you can watch the whole event sequence land on your endpoint; ask the warehouse to run it on your test order.","quickstart":[{"title":"Mint a test key","text":"In the portal: **Settings > API keys > New key**, mode **test**, give it a label. Copy the `vc_test_...` value; it is shown once."},{"title":"Register a SKU","code":"curl -X POST \"$BASE/v1/skus\" -H \"Authorization: Bearer $KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"sku\":\"BPC-5\",\"name\":\"BPC-157 5mg\",\"catalog_code\":\"VC-BPC-5\",\"size_label\":\"5mg\",\"weight_oz\":0.5,\"low_stock_threshold\":20}'","text":"Map `catalog_code` from `GET /v1/catalog` when you can; it is what the warehouse reads on the shelf. The call is an upsert by `sku`, so running it again is fine."},{"title":"Create an order","code":"curl -X POST \"$BASE/v1/orders\" -H \"Authorization: Bearer $KEY\" -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: test-1001\" \\\n  -d '{\"external_id\":\"TEST-1001\",\"customer\":{\"name\":\"Pat Doe\",\"email\":\"pat@example.com\"},\n       \"address\":{\"address1\":\"1600 Larimer St\",\"city\":\"Denver\",\"state\":\"CO\",\"zip\":\"80202\",\"country\":\"US\"},\n       \"items\":[{\"sku\":\"BPC-5\",\"qty\":2}],\"shipping_service\":\"USPS Priority\"}'","text":"Read the `ord_` id back. Run the same curl again and you get the same response with `Idempotency-Replayed: true`; change the key and you get `409 conflict` with the existing order in the body."},{"title":"Register a webhook","code":"curl -X POST \"$BASE/v1/webhooks\" -H \"Authorization: Bearer $KEY\" -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"https://example.com/hooks/veriti\",\"events\":[\"*\"]}'","text":"Store the `secret` from the response and drop one of the verify snippets above into your receiver. Then `POST /v1/webhooks/<whk_id>/test` and confirm the `ping` verifies."},{"title":"Simulate the order","text":"A test order is never picked by a person. The warehouse console has a **Simulate** action for test orders that walks one through picking, packed and shipped with a fake USPS tracking number, so every event lands on your endpoint in the order a real one would. Ask the warehouse to run it on your test order."},{"title":"Receive the shipped event, then go live","text":"Your receiver gets the received, picking, packed and shipped events; the last one carries the tracking number in `data.shipping`. `GET $BASE/v1/orders/<ord_id>` shows the same thing. Mint a live key, swap it in, and the next paid order is real."}],"platforms":[{"id":"shopify","title":"Shopify","intro":["Built in. Shopify posts its order webhook straight to a connector address on this warehouse: nothing to host, nothing to install on the store. Tracking goes back to Shopify once you give the connection a custom-app token (step 4, optional).","1. In the portal: **Settings > Connect your store > Shopify**. Type the store's `.myshopify.com` domain and choose whether line items are matched by **SKU** (the usual choice) or by product **title**. The portal shows the webhook URL for this connection, `https://HOST/connect/shopify/con_...`.","2. In Shopify: **Settings > Notifications > Webhooks > Create webhook**. Event **Order payment**, format **JSON**, URL the one from step 1, the API version left at its default. Save.","3. Under Shopify's webhook list is the signing secret (one per store). Paste it into the connection in the portal. Until it is set every delivery answers `401` and shows as \"refused: bad signature\" in the connection's event log, which is the tell for a wrong paste.","4. Optional, for tracking write-back: **Settings > Apps and sales channels > Develop apps > Create an app**, give it the `read_orders` and `write_fulfillments` Admin API scopes, install it on the store, and paste its Admin API access token into the connection. Without it the order still ships and the tracking number is in the portal and the API; Shopify is simply not told.","5. Register each product's SKU string here (portal **Settings > SKUs**, or `POST /v1/skus`) before the first order lands. An order with a SKU the account does not know is not dropped: it is created `on_hold` (`unmapped_sku`) and the portal offers a one-click map to a SKU, which releases it."],"after":["What the connection does with a delivery: `orders/paid` (and `orders/create` when `financial_status` is already `paid`) creates one order with `external_id` = the Shopify order id and the `#1001` number in the notes; a second delivery of the same order is a duplicate and answers 200. `orders/cancelled` (add a second webhook on **Order cancellation** if you want this) cancels the order while it is `received` or `on_hold`; once the warehouse is picking, a note is left on the order instead and the answer says so. Every delivery is answered within the request, and the last one is shown with its topic and result under **Test** in the portal.","On `order.shipped`, with a token on file, the warehouse lists the order's fulfillment orders and creates one fulfillment carrying `tracking_info` (number, company, url) with `notify_customer: true`, which is what sends Shopify's own shipping email. If Shopify is unreachable the write-back retries on the webhook schedule; the order's timeline records the outcome either way."]},{"id":"woocommerce","title":"WooCommerce","intro":["Built in. WooCommerce posts its order webhook straight to a connector address here: no plugin, no bridge. The secret is ours: the portal generates it when you add the connection, and you type it into WooCommerce's webhook form.","1. In the portal: **Settings > Connect your store > WooCommerce**. Type the site URL and choose SKU or product-name matching. The portal shows the webhook URL and the secret (once; copy it before you close the panel).","2. In WordPress: **WooCommerce > Settings > Advanced > Webhooks > Add webhook**. Name it anything, status **Active**, topic **Order updated**, delivery URL the one from step 1, secret the one from step 1, API version **WP REST API Integration v3**. Save.","3. WooCommerce sends a ping when the webhook is saved; the portal shows it under **Test** as \"ping: the store reached us\". From then on every order change arrives. The connection creates the order the first time it sees `status` `processing` or `completed`, and treats every later `order.updated` for the same order as the duplicate it is.","4. Optional, for tracking write-back: **WooCommerce > Settings > Advanced > REST API > Add key**, permissions **Read/Write**, and paste the consumer key and consumer secret into the connection. The site must be https.","5. Register each product's SKU string here before the first order (the product's **SKU** field in WooCommerce). An order with an unknown SKU is created `on_hold` (`unmapped_sku`) and released with one click once you map it."],"after":["Mapping: `id` is the `external_id`, `number` (the #8812 the store shows) goes in the notes, the ship-to comes from `shipping` and falls back to `billing` when the shipping block is blank (a \"ship to billing address\" store), the email from `billing.email`, the service from `shipping_lines[0].method_title`. `cancelled`, `refunded` and `trash` cancel while the warehouse still can, else leave a note on the order.","On `order.shipped`, with REST keys on file, the warehouse sets the order to **completed** (which sends WooCommerce's own completed email) and adds a customer-visible order note, \"Shipped via USPS 9400... <tracking url>\". Without keys the tracking number is in the portal and the API, and the write-back task is marked done with \"no write-back credentials\"."]},{"id":"stonegate","title":"Stonegate","intro":["A store built on the Stonegate platform ships with the connector built in. There is nothing to write; the connection is two settings plus one secret, all three shown in the portal under **Settings > Connect your store > Stonegate**.","1. Mint a **live** key in this portal (Settings > API keys).","2. On the store, set `FULFILLMENT_API_URL` to this host (no trailing slash, no `/v1`) and `FULFILLMENT_API_KEY` to the key. Either as environment variables on the store's service, or pasted into the store admin's **Fulfillment** settings; a value saved in the admin wins over the env.","3. Register a webhook in this portal pointing at `https://<your store>/api/fulfillment/webhook` with events `[\"*\"]`, and paste the secret it shows you into the store admin's Fulfillment settings as the webhook secret (or set `FULFILLMENT_WEBHOOK_SECRET`). The store's route answers 404 until the secret is set, so it never accepts an unsigned event."],"after":["What the store then does on its own, every two minutes: pushes every **paid**, shippable order that is not at the warehouse yet (`external_id` is the store's own order number, the `Idempotency-Key` is derived from the order, lines are keyed by the product's SKU); asks about every pushed order until it is delivered and applies the shipped status (tracking + the store's own shipped email) and the delivered status; tells the warehouse when a pushed order is cancelled or refunded on the store; and mirrors `available` from `GET /v1/inventory` into each product's stock. The webhook applies the same transitions in seconds instead of minutes. While stock sync is on, **the warehouse is authoritative for stock**: the store sells what is on the shelf minus what is reserved for orders already in the queue, and an owner who counts their own shelf turns sync off in the admin.","Two things the store never does: it never cancels a paid order on the warehouse's word (a cancelled, exception or returned answer flags the order for review with the reason, and the owner decides), and it never buys its own label for an order the warehouse owns (the store's Shippo paths step aside for any order with a warehouse id)."]},{"id":"csv","title":"CSV import","intro":["For a store with no webhook, an older platform, or a first backlog: upload a spreadsheet in the portal (**Settings > Connect your store > CSV**) and the warehouse creates the orders. Two file shapes are understood, told apart by the header row.","**The template.** Download it from the import panel (`GET /portal/api/orders/import-template`): one row per line item, `external_id, name, email, phone, address1, address2, city, state, zip, country, sku, qty, shipping_service, notes`. Rows with the same `external_id` are one order, and the order-level columns are read from the first row that has them. A handful of obvious other headings are accepted (`order id`, `quantity`, `postal code`, `province`). Countries may be the 2-letter code or the English name; a US state is normalized to its code.","**The Shopify orders export.** Orders > Export in Shopify, as it comes: one row per line item grouped by `Name`, only orders whose `Financial Status` is `paid`; orders Shopify already fulfilled or cancelled are skipped and listed as such. `Id` (or `Name`) becomes the `external_id`, the same value the Shopify connection uses, so an order imported today and its webhook tomorrow are one order.","**Preview, then confirm.** The panel first shows what the file would do: the orders it would create, every row it will not import with its spreadsheet row number and a plain reason, the external ids that already exist (skipped, never re-created), and the SKUs the account does not know. Confirm creates them, and the result lists every order under created, skipped-duplicate, held-unmapped or rejected. Nothing is half-imported: an order with a bad row is not created at all."],"after":["An order with some unknown SKUs is created `on_hold` (`unmapped_sku`) with the unknown lines kept on it, exactly like a connector order, and released from the portal once mapped. An order whose every line is unknown is reported rather than created; register the SKUs and import the file again (nothing was written for it, so there is no duplicate)."]},{"id":"custom","title":"Custom cart (the API)","intro":["Three calls matter; everything else on this page is optional.","1. **`POST /v1/skus`**, once per product, with the SKU string your cart already uses. Upsert by `sku`, so run it from a deploy script or an admin button without worrying about duplicates.","2. **`POST /v1/orders`** the moment an order is paid, with your order id as `external_id` and an `Idempotency-Key` derived from it. Store the `ord_` id you get back. Never create the order at checkout start: the warehouse picks on your word that it is paid.","3. **Receive the shipped event** on a webhook (or poll `GET /v1/orders/ext_<your id>` every few minutes) and write `data.shipping.tracking_number` and `tracking_url` onto your order, then send your own shipped email."],"after":["Two more once you are live: `GET /v1/inventory` to stop selling what the shelf does not have (use `inventory.available`, which already subtracts what is reserved and what has expired), and `POST /v1/inbound` to announce each box you send the warehouse, so it is received against your reference instead of guessed at."]}],"connectors":{"intro":["A connection is a row on your account with its own `con_` id and its own address on this warehouse. The store posts its order webhooks there and nothing else calls it. There is no API key on these requests: the id in the URL says whose account it is, and the platform's HMAC over the raw body says the delivery is genuine. A bad signature is `401` and an unknown id is `404`; everything else is `200`, so the store never retries a delivery the warehouse has already decided about.","Once the signature checks out the answer is always `200`, with `result` saying what happened: `created` (the order is in; `order_id` and `order_number` are ours, and `unmapped` lists any SKU codes it is waiting on), `duplicate` (already here; `order_id` names it), `cancelled`, `noted` (the cancel came after the warehouse started picking, so a note was left on the order instead), `ignored` (a topic or status this connection does not act on, or the connection is paused; `message` says which), `ping`, or `refused` with `ok: false` and the plain sentence (`message`, and `field` when it is one field) that the portal's event log shows too."],"routes":[{"method":"POST","path":"/connect/shopify/:id","scope":"","external":true,"group":"connect","status":200,"auth":"No API key. Shopify signs every delivery: `X-Shopify-Hmac-Sha256` is base64(HMAC-SHA256(signing secret, raw body)), checked timing-safe over the bytes as sent. The secret is the one Shopify shows under Settings > Notifications > Webhooks; paste it into the connection in the portal.","summary":"The address a Shopify webhook posts to. `orders/paid` (or `orders/create` when `financial_status` is already `paid`) becomes an order; `orders/cancelled` cancels it while the warehouse still can. Opening the address in a browser (GET) answers a sentence saying what it is for.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The connection's con_ id. The portal shows the full URL under Settings > Connect your store; paste it into the store as the webhook address."},{"name":"X-Shopify-Hmac-Sha256","in":"header","type":"string","required":true,"description":"base64(HMAC-SHA256(secret, raw body)). A mismatch is `401`."},{"name":"X-Shopify-Topic","in":"header","type":"string","required":true,"description":"`orders/paid`, `orders/create` or `orders/cancelled`; anything else is answered `200` with `result: ignored`."},{"name":"body","in":"body","type":"object","required":true,"description":"The order exactly as Shopify sends it (webhook format JSON). Read: `id` (the external_id), `name` (the #1001 the shop shows, kept in the notes), `customer`, `email`, `shipping_address` (`province_code` is the state, `country_code` the country), `line_items` (`sku`, or `title` when the connection matches by title; a line with `requires_shipping: false` is skipped), `shipping_lines[0].title`, `note`."}],"requestExample":{"id":5551001,"name":"#1001","email":"pat@example.com","financial_status":"paid","note":"Leave at the side door","customer":{"first_name":"Pat","last_name":"Doe","email":"pat@example.com","phone":"303-555-0100"},"shipping_address":{"first_name":"Pat","last_name":"Doe","address1":"1600 Larimer St","address2":"Suite 4","city":"Denver","province_code":"CO","zip":"80202","country_code":"US","phone":"303-555-0100"},"line_items":[{"sku":"BPC-5","title":"BPC-157 5mg","quantity":2,"requires_shipping":true}],"shipping_lines":[{"title":"USPS Priority"}]},"responseExample":{"ok":true,"result":"created","order_id":"ord_3kQ9zT7yW2mL5nR8pV1c","order_number":"ACMELABS-3-00042","status":"received","unmapped":[]},"errors":["unauthorized","not_found"]},{"method":"POST","path":"/connect/woocommerce/:id","scope":"","external":true,"group":"connect","status":200,"auth":"No API key. WooCommerce signs every delivery with the secret typed into its webhook form: `X-WC-Webhook-Signature` is base64(HMAC-SHA256(secret, raw body)), checked timing-safe. The portal generates that secret when the connection is added and shows it once.","summary":"The address a WooCommerce webhook posts to. An `order.created` or `order.updated` whose `status` is `processing` or `completed` becomes an order the first time it is seen (every later update of the same order is a duplicate); `cancelled`, `refunded` or `trash` cancels it while the warehouse still can. The `webhook_id` ping WooCommerce sends when the webhook is saved is answered `200` with `result: ping`.","params":[{"name":"id","in":"path","type":"string","required":true,"description":"The connection's con_ id. The portal shows the full URL under Settings > Connect your store; paste it into the store as the webhook address."},{"name":"X-WC-Webhook-Signature","in":"header","type":"string","required":true,"description":"base64(HMAC-SHA256(secret, raw body)). A mismatch is `401`."},{"name":"X-WC-Webhook-Topic","in":"header","type":"string","required":false,"description":"`order.created`, `order.updated` or `order.deleted`. Absent on the ping."},{"name":"body","in":"body","type":"object","required":true,"description":"The order as the WooCommerce REST API v3 renders it. Read: `id` (the external_id), `number` (the #8812 the store shows, kept in the notes), `status`, `billing.email`, `shipping` (`address_1`, `address_2`, `city`, `state`, `postcode`, `country`, `phone`; the `billing` block is used when shipping is blank), `line_items` (`sku`, or `name` when the connection matches by name), `shipping_lines[0].method_title`, `customer_note`."}],"requestExample":{"id":8812,"number":"8812","status":"processing","customer_note":"","billing":{"first_name":"Pat","last_name":"Doe","email":"pat@example.com","phone":"303-555-0100"},"shipping":{"first_name":"Pat","last_name":"Doe","address_1":"1600 Larimer St","address_2":"Suite 4","city":"Denver","state":"CO","postcode":"80202","country":"US"},"line_items":[{"sku":"BPC-5","name":"BPC-157 5mg","quantity":2}],"shipping_lines":[{"method_title":"USPS Priority"}]},"responseExample":{"ok":true,"result":"created","order_id":"ord_3kQ9zT7yW2mL5nR8pV1c","order_number":"ACMELABS-3-00042","status":"received","unmapped":[]},"errors":["unauthorized","not_found"]}],"unmapped":"A line whose SKU code the account does not know never drops the order: it is created `on_hold` with `hold_reason` `unmapped_sku`, the codes are recorded on it, the `connector.unmapped` event fires, and the portal shows the order with a one-click map to one of your SKUs (or a link to register it), which releases the hold. Nothing ships until every code on the order is mapped."},"floor":{"title":"At the warehouse","text":["However the order arrives, the floor sees one queue. When auto-label is on, postage is bought as the order lands (the service your customer chose when it maps to a live rate, else the warehouse's configured service, else the cheapest allowed under the cap) and the label prints itself at the bench, so the order is label-in-hand before anyone walks to the shelf. The order moves to `picking` with \"label bought + printed\" on its timeline. Buying the label does not ship the order.","The packing slip that goes in the box names your store (\"Packed by Veriti Collective for <your store>\"), the order number beside your own order id, every item with the lot it was picked from, the packaging tier you are on with its checklist, your gift message and notes verbatim, and the footer you set in the portal. The warehouse adds no words of its own about the goods.","Shipping is a scan: the packer scans the label on the sealed box, the order behind that tracking number ships, and that scan is the moment stock moves. `order.shipped` fires then, with the tracking number, and your store gets it: through the connection's write-back on Shopify and WooCommerce, the store's own poll or webhook on Stonegate, and the webhook or `GET /v1/orders/:id` for anything else."]},"nonGoals":"Saying so up front saves a support ticket: this version does not do multi-warehouse routing, multi-parcel shipments, an end-customer returns portal, apps you install on Shopify or WooCommerce (the built-in connections are webhook receivers: one webhook on the store's side and nothing to install), invoicing through a payment processor (invoices here are records; the warehouse collects however it collects), product purchasing (product invoices live outside this system), or kitting beyond a SKU that is itself a kit."}}