Build the flows in this order: Locations, Products, Shipping Requests, Fulfillments, Inventory, then inbound shipments and returns. Each one depends on IDs created by the ones before it.
| Flow | Setting | Trigger | Pipe17 calls |
|---|---|---|---|
| Locations | locations.in |
Your warehouse list changes (poll daily or on change) |
POST /locations, PUT /locations/{id}
|
| Products | products.out |
Poll or products webhook |
GET /products |
| Shipping Requests | shipments.out |
Poll or shipments webhook |
GET /shipping_requests, PUT /shipping_requests/{id}, POST /shipping_requests/{id}/reject
|
| Fulfillments | fulfillments.in |
Your system ships a package |
GET /fulfillments, POST /fulfillments
|
| Inventory | inventory.in |
Your stock levels change | POST /inventory |
| Inbound shipments |
arrivals.out, receipts.in
|
Poll arrivals; your system finishes receiving | GET /arrivals, PUT /arrivals/{id}, POST /receipts |
| Returns |
returns.out, returns.in
|
Poll returns; your system receives the items | GET /returns, PUT /returns/{id} |
Pipe17 owns step 1, the routing, step 7 and step 8; your connector owns steps 2 to 6 (sections 7.3 and 7.4).
SKU and shipping-method mapping are configured in the app. Customers set both up per integration, and Pipe17 applies them before data reaches your connector and after your connector writes it back. SKU mapping lets a product use a different SKU in your system (set under Products → All Products → Channel options → Channel SKU). Shipping-method mapping translates the shipping methods on orders into the shippingClass and shippingCarrier your system expects. Your connector reads and writes those fields as they arrive; it does not need its own mapping tables.
7.1 Locations (into Pipe17)
Create one Pipe17 location per warehouse, linked to your integration so routed orders reach you. Store the returned locationId; every Shipping Request and inventory update refers to it.
export async function syncLocation(ctx, wh) { // wh = a warehouse from your system
const body = {
name: wh.name,
address: {
address1: wh.address1, address2: wh.address2, city: wh.city,
stateOrProvince: wh.state, zipCodeOrPostalCode: wh.zip, country: wh.country, phone: wh.phone,
},
externalSystem: [{ integrationId: ctx.integrationId, locationValue: wh.id }],
};
const existing = state.findByExtId('locations', wh.id);
if (existing?.pipe17_id) {
await p17('PUT', `/locations/${existing.pipe17_id}`, { body });
} else {
const res = await p17('POST', '/locations', { body });
state.saveMapping('locations', { pipe17_id: res.location.locationId, ext_id: wh.id });
}
}# Create a location linked to your integration
curl -s -X POST "$PIPE17_BASE_URL/locations" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Reno Warehouse",
"address": {
"address1": "100 Example Way", "city": "Reno", "stateOrProvince": "NV",
"zipCodeOrPostalCode": "89501", "country": "USA", "phone": "555-0100"
},
"externalSystem": [{ "integrationId": "<your integrationId>", "locationValue": "WH-RENO" }]
}'
# Update it later (use the locationId returned above)
curl -s -X PUT "$PIPE17_BASE_URL/locations/<locationId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Reno Warehouse (main)"}'After the locations exist, a Pipe17 admin sets up routing rules so orders are sent to them.
7.2 Products (out of Pipe17)
Poll GET /products with pollStream() and create or update each SKU in your system. Your system must know a SKU before it can receive a Shipping Request that contains it.
Filter what you pull. Most connectors do not need the whole catalog. Filter on the API call rather than in your code:
-
status=activeskips draft and inactive products. -
sku=SKU-1,SKU-2,…limits the pull to the SKUs this warehouse actually stocks.
curl -s -G "$PIPE17_BASE_URL/products" \ -H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \ --data-urlencode "status=active" \ --data-urlencode "sku=TSHIRT-BLK-M,TSHIRT-BLK-L" \ --data-urlencode "updatedSince=2026-10-12T06:00:00Z" \ --data-urlencode "keys=*"
Let each customer choose the SKUs. Read the list from the productSKUs integration setting instead of hard-coding it. Add your own settings in Chapter 3 shows how to define the setting and how each customer fills it in. An empty list means all active products.
// products poll job – getSetting() is defined in Chapter 3
const skus = getSetting(ctx, 'productSKUs') ?? [];
const batches = skus.length ? chunk(skus, 50) : [undefined]; // keep URLs short: 50 SKUs per call
for (const batch of batches) {
await pollStream({
stream: batch ? `products:${batch[0]}` : 'products', path: '/products', listKey: 'products', idField: 'productId',
query: { status: 'active', sku: batch },
handle: (p) => handleProduct(ctx, p),
});
}When the SKU list changes, reset the products cursor (or run a one-off pull without updatedSince) so newly added SKUs are sent even if they have not changed in Pipe17. With several batches, give each its own cursor (for example products:batch-1) so one batch never moves another's cursor.
export async function handleProduct(ctx, p) {
if (!isFlowEnabled(ctx, 'products.out') || p.status !== 'active') return;
const item = { sku: p.sku, name: p.name, upc: p.upc, weight: p.weight, weightUnit: p.weightUnit,
length: p.length, width: p.width, height: p.height, dimensionsUnit: p.dimensionsUnit };
const extId = await mySystem.upsertItem(item); // your system's API
state.saveMapping('products', { pipe17_id: p.productId, ext_id: extId, last_version: p.updatedAt });
}7.3 Shipping Requests (out of Pipe17)
Pick up requests in readyForFulfillment for your locations, create them in your system, then mark them sentToFulfillment and record your ID in extReferenceId.
export async function handleShippingRequest(ctx, sr) {
if (!isFlowEnabled(ctx, 'shipments.out')) return;
if (sr.status === 'canceled' || sr.status === 'canceledRestock') return handleCancel(ctx, sr); // see Cancellations below
if (sr.status !== 'readyForFulfillment') return;
const mapped = state.getMapping('shipments', sr.shipmentId);
if (mapped?.ext_id) return; // already sent
state.saveMapping('shipments', { pipe17_id: sr.shipmentId, last_status: 'sending' });
let extId = await mySystem.findOrderByReference(sr.shipmentId); // crash recovery
if (!extId) {
try {
extId = await mySystem.createOrder({
reference: sr.shipmentId,
orderNumber: sr.extOrderId,
shipTo: sr.shippingAddress,
lines: sr.lineItems.map((l) => ({ sku: l.sku, quantity: l.quantity })),
shippingMethod: sr.shippingClass,
shipBy: sr.shipByDate,
giftNote: sr.giftNote,
});
} catch (err) {
if (err.permanent) { // e.g. unknown SKU, bad address
await p17('POST', `/shipping_requests/${sr.shipmentId}/reject`, { body: { status: 'reviewRequired' } });
return reportException('shipments', sr.shipmentId, err.message); // Chapter 8
}
throw err; // temporary: retry later
}
}
await p17('PUT', `/shipping_requests/${sr.shipmentId}`, {
body: { status: 'sentToFulfillment', extReferenceId: String(extId) },
});
state.saveMapping('shipments', { pipe17_id: sr.shipmentId, ext_id: String(extId), last_status: 'sentToFulfillment' });
}Marking a request as sent, with your order ID:
curl -s -X PUT "$PIPE17_BASE_URL/shipping_requests/<shipmentId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "sentToFulfillment", "extReferenceId": "WO-88812"}'POST /shipping_requests/{id}/reject accepts status values canceled, canceledRestock, failed or reviewRequired. Use reviewRequired when a person can fix the problem and resend.
curl -s -X POST "$PIPE17_BASE_URL/shipping_requests/<shipmentId>/reject" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "reviewRequired"}'Cancellations. When a cancel reaches a request you have already sent, Pipe17 needs your answer: could your system stop it or not? You give that answer in the request's cancellationStatus field: accepted (stopped) or rejected (too late). Once either is set the cancel is settled; never ask your system again.
| Case | How you see it | What you do |
|---|---|---|
| The Shipping Request is canceled in Pipe17 | Your shipping-request poll returns it with status canceled or canceledRestock
|
Never sent (no extReferenceId): set cancellationStatus: accepted. Otherwise ask your system to cancel, then set accepted or rejected
|
| The order is canceled after you received its Shipping Request | The request stays sentToFulfillment or partialFulfillment, and Pipe17 adds an eLateCancel exception to it |
Find these requests, confirm the order is canceled, ask your system to cancel, then set accepted or rejected
|
| Your system cancels the order itself | Your own poll of your system | Set status: canceled and cancellationStatus: accepted
|
When your system refuses because the order is already picked or shipped, set cancellationStatus: rejected and move the request to reviewRequired so someone looks at it. Many connectors let each customer choose that status with a setting. If the order did ship, send its Fulfillment as normal.
const isSettled = (sr) => ['accepted', 'rejected'].includes(sr.cancellationStatus);
export async function handleCancel(ctx, sr) {
if (isSettled(sr)) return; // already answered
const answer = (body) => p17('PUT', `/shipping_requests/${sr.shipmentId}`, { body });
if (!sr.extReferenceId) return answer({ cancellationStatus: 'accepted' }); // never sent
const result = await mySystem.cancelOrder(sr.extReferenceId); // 'canceled' | 'tooLate'
if (result === 'canceled') return answer({ cancellationStatus: 'accepted' });
return answer({ cancellationStatus: 'rejected', status: getSetting(ctx, 'lateCancelStatus') ?? 'reviewRequired' });
}# Requests whose order was canceled after you received them
curl -s -G "$PIPE17_BASE_URL/shipping_requests" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
--data-urlencode "fulfillmentIntegrationId=<your integrationId>" \
--data-urlencode "status=sentToFulfillment,partialFulfillment" \
--data-urlencode "exceptionType=eLateCancel" \
--data-urlencode "keys=shipmentId,orderId,extReferenceId,status,cancellationStatus"
# Confirm the order itself is canceled
curl -s "$PIPE17_BASE_URL/orders/<orderId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY"
# Your system stopped it
curl -s -X PUT "$PIPE17_BASE_URL/shipping_requests/<shipmentId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"cancellationStatus": "accepted"}'
# Too late: refuse and flag for review
curl -s -X PUT "$PIPE17_BASE_URL/shipping_requests/<shipmentId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"cancellationStatus": "rejected", "status": "reviewRequired"}'7.4 Fulfillments (into Pipe17)
When your system ships a package, create a Fulfillment against the Shipping Request. Fulfillments are immutable, so create them only when that fulfillment record is complete. You can create multiple Fulfillments for a shipping request but cannot update a fulfillment once created.
export async function pushFulfillment(ctx, pkg) { // pkg = a shipped package from your system
if (!isFlowEnabled(ctx, 'fulfillments.in')) return;
const sr = state.findByExtId('shipments', pkg.orderId);
if (!sr) throw new Error(`No Shipping Request mapped to ${pkg.orderId}`);
// Idempotency: has this package already been sent?
const found = await p17('GET', '/fulfillments', { query: { extFulfillmentId: pkg.id, keys: 'fulfillmentId' } });
if (found.fulfillments?.length) return;
const res = await p17('POST', '/fulfillments', {
body: [{
shipmentId: sr.pipe17_id,
extFulfillmentId: String(pkg.id),
lineItems: pkg.items.map((i) => ({ sku: i.sku, quantity: i.quantity })),
trackingNumber: [pkg.trackingNumber],
trackingURLs: pkg.trackingUrl ? [pkg.trackingUrl] : [],
shippingCarrier: pkg.carrier,
shippingClass: pkg.service,
actualShipDate: pkg.shippedAt,
fulfillmentType: 'accepted',
}],
});
const result = res.fulfillments[0]; // check each item, not just the HTTP code
if (result.status !== 'submitted') throw new Error(result.message ?? 'Fulfillment failed');
// No status update needed: Pipe17 moves the Shipping Request to partialFulfillment or fulfilled.
}The same two calls with curl:
# 1. Check whether this package was already sent
curl -s -G "$PIPE17_BASE_URL/fulfillments" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
--data-urlencode "extFulfillmentId=PKG-55501" \
--data-urlencode "keys=fulfillmentId"
# 2. Create the fulfillment (the body is an array, up to 10 items)
curl -s -X POST "$PIPE17_BASE_URL/fulfillments" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '[{
"shipmentId": "<shipmentId>",
"extFulfillmentId": "PKG-55501",
"lineItems": [{ "sku": "TSHIRT-BLK-M", "quantity": 2 }],
"trackingNumber": ["1Z999AA10123456784"],
"trackingURLs": ["https://www.ups.com/track?tracknum=1Z999AA10123456784"],
"shippingCarrier": "UPS",
"shippingClass": "Ground",
"actualShipDate": "2026-10-12T15:30:00Z",
"fulfillmentType": "accepted"
}]'Required fields are shipmentId, lineItems (each with sku and quantity) and trackingNumber. You can send up to 10 fulfillments per call. The response is HTTP 202 with a status of submitted or failed for each item.
Pipe17 updates the status for you. Once a Fulfillment is accepted, Pipe17 moves the Shipping Request to partialFulfillment or, when every line item is covered, to fulfilled. Do not set these statuses yourself. Keep polling requests in partialFulfillment if your system can ship the rest later.
7.5 Inventory (into Pipe17)
Send full stock levels per SKU per location with the ingest event. Pipe17 works out the change and updates selling channels.
curl -s -X POST "$PIPE17_BASE_URL/inventory" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '[{
"event": "ingest",
"ptype": "wms",
"sku": "TSHIRT-BLK-M",
"locationId": "<Pipe17 locationId>",
"onHand": 120,
"committed": 15,
"available": 105
}]'export async function pushInventory(ctx, levels) { // [{ sku, warehouseId, onHand, committed, available }]
if (!isFlowEnabled(ctx, 'inventory.in')) return;
const rows = levels.map((l) => ({
event: 'ingest', ptype: 'wms', sku: l.sku,
locationId: state.findByExtId('locations', l.warehouseId).pipe17_id,
onHand: l.onHand, committed: l.committed, available: l.available,
}));
for (let i = 0; i < rows.length; i += 10) { // max 10 per call
const res = await p17('POST', '/inventory', { body: rows.slice(i, i + 10) });
for (const r of res.inventory ?? []) {
if (r.status === 'failed') state.enqueueRetry('pushInventory', r, new Error(r.message));
}
}
}Required fields are event, ptype, sku, locationId, onHand and committed. Send only SKUs whose levels changed since the last run, and keep the last value per SKU and location in your state store to compare.
7.6 Inbound shipments: arrivals and receipts
An arrival is Pipe17's advance notice that stock is on its way to your warehouse, for example a purchase order or a transfer (often called an ASN). A receipt is your record of what actually arrived. Your connector sends arrivals to your system and reports receipts back.
| Step | Who | Arrival status |
|---|---|---|
| Stock is shipped to your warehouse | Pipe17 (from a purchase order or transfer) | shipped |
| Your system accepts the inbound | Your connector | expected |
| Your system rejects it (unknown SKU, bad data) | Your connector | failed |
| You receive the goods and post a receipt | Your connector creates a receipt | Pipe17 tracks receiving |
Settings: arrivals.out (send arrivals to your system) and receipts.in (report receipts).
Send arrivals. Poll arrivals headed to your locations, create the inbound in your system, then mark the arrival expected with your ID in extReferenceId.
export async function handleArrival(ctx, arrival) {
if (!isFlowEnabled(ctx, 'arrivals.out') || arrival.status !== 'shipped') return;
try {
const inboundId = await mySystem.createInbound({
reference: arrival.arrivalId,
warehouse: state.findByPipe17Id('locations', arrival.toLocationId).ext_id,
expectedDate: arrival.expectedArrivalDate,
carrier: arrival.shippingCarrier, tracking: arrival.trackingNumber,
lines: arrival.lineItems.map((l) => ({ lineId: l.uniqueId, sku: l.sku, quantity: l.quantity, lot: l.lotNumber })),
});
await p17('PUT', `/arrivals/${arrival.arrivalId}`, {
body: { status: 'expected', extReferenceId: String(inboundId) },
});
} catch (err) {
if (!err.permanent) throw err; // temporary: retry later
await p17('PUT', `/arrivals/${arrival.arrivalId}`, { body: { status: 'failed' } });
await reportException('arrivals', arrival.arrivalId, err.message);
}
}# Arrivals waiting to be sent to your warehouse
curl -s -G "$PIPE17_BASE_URL/arrivals" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
--data-urlencode "toLocationId=<your Pipe17 location IDs, comma-separated>" \
--data-urlencode "status=shipped" \
--data-urlencode "updatedSince=2026-10-12T06:00:00Z" \
--data-urlencode "order=updatedAt" \
--data-urlencode "keys=*"
# Accepted by your system
curl -s -X PUT "$PIPE17_BASE_URL/arrivals/<arrivalId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "expected", "extReferenceId": "IN-30021"}'Report receipts. When receiving is complete in your system, create a receipt linked to the arrival. Post only finished receiving; if goods arrive in several loads, post one receipt per load, each with its own extReceiptId.
curl -s -X POST "$PIPE17_BASE_URL/receipts" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '[{
"arrivalId": "<arrivalId>",
"extReceiptId": "RCV-77310",
"actualArrivalDate": "2026-10-14T09:12:00Z",
"lineItems": [
{ "uniqueId": "1", "sku": "TSHIRT-BLK-M", "quantity": 48, "quantityDamaged": 2, "lotNumber": "L2026-10" }
]
}]'Required fields are arrivalId and lineItems (each with sku and quantity). The body is an array of up to 10 receipts, and the response is HTTP 202 with a status per receipt. extReceiptId must be unique in the organization, so a repeated post is rejected instead of double-counting stock; treat that as already done.
7.7 Returns
A return tells your warehouse that a customer is sending items back. Your connector sends it to your system as an RMA and reports what was received. Settings: returns.out (send returns) and returns.in (report what was received).
| Step | Who | Return status |
|---|---|---|
| The customer ships the return to your warehouse | Pipe17 (from a returns app or selling channel) | shipped |
| Your system creates the RMA | Your connector | expected |
| Your system rejects it | Your connector | failed |
| You receive the items | Your connector adds a receipt to the return | Pipe17 tracks receiving |
Send returns.
export async function handleReturn(ctx, ret) {
if (!isFlowEnabled(ctx, 'returns.out') || ret.status !== 'shipped') return;
try {
const rma = await mySystem.createRma({
reference: ret.returnId,
warehouse: state.findByPipe17Id('locations', ret.locationId).ext_id,
tracking: ret.trackingNumber,
lines: ret.lineItems.map((l) => ({ lineId: l.uniqueId, sku: l.sku, quantity: l.quantity, reason: l.cause })),
});
await p17('PUT', `/returns/${ret.returnId}`, { body: { status: 'expected', extReferenceId: String(rma) } });
} catch (err) {
if (!err.permanent) throw err;
await p17('PUT', `/returns/${ret.returnId}`, { body: { status: 'failed' } });
await reportException('returns', ret.returnId, err.message);
}
}# Returns on their way to your warehouse
curl -s -G "$PIPE17_BASE_URL/returns" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
--data-urlencode "locationId=<your Pipe17 location IDs, comma-separated>" \
--data-urlencode "status=shipped" \
--data-urlencode "updatedSince=2026-10-12T06:00:00Z" \
--data-urlencode "order=updatedAt" \
--data-urlencode "keys=*"
# RMA created in your system
curl -s -X PUT "$PIPE17_BASE_URL/returns/<returnId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "expected", "extReferenceId": "RMA-5512"}'Report what was received. Add a receipt to the return with PUT /returns/{returnId}. First read the return (keys=receipts) and skip it if a receipt with your extReceiptId is already there.
curl -s -X PUT "$PIPE17_BASE_URL/returns/<returnId>" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '{
"receipts": [{
"extReceiptId": "RMA-5512-R1",
"actualArrivalDate": "2026-10-20T11:40:00Z",
"lineItems": [
{ "uniqueId": "1", "sku": "TSHIRT-BLK-M", "quantity": 1, "quantityDamaged": 0 }
]
}]
}'Each receipt needs extReceiptId and lineItems (each with sku and quantity); quantity includes any damaged units, which you also report in quantityDamaged.
7.8 If your system is a selling channel
The same patterns work in the other direction. Create orders with POST /orders (your order number in extOrderId), and poll GET /fulfillments with updatedSince to send tracking back to your channel. Turn on the matching flows (orders.in, fulfillments.out) when you create the integration.
# Create an order (the body is an array of up to 10; extOrderId is required, and each line item needs uniqueId, sku, quantity and requiresShipping)
curl -s -X POST "$PIPE17_BASE_URL/orders" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
-H "Content-Type: application/json" \
-d '[{
"extOrderId": "SO-10045",
"lineItems": [{ "uniqueId": "SO-10045-1", "sku": "TSHIRT-BLK-M", "quantity": 2, "requiresShipping": true, "itemPrice": 19.99 }],
"shippingAddress": {
"firstName": "Alex", "lastName": "Sample", "address1": "1 Example St",
"city": "Austin", "stateOrProvince": "TX", "zipCodeOrPostalCode": "78701", "country": "USA"
}
}]'
# Pick up fulfillments for your orders
curl -s -G "$PIPE17_BASE_URL/fulfillments" \
-H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \
--data-urlencode "updatedSince=2026-10-12T06:53:00Z" \
--data-urlencode "order=updatedAt" \
--data-urlencode "keys=*"Need Help?
If you need additional assistance:
- Use Ask Pippen, our AI agent, located at the top of the app page.
- Submit a support request with as much relevant detail as possible. Learn how to submit a request.
We're here to help you succeed with your operations.
Comments
0 comments