Connect your own warehouse or prep software to ProfitDesk. Once connected, ProfitDesk treats your software like a prep center:
- ProfitDesk → you: every announcement, cancellation and update reaches your URL as a signed webhook event.
- You → ProfitDesk: you report goods received and outbound shipments through this REST API. ProfitDesk updates the seller's purchases from your reports.
Every object carries stable identifiers, so both sides can follow each unit from purchase to inbound, inbound line, outbound and finally the Amazon FBA shipment.
This guide covers a seller's own software: a private connection that the seller creates in ProfitDesk, with its own signing secret and API key.
Building software for many sellers? Warehouse software that serves many ProfitDesk sellers is registered by ProfitDesk as a prep provider, with one endpoint and one key for all of them. See
prep-providers-v1.md.
Machine-readable spec: prep-v1.openapi.yaml (OpenAPI 3.1). Interactive docs: prep-v1-swagger.html.
1. How it works
Seller buys goods ──► ProfitDesk lists them on Amazon
│
▼
inbound.announced ───────────────► your software
(signed webhook) creates an expected inbound
│
goods arrive at your warehouse
│
ProfitDesk updates received qty ◄── POST /v1/prep/inbounds/{id}/receipts
│
you ship to Amazon (FBA) or elsewhere
│
ProfitDesk stores the outbound ◄── POST /v1/prep/outbounds
trace (incl. FBA shipment id) │
▼
Amazon receives the FBA shipment ──► ProfitDesk books "forwarded" from Amazon's data- An inbound is one announcement: the goods of one order, or of several orders that arrive together. It has one line per purchase.
- A purchase counts as announced in ProfitDesk only once your endpoint has answered
2xxto itsinbound.announced. Until then the seller sees "Delivering". If the delivery fails, the purchase stays unannounced and the seller can retry the delivery or announce it again. - Outbounds are a trace. They link purchases to the shipments you send out, including the Amazon FBA shipment id, but they do not change stock or forwarded quantities. ProfitDesk books forwarded quantities from Amazon's FBA data.
2. Setup
The seller creates the connection in ProfitDesk under Settings → Integrations → Prep centers → Add → "Own software (Webhook)" and enters:
| Field | Meaning |
|---|---|
| Name | Display name of your software or warehouse, max 100 characters |
| Endpoint URL | Your HTTPS URL that receives the webhook events, max 2048 characters, without user name or password. It must resolve to public addresses only; private and internal addresses are refused. |
| Events | Which event types to receive (§5.4). Without a choice, the events marked "on" there. |
| Link template (optional) | A deep link into your software for one inbound, e.g. https://wms.example.com/inbound/{external_id}, shown to the seller next to the announced purchase. Must be an http(s) URL. Exactly two placeholders: {inbound_id} becomes the URL-encoded inb_… id, {external_id} your URL-encoded external_id. A template with {external_id} shows no link until the inbound has an external id. |
| Auto-announce FBA/FBM, transfer product name/notes | Same options as for other prep centers |
After saving, ProfitDesk shows two credentials once:
| Credential | Format | Used for |
|---|---|---|
| Signing secret | whsec_… | Verifying webhook signatures (§5.2) |
| API key | ppk_… | Calling the REST API (§4) |
On the connection's detail page the seller can change the URL, the events and the other settings, and rotate both credentials. After a secret rotation the old secret stays valid for 24 hours. A key rotation revokes the old key immediately. The detail page also has "Send test", which sends a ping event, and a delivery log where deliveries can be sent again.
The seller then assigns the connection to a warehouse. From then on, purchases in that warehouse are announced to you.
A warehouse is linked to exactly one prep center. Purchases in a warehouse linked to your connection are announced to you and to no one else.
A seller can use their own software (webhook) and PrepBusiness/PrepTool prep centers at the same time, each for different warehouses. They can also hold several webhook connections, for example one per warehouse setup. Each has its own
pcn_…id, signing secret and API key, and a key only sees its own connection.
3. Identifiers
All ids are strings with a type prefix. Store them as opaque strings.
| Prefix | Object | Stable? | Notes |
|---|---|---|---|
pur_ | Purchase | Forever | The seller's purchase. The key to trace a unit end-to-end. Stays the same if the goods are re-announced or move warehouse. |
itm_ | Item | Forever per SKU | One article per seller SKU on your connection. Carries sku, fnsku, asin, ean. |
inb_ | Inbound | Per announcement | A re-announcement creates a new inbound; the old one gets inbound.cancelled with replaced_by_inbound_id. |
inl_ | Inbound line | Per announcement | One line per purchase inside an inbound. Use it in receipts and outbounds for exact matching. |
out_ | Outbound | Forever | Our id for an outbound. You address outbounds by your own external_id. |
pcn_ | Connection | Forever | The seller's connection to your software. Every response and event names it as connection_id. |
wh_ | Warehouse | Forever | The seller's warehouse the goods are sent to |
evt_ | Event | Forever | Webhook event id, for deduplication |
Your own references. Inbounds and outbounds can carry your external_id. Every path that takes an inbound or outbound {id} also accepts ext:<external_id>.
Traces. Every inbound response and inbound event contains traces: one entry per line, with all ids known for that unit.
{
"purchase_id": "pur_4720", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_120", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": ["out_55"], "amazon_shipment_ids": ["FBA15XYZ"]
}4. Authentication, rate limits, conventions
Base URL: https://api.profitpath.app
Authentication. Send the API key as a bearer token on every request:
Authorization: Bearer ppk_3f9a…A key only sees the data of its own connection. A missing, unknown or revoked key returns 401 unauthorized.
Rate limit. 2 requests per second per key, with bursts up to 10. Beyond that the API returns 429 rate_limited with Retry-After: 1.
Format.
- Requests and responses are JSON (UTF-8).
- Timestamps are RFC 3339 (
2026-11-05T08:01:00Z). Calendar dates areYYYY-MM-DD. - Request bodies are limited to 1 MB. Unknown fields are ignored.
Pagination. List endpoints take limit (1–100, default 50) and cursor. They return next_cursor; pass it as cursor to get the next page. It is null on the last page.
Idempotency. Every POST/PUT may carry an idempotency key: either event_id in the body or the Idempotency-Key header. If both are present, the body wins. The key is required for receipts and outbounds.
| You send | Result |
|---|---|
| Same key, identical method, path and body | The stored response is replayed with "duplicate": true. Nothing changes. |
| Same key, different request | 409 idempotency_conflict |
| A request that failed (4xx/5xx) | Not stored. Fix the request and retry with the same key. |
Keys are scoped to the connection (also across key rotations), max 200 characters, and a stored response is kept for 30 days. A good key is your own event or document id plus a version, e.g. rcpt-8812-v2.
Ordering. Receipts and outbounds carry occurred_at, the time the event happened in your warehouse (default: now). A line or outbound only changes if the event is newer than the last one applied to it. Older events are reported as stale and change nothing, so retries and out-of-order delivery are safe.
5. Webhooks (ProfitDesk → you)
5.1 Delivery
ProfitDesk sends each event as POST <your endpoint URL> with a JSON body (the envelope) and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Event | The event id (evt_…), the same as the envelope's id. Use it to deduplicate. |
X-Webhook-Event-Type | e.g. inbound.announced |
X-Webhook-Timestamp | Unix seconds of this delivery attempt |
X-Webhook-Signature | sha256=<hex>. During a secret rotation it holds two values, separated by a comma. |
User-Agent | ProfitDesk-Webhooks/1 |
Envelope (here of a ping):
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a08",
"type": "ping",
"api_version": "2026-11-01",
"occurred_at": "2026-11-03T09:00:00Z",
"created_at": "2026-11-03T09:00:00Z",
"connection_id": "pcn_812",
"data": {"message": "Test event from ProfitDesk"}
}Every retry of an event sends the same envelope with the same id; only X-Webhook-Timestamp and the signature change. Your endpoint receives one request at a time.
Responding.
- Answer with any
2xxwithin 10 seconds. Do slow work asynchronously after answering. - For
inbound.announcedyou may answer with{"external_id": "<your id>"}(a string or a number, max 200 characters). ProfitDesk stores it as the inbound'sexternal_id, likePUT …/external-id, but sends noinbound.updatedfor it: you already know the value. Later events carry it. It follows the same rule asPUT: unique within your connection. If another inbound already uses the value, the delivery still counts as delivered, but the value is not stored; set it withPUT …/external-idto get the409 external_id_conflicterror explicitly. - Any other body, and the body of any other event, is ignored.
Retries.
| Your response | What ProfitDesk does |
|---|---|
2xx | Delivered |
Timeout, connection error, 408, 429, 5xx | Retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, then every 12 h, until 72 h after the event was queued; the last attempt is made at the 72 h mark |
3xx and any other 4xx (including 410) | Failed immediately, no retry. A failed announcement shows as failed to the seller. The endpoint stays enabled. |
- Redirects are not followed, so a
3xxcounts as a failed delivery. - An endpoint is disabled only after a failed attempt once it has gone 72 hours without a single successful delivery; no status code disables it on its own (a
410is a failed delivery like any other4xx). While it is disabled, waiting deliveries are skipped, new events are stored but not queued, and new announcements fail. The seller can re-enable it and send failed or skipped deliveries again from the delivery log; events created while it was off are not in the log, so catch up withGET /v1/prep/events. - A failed
inbound.announcedis not re-sent once its inbound has been cancelled; the seller re-announces the purchase instead, which creates a new inbound.
Ordering, skips and duplicates.
- Events of the same inbound are delivered in order, one at a time. An
inbound.cancellednever overtakes theinbound.announcedof the same inbound. An event whose delivery failed for good does not hold back later events. - Cancelled before you saw it. If an inbound is cancelled before you received any of its events, ProfitDesk skips all its open deliveries, the
inbound.cancelledincluded. You never hear of that inbound by webhook. A delivery that failed counts as not received. - Reactivated without you having seen it. If such an inbound becomes active again, the reactivation arrives as
inbound.announced, notinbound.restored. Treat it like any announcement. - Never sent after a cancel. An event of an inbound is never delivered after an
inbound.cancelledof that inbound that you already received (unless aninbound.restoredreached you after it), also not when it is sent again from the delivery log. It is skipped instead, so you never receive an older state on top of a cancellation. - Skipped events stay in the feed.
GET /v1/prep/eventsholds every event, delivered or not. A feed reader therefore also sees inbounds that were cancelled before delivery, and can seeinbound.announcedmore than once for the same inbound: treat it as an upsert. - Delivery is at least once. Deduplicate by
X-Webhook-Event/id. - Missed something?
GET /v1/prep/eventsreplays every event (§6.7).
5.2 Verifying the signature
Verify every request before trusting its body:
- Read the raw request body. Do not parse and re-serialize it first.
- Compute
hex(HMAC-SHA256(secret, timestamp + "." + rawBody)). The key is your signing secret exactly as shown (includingwhsec_), and the timestamp comes from theX-Webhook-Timestampheader. - The header
X-Webhook-Signatureholds one or moresha256=<hex>values separated by commas. Accept if any of them equals yours. Compare in constant time. - Reject the request if the timestamp is more than 5 minutes away from your clock.
import crypto from "node:crypto";
function verify(rawBody, headers, secret) {
const ts = headers["x-webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return headers["x-webhook-signature"].split(",").some((sig) => {
const a = Buffer.from(sig.trim()), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}import hashlib, hmac, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers["X-Webhook-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(sig.strip(), expected) for sig in headers["X-Webhook-Signature"].split(","))function verify(string $rawBody, array $headers, string $secret): bool {
$ts = $headers['X-Webhook-Timestamp'];
if (abs(time() - (int)$ts) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
foreach (explode(',', $headers['X-Webhook-Signature']) as $sig) {
if (hash_equals($expected, trim($sig))) return true;
}
return false;
}func verify(rawBody []byte, h http.Header, secret string) bool {
ts := h.Get("X-Webhook-Timestamp")
sec, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-sec)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts + "."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
for _, sig := range strings.Split(h.Get("X-Webhook-Signature"), ",") {
if hmac.Equal([]byte(strings.TrimSpace(sig)), []byte(expected)) {
return true
}
}
return false
}5.3 The inbound object
Every inbound event carries data.inbound, data.traces, data.changes, data.reason and data.replaced_by_inbound_id (the last three are null unless the event type sets them):
{
"inbound": {
"id": "inb_2000000123",
"external_id": null,
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": null, "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver", "image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": null, "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567", "batch_id": null,
"outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": null,
"reason": null,
"replaced_by_inbound_id": null
}| Field | Notes |
|---|---|
status | open, received (every line closed) or cancelled (every line cancelled). Each line has its own status as well. |
batch_id | Set when several purchases were announced together and will physically arrive together |
name, notes | Built like the names other prep centers receive. The product name and the seller's notes are included only if the seller enabled "transfer product name/notes". lines[].item.title and image_url are sent either way. |
fulfillment | FBA or FBM: where the goods go after your prep |
item.fnsku | Can be null at announcement time. It follows in an inbound.updated event once Amazon has assigned it. |
tracking_numbers | Inbound parcel tracking codes known so far. New codes follow in inbound.updated. |
5.4 Event types
| Event | When | data | Default |
|---|---|---|---|
inbound.announced | Goods are on their way to you | inbound, traces | on |
inbound.updated | Tracking numbers were added, an FNSKU became known, your external_id changed, or some lines of a shared inbound were cancelled or restored | inbound, traces, changes (see below); reason when a line was cancelled | on |
inbound.cancelled | The whole inbound is cancelled: the purchase was cancelled or deactivated, moved to another warehouse, or re-announced | inbound, traces, reason, optionally replaced_by_inbound_id | on |
inbound.restored | A cancelled inbound is active again. If you never received the inbound, the reactivation arrives as inbound.announced instead. | inbound, traces | on |
inbound.received | Echo of your receipt via the API | inbound, traces | off |
outbound.recorded | Echo of your outbound via the API | outbound, traces | off |
ping | "Send test" in ProfitDesk. Not stored and not replayable. | message | always |
The seller picks the events in the connection settings. A new connection receives the four events marked on. The two echo events are off by default: they only repeat what your software itself just reported through the API, and an integration that reacts to them naively loops. GET /v1/prep/events returns every event type, whether or not the connection is subscribed to it.
changes values on inbound.updated:
| Value | Meaning |
|---|---|
tracking_numbers | New inbound tracking codes, in inbound.tracking_numbers |
fnsku | An item got its FNSKU, in lines[].item.fnsku |
external_id | Your external_id was set or changed with PUT …/external-id. Not sent for an external_id returned in your webhook response, nor when the value is unchanged. |
lines | Some lines of a shared inbound were cancelled or restored while the inbound stays open. Check each line's status. |
reason values: purchase_cancelled, purchase_deactivated, warehouse_changed, reannounced (offer_deleted is reserved). On inbound.updated it is set when a line was cancelled and null when one was restored.
When the reason is reannounced, replaced_by_inbound_id names the new inbound that replaces this one. Move your expectation over to it.
5.5 Webhook schemas
Every key is always present. A field without a value is null, never left out; fields typed \| null below can be null.
Envelope (every event)
| Field | Type | Description |
|---|---|---|
id | string | Event id evt_<uuid>, equal to the X-Webhook-Event header. Unique; use it to deduplicate. |
type | string | inbound.announced, inbound.updated, inbound.cancelled, inbound.restored, inbound.received, outbound.recorded or ping |
api_version | string | Payload version, currently 2026-11-01 |
occurred_at | string (date-time) | When the change happened in ProfitDesk; for the echo events, the occurred_at you reported |
created_at | string (date-time) | When the event was created. Delivery may be later because of retries. |
connection_id | string | pcn_… of the seller's connection |
data | object | Depends on type; see below |
data of inbound.* events (InboundEventData)
| Field | Type | Description |
|---|---|---|
inbound | Inbound | The inbound after the change |
traces | array of Trace | One entry per line, in the same order as inbound.lines |
changes | array of string | null | null except on inbound.updated: tracking_numbers, fnsku, external_id, lines |
reason | string | null | null except on inbound.cancelled, and on inbound.updated when a line was cancelled: purchase_cancelled, purchase_deactivated, warehouse_changed, reannounced |
replaced_by_inbound_id | string | null | null except on inbound.cancelled/inbound.updated with reason reannounced: the new inb_… that takes over the goods |
Inbound
| Field | Type | Description |
|---|---|---|
id | string | inb_… |
external_id | string | null | Your reference, if set |
batch_id | string | null | Same value for purchases announced together, which arrive together |
status | string | open; received when every line is received or cancelled and at least one is received; cancelled when every line is cancelled |
name | string | <supplier> | <product> | <order numbers>. The product (first 30 characters, plus +N when the inbound has more lines) only if the seller transfers product names. Parts that are unknown are left out; if none is known, the name is the inbound id. Max 255 characters. |
notes | string | The seller's notes (if transferred, each distinct note once), plus a final line Fulfillment: FBA or Fulfillment: FBM |
fulfillment | string | FBA or FBM |
warehouse | object | null | { "id": "wh_…", "name": "…" }: the seller's warehouse the goods are sent to. Moving a purchase between two warehouses of the same connection sends no event; the next payload carries the new warehouse. A move to another connection or to a warehouse without a prep center cancels the inbound (warehouse_changed). |
order.numbers | array of string | Supplier order numbers of all purchases in the inbound |
order.supplier_host | string | null | Shop domain, e.g. amazon.de; null if unknown |
order.purchased_at | string (date) | null | Purchase date YYYY-MM-DD; null if unknown |
tracking_numbers | array of string | Inbound parcel tracking codes known so far, sorted ascending. Codes are normalised (see below). |
lines | array of InboundLine | One line per purchase |
created_at | string (date-time) | When the inbound was created |
Tracking-code normalisation. Tracking codes come from free-text fields and carrier data, and are normalised before they are sent:
- The text is split at whitespace (including non-breaking and other Unicode spaces) and at
,;:|/\. - Inside each part, every character other than
A–Z,a–zand0–9is removed. Hyphens and dots are dropped, not split on:DE-LPLV-0GLIbecomesDELPLV0GLI. - Parts without a digit are dropped. This removes carrier labels such as
DHL. - Duplicates are compared case-insensitively. The first spelling seen is kept; letter case is not changed.
Example: "DHL: 00340434161094015902, 1z999-aa1" gives ["00340434161094015902", "1z999aa1"].
InboundLine
| Field | Type | Description |
|---|---|---|
id | string | inl_…. Use it as line_id in receipts and outbounds. |
purchase_id | string | pur_…, the stable trace key |
item | Item | The article |
quantity_expected | integer | Units announced |
quantity_received | integer | Absolute count reported via receipts, 0 before the first one |
quantity_damaged | integer | Damaged units reported via receipts |
status | string | open, received or cancelled |
Item
| Field | Type | Description |
|---|---|---|
id | string | null | itm_…; one item per SKU and connection |
sku | string | The seller's Amazon SKU |
fnsku | string | null | Amazon label barcode. Can be null at announcement; then it follows with inbound.updated and changes: ["fnsku"]. |
asin | string | null | Amazon ASIN |
ean | string | null | EAN/GTIN |
title | string | null | Product title |
image_url | string | null | Product image |
Trace
| Field | Type | Description |
|---|---|---|
purchase_id | string | pur_… |
inbound_id | string | null | inb_… |
inbound_line_id | string | null | inl_… |
item_id | string | null | itm_… |
sku, fnsku, asin | string | null | As on the item |
order_number | string | null | Supplier order number of this purchase |
batch_id | string | null | As on the inbound |
outbound_ids | array of string | out_… of non-cancelled outbounds that carry units of this purchase |
amazon_shipment_ids | array of string | FBA shipment ids (FBA…) of those outbounds |
data of outbound.recorded
| Field | Type | Description |
|---|---|---|
outbound | Outbound | The outbound as stored after your request |
traces | array of Trace | One per purchase on the outbound |
Outbound
| Field | Type | Description |
|---|---|---|
id | string | out_… |
external_id | string | Your outbound id |
status | string | created, shipped or cancelled |
destination.type | string | fba, fbm or other |
destination.amazon_shipment_id | string | null | FBA shipment id |
destination.marketplace | string | null | e.g. DE |
tracking_numbers | array of string | Outbound tracking codes |
shipped_at | string (date-time) | null | Set the first time the status is shipped; kept afterwards |
lines[].purchase_id | string | pur_… the units were matched to |
lines[].inbound_line_id | string | null | inl_…, when known |
lines[].quantity | integer | Units of this purchase in the outbound |
lines[].resolved_by | string | How the line was matched: line_id, purchase_id, item_id or sku_fifo |
data of ping
| Field | Type | Description |
|---|---|---|
message | string | A fixed test message |
Your response
| Field | Type | Description |
|---|---|---|
external_id | string or number, optional | Only read on inbound.announced with a 2xx, max 200 characters. Stored as the inbound's external_id. Any other body is ignored. |
5.6 Example per event
inbound.announced: see §5.3 for data; the full envelope:
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a01",
"type": "inbound.announced",
"api_version": "2026-11-01",
"occurred_at": "2026-11-03T10:15:00Z",
"created_at": "2026-11-03T10:15:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000123",
"external_id": null,
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": null, "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": null, "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567", "batch_id": null,
"outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": null,
"reason": null,
"replaced_by_inbound_id": null
}
}Respond:
HTTP/1.1 200 OK
Content-Type: application/json
{"external_id": "WMS-IN-5521"}inbound.updated: new tracking code and FNSKU
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a02",
"type": "inbound.updated",
"api_version": "2026-11-01",
"occurred_at": "2026-11-04T07:30:00Z",
"created_at": "2026-11-04T07:30:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784", "JJD000390007754321"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": ["tracking_numbers", "fnsku"],
"reason": null,
"replaced_by_inbound_id": null
}
}inbound.updated: one line of a shared inbound was cancelled
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a03",
"type": "inbound.updated",
"api_version": "2026-11-01",
"occurred_at": "2026-11-04T12:00:00Z",
"created_at": "2026-11-04T12:00:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000131",
"external_id": "WMS-IN-5530",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10",
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver +1 | 302-1234567-1234567, 302-7654321-7654321",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {
"numbers": ["302-1234567-1234567", "302-7654321-7654321"], "supplier_host": "amazon.de",
"purchased_at": "2026-11-02"
},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_120",
"purchase_id": "pur_4720",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
},
{
"id": "inl_121",
"purchase_id": "pur_4721",
"item": {
"id": "itm_18", "sku": "CD-456", "fnsku": "X00BCDEFGH", "asin": "B0BCDEFGHI", "ean": "4059952513164",
"title": "Bosch GBA 18V 4.0Ah Battery", "image_url": "https://m.media-amazon.com/images/I/51Zk2mN8pRL.jpg"
},
"quantity_expected": 2,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "cancelled"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4720", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_120", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": [], "amazon_shipment_ids": []
},
{
"purchase_id": "pur_4721", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_121", "item_id": "itm_18",
"sku": "CD-456", "fnsku": "X00BCDEFGH", "asin": "B0BCDEFGHI", "order_number": "302-7654321-7654321",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": ["lines"],
"reason": "purchase_cancelled",
"replaced_by_inbound_id": null
}
}inbound.cancelled: re-announced as a new inbound
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a04",
"type": "inbound.cancelled",
"api_version": "2026-11-01",
"occurred_at": "2026-11-04T15:00:00Z",
"created_at": "2026-11-04T15:00:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "cancelled",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "cancelled"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": null,
"reason": "reannounced",
"replaced_by_inbound_id": "inb_2000000140"
}
}inbound.restored
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a05",
"type": "inbound.restored",
"api_version": "2026-11-01",
"occurred_at": "2026-11-04T16:00:00Z",
"created_at": "2026-11-04T16:00:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": null,
"reason": null,
"replaced_by_inbound_id": null
}
}inbound.received (echo, opt-in)
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a06",
"type": "inbound.received",
"api_version": "2026-11-01",
"occurred_at": "2026-11-05T09:30:00Z",
"created_at": "2026-11-05T09:30:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "received",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 6,
"quantity_damaged": 1,
"status": "received"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": null,
"reason": null,
"replaced_by_inbound_id": null
}
}outbound.recorded (echo, opt-in)
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a07",
"type": "outbound.recorded",
"api_version": "2026-11-01",
"occurred_at": "2026-11-06T14:00:00Z",
"created_at": "2026-11-06T14:00:01Z",
"connection_id": "pcn_812",
"data": {
"outbound": {
"id": "out_55",
"external_id": "OUT-311",
"status": "shipped",
"destination": {"type": "fba", "amazon_shipment_id": "FBA15XYZ", "marketplace": "DE"},
"tracking_numbers": ["1Z999AA10123456784"],
"shipped_at": "2026-11-06T14:00:00Z",
"lines": [
{"purchase_id": "pur_4720", "inbound_line_id": "inl_120", "quantity": 4, "resolved_by": "line_id"},
{"purchase_id": "pur_4721", "inbound_line_id": "inl_121", "quantity": 2, "resolved_by": "sku_fifo"}
]
},
"traces": [
{
"purchase_id": "pur_4720", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_120", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": ["out_55"],
"amazon_shipment_ids": ["FBA15XYZ"]
},
{
"purchase_id": "pur_4721", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_121", "item_id": "itm_18",
"sku": "CD-456", "fnsku": "X00BCDEFGH", "asin": "B0BCDEFGHI", "order_number": "302-7654321-7654321",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": ["out_55"],
"amazon_shipment_ids": ["FBA15XYZ"]
}
]
}
}ping: see §5.1.
What your response means
| Your response | Result |
|---|---|
2xx (any body) | Delivered. On inbound.announced, an optional {"external_id": "…"} is stored. |
3xx, other 4xx (except 408, 429; 410 included) | Failed at once, no retry; the endpoint stays enabled |
5xx, 408, 429, timeout (10 s), connection error | Retried for up to 72 h (§5.1) |
6. REST API (you → ProfitDesk)
| Method | Path | Purpose |
|---|---|---|
GET | /v1/prep/inbounds | List inbounds |
GET | /v1/prep/inbounds/{id} | Get one inbound |
PUT | /v1/prep/inbounds/{id}/external-id | Set your reference |
POST | /v1/prep/inbounds/{id}/receipts | Report goods received |
POST | /v1/prep/outbounds | Report an outbound (create, update, ship, cancel) |
GET | /v1/prep/outbounds/{id} | Get one outbound |
GET | /v1/prep/items | List items (one per SKU) |
GET | /v1/prep/events | Replay webhook events |
GET | /v1/prep/connections | The key's connection |
Every inbound, receipt, outbound and item response carries connection_id.
6.1 List inbounds
GET /v1/prep/inbounds?status=open&updated_since=2026-11-01T00:00:00Z&limit=50&cursor=
| Query | Meaning |
|---|---|
status | open, received or cancelled |
updated_since | Only inbounds changed after this instant, including tracking and FNSKU updates |
limit, cursor | Pagination |
{
"data": [
{
"connection_id": "pcn_812",
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
]
}
],
"next_cursor": "inb_2000000123"
}Inbounds are ordered by id. Use the list as a safety net: for example, a nightly sync of all open inbounds.
The list contains inbounds that still have lines in ProfitDesk. An inbound whose lines were all removed is not listed: this happens when the purchase moved to another warehouse (warehouse_changed) or was re-announced as a new inbound (reannounced). Find these through GET /v1/prep/events (their inbound.cancelled event). GET /v1/prep/inbounds/{id} still returns them.
6.2 Get one inbound
GET /v1/prep/inbounds/inb_2000000123 or GET /v1/prep/inbounds/ext:WMS-IN-5521
{
"connection_id": "pcn_812",
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver", "image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
]
}For an inbound whose lines were removed (moved or re-announced, see §6.1), the response is the inbound as of its latest event, with status: "cancelled" and every line cancelled. 404 inbound_not_found means the id was never announced to this connection.
6.3 Set your reference
PUT /v1/prep/inbounds/inb_2000000123/external-id
{ "external_id": "WMS-IN-5521" }- The value is unique per connection (
409 external_id_conflictotherwise), 1–200 printable characters. - Afterwards the inbound is addressable as
ext:WMS-IN-5521. - A changed value sends
inbound.updatedwithchanges: ["external_id"]. - An idempotency key is optional here;
event_idisnullin the response without one. - A cancelled inbound answers
409 inbound_cancelled. - Alternatively, return
{"external_id": "…"}in your response toinbound.announced.
Response 200:
{
"event_id": null,
"duplicate": false,
"connection_id": "pcn_812",
"inbound": {
"id": "inb_2000000123",
"external_id": "WMS-IN-5521",
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver", "image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
]
}6.4 Report goods received
POST /v1/prep/inbounds/{id}/receipts
{
"event_id": "rcpt-8812",
"occurred_at": "2026-11-05T08:01:00Z",
"status": "receiving",
"lines": [
{"line_id": "inl_120", "quantity_received": 5, "quantity_damaged": 1},
{"sku": "CD-456", "quantity_received": 2}
]
}| Field | Meaning |
|---|---|
event_id | Idempotency key (required, or send the Idempotency-Key header) |
occurred_at | When you counted (default: now) |
status | Required. receiving: count so far, more may follow. A line whose count reaches its expected quantity closes; a later receiving count below it opens the line again. received: the inbound is complete; every line is closed, including lines counted short or not counted. |
lines[].line_id | inl_…, exact match (preferred) |
lines[].sku | Alternative to line_id, matched within this inbound. If several lines share the SKU, they are filled in order up to their expected quantity, and any surplus goes to the last line. |
lines[].quantity_received | Required. The absolute count for the line, not a delta. To correct a count downwards, send the smaller number. |
lines[].quantity_damaged | Optional, stored and returned. If omitted, the previous value is kept. |
lines may be empty only together with status: "received", to close an inbound. At most 1000 lines; each line may be addressed only once per request.
Response 200:
{
"event_id": "rcpt-8812",
"duplicate": false,
"connection_id": "pcn_812",
"inbound": {
"id": "inb_2000000131",
"external_id": "WMS-IN-5530",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10",
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver +1 | 302-1234567-1234567, 302-7654321-7654321",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {
"numbers": ["302-1234567-1234567", "302-7654321-7654321"], "supplier_host": "amazon.de",
"purchased_at": "2026-11-02"
},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_120",
"purchase_id": "pur_4720",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver", "image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 5,
"quantity_damaged": 1,
"status": "open"
},
{
"id": "inl_121",
"purchase_id": "pur_4721",
"item": {
"id": "itm_18", "sku": "CD-456", "fnsku": "X00BCDEFGH", "asin": "B0BCDEFGHI", "ean": "4059952513164",
"title": "Bosch GBA 18V 4.0Ah Battery", "image_url": "https://m.media-amazon.com/images/I/51Zk2mN8pRL.jpg"
},
"quantity_expected": 2,
"quantity_received": 2,
"quantity_damaged": 0,
"status": "received"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4720", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_120", "item_id": "itm_17",
"sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "order_number": "302-1234567-1234567",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": [], "amazon_shipment_ids": []
},
{
"purchase_id": "pur_4721", "inbound_id": "inb_2000000131", "inbound_line_id": "inl_121", "item_id": "itm_18",
"sku": "CD-456", "fnsku": "X00BCDEFGH", "asin": "B0BCDEFGHI", "order_number": "302-7654321-7654321",
"batch_id": "0d1f6a2e-4b7c-4c11-9a3e-5f2b8c7d9e10", "outbound_ids": [], "amazon_shipment_ids": []
}
],
"lines": [{"line_id": "inl_120", "result": "applied"}, {"line_id": "inl_121", "result": "applied"}]
}lines[].result is one of:
applied: changedunchanged: same values as beforestale: a receipt with the same or a lateroccurred_athad already been applied to the line
A receipt that changed anything sends inbound.received.
Only announced goods are accepted:
| Situation | Response |
|---|---|
| Inbound never announced to this connection | 404 inbound_not_found |
line_id or SKU not part of this inbound | 422 line_not_found, details lists index, line_id and sku of each unknown line |
| Inbound (or the addressed line) is cancelled, including an inbound whose lines were removed because the purchase moved or was re-announced | 409 inbound_cancelled |
If goods arrive that were never announced, ask the seller to announce them in ProfitDesk. Do not book them on a different inbound.
6.5 Report an outbound
POST /v1/prep/outbounds
{
"event_id": "out-311-v3",
"external_id": "OUT-311",
"occurred_at": "2026-11-06T14:00:00Z",
"status": "shipped",
"destination": {"type": "fba", "amazon_shipment_id": "FBA15XYZ", "marketplace": "DE"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [{"line_id": "inl_120", "quantity": 4}, {"sku": "CD-456", "quantity": 2}]
}| Field | Meaning |
|---|---|
event_id | Idempotency key (required, or send the Idempotency-Key header) |
external_id | Your outbound id, 1–200 printable characters. The request is an upsert on this value: send the same outbound again whenever it changes. |
occurred_at | When it happened (default: now) |
status | created, shipped or cancelled (cancelled clears the lines) |
destination.type | Required: fba, fbm or other. Missing or empty is refused with 400 invalid_request. |
destination.amazon_shipment_id | The Amazon FBA shipment id (FBA…), max 64 characters. Please always send it for FBA: it links your outbound to Amazon's shipment. |
destination.marketplace | e.g. DE, max 8 characters |
tracking_numbers | Up to 50 codes, 1–200 characters each |
lines | The complete list of lines, at most 1000. It replaces what was stored before. |
Each line names its units with one of these references, tried in this order:
| Reference | Matching |
|---|---|
line_id (inl_…) | Exact. Recommended. |
purchase_id (pur_…) | Exact. The purchase must belong to an inbound of this connection. |
item_id (itm_…) or sku | First received, first out, across the connection's purchases of that item. Capped at what was received minus what other open or shipped outbounds already hold. |
Response 200:
{
"event_id": "out-311-v3",
"duplicate": false,
"stale": false,
"connection_id": "pcn_812",
"outbound": {
"id": "out_55",
"external_id": "OUT-311",
"status": "shipped",
"destination": {"type": "fba", "amazon_shipment_id": "FBA15XYZ", "marketplace": "DE"},
"tracking_numbers": ["1Z999AA10123456784"],
"shipped_at": "2026-11-06T14:00:00Z",
"lines": [
{"purchase_id": "pur_4720", "inbound_line_id": "inl_120", "quantity": 4, "resolved_by": "line_id"},
{"purchase_id": "pur_4721", "inbound_line_id": "inl_121", "quantity": 2, "resolved_by": "sku_fifo"}
]
},
"unresolved": []
}- Quantities that cannot be matched are listed in
unresolved, each withindex(its position inlines), the reference you sent (the othersnull), the part ofquantitythat could not be matched, and areason:line_not_found,purchase_not_found,item_not_found,sku_not_found,insufficient_received_quantityorinvalid_reference. - The request is refused with
422 line_not_foundonly if nothing matches;detailsthen holds theunresolvedentries. stale: truemeans a newer version of this outbound had already been applied, and nothing changed.- Every request that is not stale sends
outbound.recorded.
A partly matched outbound (4 units of AB-123 requested, 2 still available):
{
"event_id": "out-312-v1",
"duplicate": false,
"stale": false,
"connection_id": "pcn_812",
"outbound": {
"id": "out_56",
"external_id": "OUT-312",
"status": "created",
"destination": {"type": "fba", "amazon_shipment_id": "FBA15XZA", "marketplace": "DE"},
"tracking_numbers": [],
"shipped_at": null,
"lines": [{"purchase_id": "pur_4720", "inbound_line_id": "inl_120", "quantity": 2, "resolved_by": "sku_fifo"}]
},
"unresolved": [
{
"index": 0, "line_id": null, "purchase_id": null, "item_id": null, "sku": "AB-123", "quantity": 2,
"reason": "insufficient_received_quantity"
}
]
}6.6 Get one outbound
GET /v1/prep/outbounds/out_55 or GET /v1/prep/outbounds/ext:OUT-311
{
"connection_id": "pcn_812",
"outbound": {
"id": "out_55",
"external_id": "OUT-311",
"status": "shipped",
"destination": {"type": "fba", "amazon_shipment_id": "FBA15XYZ", "marketplace": "DE"},
"tracking_numbers": ["1Z999AA10123456784"],
"shipped_at": "2026-11-06T14:00:00Z",
"lines": [
{"purchase_id": "pur_4720", "inbound_line_id": "inl_120", "quantity": 4, "resolved_by": "line_id"},
{"purchase_id": "pur_4721", "inbound_line_id": "inl_121", "quantity": 2, "resolved_by": "sku_fifo"}
]
}
}6.7 Replay events
GET /v1/prep/events?since=evt_0193a1b2-…&types=inbound.announced,inbound.cancelled&limit=100
Returns the same envelopes that are sent to your URL, in feed order (see below). This includes every event type, whether or not the connection is subscribed to it, and events whose delivery failed or was skipped. Events are kept for 90 days.
| Query | Meaning |
|---|---|
since | An event id (exclusive, recommended) or an RFC 3339 instant. An unknown event id answers 400 invalid_request. |
types | Comma-separated event types (not ping, which is never stored) |
limit | 1–100, default 50 |
{
"data": [
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a01",
"type": "inbound.announced",
"api_version": "2026-11-01",
"occurred_at": "2026-11-03T10:15:00Z",
"created_at": "2026-11-03T10:15:01Z",
"connection_id": "pcn_812",
"data": {
"inbound": {
"id": "inb_2000000123",
"external_id": null,
"batch_id": null,
"status": "open",
"name": "amazon.de | Bosch GSR 18V-55 Drill Driver | 302-1234567-1234567",
"notes": "Fragile, please bubble-wrap\nFulfillment: FBA",
"fulfillment": "FBA",
"warehouse": {"id": "wh_12", "name": "Warehouse South"},
"order": {"numbers": ["302-1234567-1234567"], "supplier_host": "amazon.de", "purchased_at": "2026-11-02"},
"tracking_numbers": ["1Z999AA10123456784"],
"lines": [
{
"id": "inl_99",
"purchase_id": "pur_4711",
"item": {
"id": "itm_17", "sku": "AB-123", "fnsku": null, "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver",
"image_url": "https://m.media-amazon.com/images/I/61Xf3kqcQhL.jpg"
},
"quantity_expected": 6,
"quantity_received": 0,
"quantity_damaged": 0,
"status": "open"
}
],
"created_at": "2026-11-03T10:15:00Z"
},
"traces": [
{
"purchase_id": "pur_4711", "inbound_id": "inb_2000000123", "inbound_line_id": "inl_99",
"item_id": "itm_17", "sku": "AB-123", "fnsku": null, "asin": "B0ABCDEFGH",
"order_number": "302-1234567-1234567", "batch_id": null, "outbound_ids": [], "amazon_shipment_ids": []
}
],
"changes": null,
"reason": null,
"replaced_by_inbound_id": null
}
}
],
"next_cursor": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a01",
"has_more": true
}Feed order. Events are ordered by the database transaction that created them, not strictly by created_at, and an event only shows up once every transaction that started before it has finished (usually within a second; at most ten minutes). A cursor therefore never skips an event: nothing is ever inserted behind a next_cursor you already have.
Resume with the evt_… id: pass the last next_cursor (or the id of the last event you processed) as since. That position is exact. has_more tells you whether to fetch again right away.
An instant is a rough starting point. It is compared with the time the event was stored, which can differ from the envelope's created_at by a few milliseconds. To make sure the event at that instant is included, the window starts one second early. Results for an instant can therefore repeat events you already have. Always deduplicate by id.
You can use this instead of webhooks, as a pull-only integration: poll every minute with since set to the last next_cursor you processed, and deduplicate by id.
6.8 List items
GET /v1/prep/items?sku=AB-123&limit=50&cursor=
{
"data": [
{
"id": "itm_17", "sku": "AB-123", "fnsku": "X00ABCDEFG", "asin": "B0ABCDEFGH", "ean": "4006381333931",
"title": "Bosch GSR 18V-55 Drill Driver", "image_url": null, "connection_id": "pcn_812",
"created_at": "2026-11-03T10:15:00Z", "updated_at": "2026-11-03T10:15:00Z"
},
{
"id": "itm_18", "sku": "CD-456", "fnsku": "X00BCDEFGH", "asin": "B0BCDEFGHI", "ean": "4059952513164",
"title": "Bosch GBA 18V 4.0Ah Battery", "image_url": null, "connection_id": "pcn_812",
"created_at": "2026-11-03T10:15:00Z", "updated_at": "2026-11-04T07:30:00Z"
}
],
"next_cursor": null
}sku filters by exact SKU. image_url is not filled in this list; the inbound payloads carry it.
6.9 The key's connection
GET /v1/prep/connections returns the key's own connection, in the list shape the endpoint shares with prep providers (prep-providers-v1.md):
{
"data": [
{
"id": "pcn_812",
"status": "active",
"seller": {"name": "Sample Trading", "customer_reference": null, "seller_id": "A1B2C3D4E5F6G7"},
"created_at": "2026-11-01T09:00:00Z"
}
],
"next_cursor": null
}seller.name is the seller's workspace name (null if none is set), seller.seller_id the Amazon selling partner id of the account owner (null without an Amazon connection), and seller.customer_reference is always null here.
7. Errors
Every non-2xx response has this body:
{
"error": {
"code": "line_not_found",
"message": "lines not announced in this inbound",
"details": [{"index": 1, "line_id": "", "sku": "EF-789"}]
}
}| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Invalid input; the message names the field |
| 400 | invalid_json | Empty body or malformed JSON |
| 400 | idempotency_key_required | Receipt or outbound sent without event_id / Idempotency-Key |
| 401 | unauthorized | Missing, unknown or revoked API key |
| 404 | inbound_not_found, outbound_not_found | No such object on this connection (for inbounds: never announced to it) |
| 409 | inbound_cancelled | The inbound or line is cancelled, or its lines were removed because the purchase moved or was re-announced |
| 409 | idempotency_conflict | The key was already used for a different request |
| 409 | external_id_conflict | Another inbound already uses this external_id |
| 413 | payload_too_large | Body over 1 MB |
| 422 | line_not_found | The line, SKU or reference is not part of the inbound, or nothing matched |
| 429 | rate_limited | Slow down. Retry after Retry-After seconds. |
| 500 | internal_error | Retry with the same idempotency key |
details is an array, empty unless the code fills it: line_not_found (receipts: index, line_id, sku of each unknown line; outbounds: the unresolved entries) and inbound_cancelled for one line (line_id, sku).
8. Integration checklist
- Receive webhooks:
- an HTTPS endpoint that verifies the signature (§5.2)
- answers
2xxfast - deduplicates by
X-Webhook-Event
- On
inbound.announced, create an expected inbound in your software, keyed byinbound.id, with one expected line perlines[].id. Return your id as{"external_id": "…"}if you have one. Treat an announcement of an inbound you already know as an update. - On
inbound.updated, refresh tracking numbers and FNSKUs (label printing needs the FNSKU). Withchanges: ["lines"], refresh each line's status. - On
inbound.cancelled, stop expecting the goods. Ifreplaced_by_inbound_idis set, the goods now belong to that inbound. - When goods arrive,
POST …/receiptswith absolute counts perline_id. Sendstatus: "received"when the inbound is complete. - When you ship,
POST /v1/prep/outboundswith yourexternal_id, the lines byline_id, and for FBA theamazon_shipment_id. Send it again when it changes (created→shipped, orcancelled). - Safety net: poll
GET /v1/prep/eventsorGET /v1/prep/inbounds?status=openonce in a while. - Test with "Send test" in ProfitDesk, then with a real purchase.
9. FAQ
Why is the FNSKU sometimes empty?
Goods are announced as soon as the seller's Amazon listing is created. Amazon assigns the FNSKU a little later, and ProfitDesk then sends inbound.updated with changes: ["fnsku"].
What happens if my endpoint is down?
ProfitDesk retries for up to 72 hours (§5.1), and the seller sees the announcement as "Delivering". After that the announcement is marked failed, and the seller can send it again from the delivery log. You can also always catch up with GET /v1/prep/events.
Can I correct a receipt?
Yes. Send the corrected absolute count with a newer occurred_at and a new idempotency key.
Several purchases in one parcel?
They arrive as one inbound with one line per purchase and share a batch_id. Count per line. If you only know the SKU, send sku: lines with the same SKU are filled in order.
Do outbounds change the seller's stock?
No. Outbounds are a trace. "Forwarded" quantities for FBA come from Amazon's own shipment data. Send amazon_shipment_id so the two can be linked.
Is there a sandbox?
Use a test warehouse and a test purchase in the seller's own account, and "Send test" for connectivity. Events can always be replayed via GET /v1/prep/events.
Versioning.
The envelope carries api_version (currently 2026-11-01). New optional fields and new event types can appear at any time, so ignore what you do not know. Breaking changes come with a new api_version.