Ingest
For events, a 202 with an event id means the event is recorded. If it can’t be recorded you get 503 ingest_unavailable. Retry with backoff on 503.
POST /cx/v1/ingest/event
Section titled “POST /cx/v1/ingest/event”Publishable or secret key.
| Field | Type | Notes |
|---|---|---|
type |
string | Required. For example page_view, product_view, add_to_cart, conversion, or a custom name |
visitorRef |
string | Your anonymous visitor reference |
url |
string | Page URL |
productId |
string | Product the event concerns |
value |
number | Monetary value, such as a conversion amount |
properties |
object | Free-form additional attributes |
curl -X POST https://api.plunk.io/cx/v1/ingest/event \ -H "Authorization: Bearer pk_live_••••" -H "Content-Type: application/json" \ -d '{ "type": "product_view", "visitorRef": "v_84af", "productId": "SF-1042" }'await fetch('https://api.plunk.io/cx/v1/ingest/event', { method: 'POST', headers: { Authorization: 'Bearer pk_live_••••', 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'product_view', visitorRef: 'v_84af', productId: 'SF-1042' }),});requests.post( "https://api.plunk.io/cx/v1/ingest/event", headers={"Authorization": "Bearer sk_live_••••"}, json={"type": "product_view", "visitorRef": "v_84af", "productId": "SF-1042"},){ "accepted": true, "eventId": "9d1c…", "type": "product_view"}Reorder requests
Section titled “Reorder requests”A dealer or rep portal can send "type": "reorder_request" with properties: { customerRef, lines: [{ productId, quantity, note }], poNumber, note }. Because it creates an order request, send it from your server with a secret key. A publishable key gets 403 secret_key_required.
It records the event as usual and, when your account runs the Plunk Commerce order desk, also creates an order request draft. The response then adds orderRequestId and orderRequestNumber. See order requests.
POST /cx/v1/ingest/identify
Section titled “POST /cx/v1/ingest/identify”Associates an anonymous visitor with a known identity. visitorRef is required, plus at least one of email, hashedEmail or userId. An optional traits object rides along.
{ "accepted": true, "eventId": "41be…", "visitorRef": "v_84af"}POST /cx/v1/ingest/sale
Section titled “POST /cx/v1/ingest/sale”Secret key only. Records a settled order as real sales data. Revenue dashboards, product and store performance, customer purchase history and lifecycle stages all read from it. This is the endpoint a payment integration calls from its webhook handler: wire it to Stripe’s checkout.session.completed or invoice.paid and your sales arrive as they happen.
- Posting the same
orderIdagain replaces that order’s lines, so webhook retries and corrections are safe. - The buyer behind
customerRefis created on first sight and linked to the sale. A first sale moves them to the Customer lifecycle stage automatically. - A publishable key gets
403 secret_key_required.
| Field | Type | Notes |
|---|---|---|
orderId |
string | Required. Stable per order, such as the Stripe session or invoice id. Reposting it replaces the order’s lines. |
customerRef |
string | Required. Your stable id for the buyer, such as the Stripe customer id. |
lines[].productId |
string | Required. Joins your catalog when the id matches. Unknown ids still count as revenue. |
lines[].price |
number | Required. The extended line total (already multiplied by quantity). Up to 500 lines per order. |
lines[].quantity |
integer | Optional. Defaults to 1. |
lines[].description |
string | Optional line description. |
email |
string | Optional but recommended. Fills the buyer’s contact record. |
customerName |
string | Optional. Defaults to email or customerRef. |
orderDate |
datetime | Optional. Defaults to now. |
storeId · channel · currency |
string | Optional. channel defaults to api. |
status |
string | Optional. Defaults to Shipped (settled). Pass an open-order status only for unsettled orders: they report as backlog, not revenue. |
curl -X POST "https://api.plunk.io/cx/v1/ingest/sale" \ -H "Authorization: Bearer sk_live_••••" -H "Content-Type: application/json" \ -d '{ "orderId": "cs_a1B2c3", "customerRef": "cus_R8xT2m", "email": "buyer@example.com", "channel": "stripe", "currency": "USD", "lines": [ {"productId": "SF-1042", "description": "Harbor Sofa", "quantity": 2, "price": 2398.00} ] }'// Inside your Stripe webhook handler, after verifying the eventif (event.type === 'checkout.session.completed') { const session = event.data.object; await fetch('https://api.plunk.io/cx/v1/ingest/sale', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PLUNK_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ orderId: session.id, customerRef: session.customer, email: session.customer_details?.email, channel: 'stripe', currency: session.currency.toUpperCase(), lines: [{ productId: 'SF-1042', description: 'Harbor Sofa', quantity: 2, price: 2398.0 }], }), });}requests.post( "https://api.plunk.io/cx/v1/ingest/sale", headers={"Authorization": f"Bearer {os.environ['PLUNK_SECRET_KEY']}"}, json={ "orderId": "cs_a1B2c3", "customerRef": "cus_R8xT2m", "email": "buyer@example.com", "channel": "stripe", "currency": "USD", "lines": [{"productId": "SF-1042", "description": "Harbor Sofa", "quantity": 2, "price": 2398.00}], },){ "accepted": true, "orderId": "cs_a1B2c3", "linesWritten": 1, "customerId": "43f7…", "customerCreated": true}Two posts for the same new buyer at the same moment can return 503 ingest_busy. Retry with backoff.
POST /cx/v1/ingest/order-status
Section titled “POST /cx/v1/ingest/order-status”Secret key only. Your ERP’s order status reports: one call per status change, or a batch of up to 500 orders. erpOrderId is the join key to the order’s sales rows, so open and invoiced orders report correctly everywhere. orderRequest or poNumber links the first report to a portal order request. Status words you send are stored as given.
| Field | Type | Notes |
|---|---|---|
orders[].erpOrderId |
string | Required. Your ERP order number (the orderId on its sales rows). |
orders[].status |
string | Required. For example Open, Shipped, Invoiced, Cancelled. |
orders[].statusDate |
datetime | Optional. Defaults to now. |
orders[].orderRequest · poNumber · customerRef |
string | Optional. Link the report to a portal order request. |
orders[].lines[] |
array | Optional: { productId, ordered, shipped, backordered, eta } |
orders[].shipments[] |
array | Optional: { carrier, tracking, shippedDate, lines: [{ productId, qty }] } |
orders[].note |
string | Optional. |
curl -X POST "https://api.plunk.io/cx/v1/ingest/order-status" \ -H "Authorization: Bearer sk_live_••••" -H "Content-Type: application/json" \ -d '{"orders":[ {"erpOrderId":"SO-55120","status":"Shipped","poNumber":"PO-7781", "shipments":[{"carrier":"UPS","tracking":"1Z999…","shippedDate":"2026-10-13", "lines":[{"productId":"SF-1042","qty":2}]}]} ]}'{ "accepted": 1, "rejected": 0, "orders": [ { "erpOrderId": "SO-55120", "orderRequestId": "…", "orderRequestNumber": "OR-1042", "linked": true, "from": "Open", "to": "Shipped", "salesRowsUpdated": 3, "error": null } ]}You get 422 invalid_status_report when no order in the call was accepted (each row’s error says why), and 404 not_tracked when your account doesn’t run the order desk.
POST /cx/v1/ingest/inventory
Section titled “POST /cx/v1/ingest/inventory”Secret key only, scope inventory:write. Push stock levels from your own system. This updates business data and changes product availability. The same numbers can arrive by CSV feed or the Shopify connection instead. All three write identically, so use whichever your stack produces most easily.
| Field | Type | Notes |
|---|---|---|
items[].sku |
string | Required. Must match a product id in your catalog. Unknown SKUs are skipped and counted in the response. Connect the product feed first. |
items[].source |
string | Required. Warehouse or location name. A source containing “transit” counts as sellable inbound stock. |
items[].qtyOnHand |
number | Units on hand at that source. |
items[].qtyBackordered |
number | Optional. |
items[].availableDate |
date | Optional. When inbound units become sellable. |
items[].asOf |
datetime | Optional. Defaults to now. |
curl -X POST "https://api.plunk.io/cx/v1/ingest/inventory" \ -H "Authorization: Bearer sk_live_••••" -H "Content-Type: application/json" \ -d '{"items":[ {"sku":"SF-1042","source":"Main Warehouse","qtyOnHand":6,"qtyBackordered":4}, {"sku":"SF-1042","source":"In Transit","qtyOnHand":30,"availableDate":"2026-10-12"} ]}'{ "accepted": true, "itemsWritten": 2, "skusAggregated": 1, "availabilityUpdated": 1, "unknownSkusSkipped": 0, "sources": ["Main Warehouse", "In Transit"]}- Each request replaces the sources it carries and leaves other sources alone, so a snapshot per warehouse is safe to send independently.
- Up to 5,000 items per request. Page larger snapshots.
- Availability follows the numbers: on hand means in stock, inbound only means backorder, neither means out of stock.
- If no SKU in the push matches your catalog, the response is
422 no_matching_skus.
Related
Section titled “Related”- Install the site tracker for page views and conversions without hand-written calls
- Catalog and availability
- Customer sales and order requests
Still stuck? Contact support and include the job or page you were on.