Connect your warehouse or prep software to ProfitDesk once and serve every ProfitDesk seller who works with you. ProfitDesk lists you in the sellers' prep-center picker. Each seller who picks you becomes one connection, and one endpoint, one signing secret and one API key cover all of them:
- ProfitDesk → you: every new seller, 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.
Building for a single seller's own software? That is a private connection the seller sets up in ProfitDesk themselves, with its own key per connection. See
prep-v1.md.
Machine-readable spec: prep-providers-v1.openapi.yaml (OpenAPI 3.1). Interactive docs: prep-providers-v1-swagger.html.
1. How it works
ProfitDesk registers you ──► you appear in every seller's prep-center list
│
A seller picks you ──► connection.created ──────────► your software
(signed webhook) links pcn_… to your customer
│
The seller buys goods ──► inbound.announced ───────────► creates an expected inbound
for a warehouse linked to you │
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- A connection (
pcn_…) is one seller working with you. Every event and every API response names it asconnection_id. - An inbound is one announcement: the goods of one order, or of several orders that arrive together. It has one line per purchase. An inbound always belongs to exactly one connection.
- 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 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. Getting access
There is no self-service sign-up and no provider dashboard. ProfitDesk registers you, and every later change goes through the same contact: support@profitpath.com.
2.1 What you send us
| Item | Notes |
|---|---|
| Company name | The name sellers see in the prep-center list, max 100 characters |
| Country and address | Country (ISO code or name) is required. Address line(s), postal code and city are shown to sellers. |
| Webhook endpoint URL | The one URL that receives the events of all your sellers. Requirements below. |
| Event types (optional) | Which events you want (§6.2). Without a choice you get the defaults. |
| 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. |
| Expected volume (optional) | If you need more than the default rate limit of 20 requests per second (§5) |
Endpoint requirements:
https://only, without user name or password in the URL, at most 2048 characters.- The host must resolve to public addresses only. Private, loopback, link-local, multicast, CGNAT (
100.64.0.0/10) and other non-public ranges are refused when the URL is registered, and the resolved address is checked again on every connection, so a DNS change cannot point deliveries into a private network. - It accepts
POSTwith a JSON body and answers2xxwithin 10 seconds (connect 5 s, TLS handshake 5 s, the whole request 10 s). - Redirects are not followed. A
3xxanswer counts as a failed delivery and is not retried.
2.2 What you get
ProfitDesk reviews the request and registers you as a prep center. You then receive, once and over a secure channel:
| Credential | Format | Used for |
|---|---|---|
| Signing secret | whsec_ + 64 hex characters | Verifying webhook signatures (§6.5) |
| Provider API key | ppk_ + 48 hex characters | Calling the REST API for all your connections (§5) |
Neither is shown again. If you lose one, ask for a rotation.
From then on you appear in the prep-center list of every ProfitDesk seller. Nothing is sent until the first seller connects: your first event is a connection.created. Ask us for a test connection from a test seller account to try the whole flow before your first real seller connects.
2.3 Changes after go-live
All of these go through support@profitpath.com:
| Change | Effect |
|---|---|
| Webhook URL | Used from the next delivery attempt on, including retries already waiting |
| Link template | Changed or removed on request; the placeholders are {inbound_id} and {external_id} (§2.1) |
| Event types | Applies to events created afterwards. Deliveries already queued are still sent. |
| Signing secret rotation | The new secret signs at once. The old one keeps signing as well for 24 hours, so X-Webhook-Signature carries two values during that time. |
| API key rotation | The old key is revoked at once; API servers that cached it accept it for up to one more minute. |
| Rate limit | Set together with a key rotation, so it comes with a new key |
| Deactivation | You leave the prep-center list, so no new seller can pick you. Deliveries stop and your sellers see you as deactivated; their announcements to you fail. Your API key keeps working, so receipts and outbounds for goods already announced still arrive. |
| Re-activation | Deliveries start again and the endpoint's failure counter is reset. Deliveries skipped while you were off are not re-sent (§6.3). |
3. Connections
3.1 Lifecycle
- A seller picks you in ProfitDesk's prep-center list. They may enter the name you see them under (default: their ProfitDesk workspace name) and their customer number at your company. You receive
connection.created. A seller can connect to you only once at a time. - The seller links warehouses to the connection. Purchases in those warehouses are announced to you, depending on the seller's settings: automatic announcement for FBA and FBM purchases, and whether product names and notes are transferred.
- The seller changes the name you see or their customer number. You receive
connection.updatedwithchanges. - The seller removes the connection, which ProfitDesk only allows once no warehouse is linked to it any more. You receive
connection.removed, the last event of that connection.
You cannot create, change or end a connection yourself. The seller never sees your URL, secret or key.
3.2 What you learn about the seller
The connection object comes with every connection.* event and from GET /v1/prep/connections:
| Field | Meaning |
|---|---|
id | pcn_…, the connection |
status | active; removed only on connection.removed |
seller.name | The name the seller entered for you (control characters removed, cut to 50 characters), else their ProfitDesk workspace name; null if neither is set. If the seller entered no name for you, a rename of their workspace changes this value without a connection.updated; GET /v1/prep/connections always shows the current value. |
seller.customer_reference | Their customer number at your company exactly as they entered it (max 100 characters), null if none |
seller.seller_id | The Amazon selling partner id of the workspace owner's most recently updated Amazon account, null if no Amazon account is connected. It can change without an event. |
created_at | When the seller connected |
Match sellers with care. ProfitDesk does not check customer_reference against your records: any seller can enter any value. Treat it as a hint, and confirm with your customer before you link the connection to an account in your software.
3.3 One endpoint, many sellers
- Every event of every connection goes to your one endpoint; the envelope's
connection_idsays whose it is. - Your API key sees the data of all your active connections and nothing else.
- Every inbound, receipt, outbound and item response names the
connection_idof its object. Lists and the event feed span all connections; narrow them with?connection=pcn_…. - Every id (
inb_…,out_…,ext:…) is looked up across your active connections. Ids are unique across ProfitDesk, so you never see the sameinb_…on two connections. - Your external ids and idempotency keys share one namespace across all your connections (§5).
3.4 After a removal
- The connection's inbounds, outbounds and items are gone from the API: their ids answer
404, and receipts and outbounds for them are refused.?connection=no longer accepts it (404 connection_not_found). - Inbounds still open at that moment do not get an
inbound.cancelledeach.connection.removedvoids all of them: stop expecting their goods. - Events that were already queued for delivery are still delivered, before
connection.removed. - The connection's events stay in your event feed for the retention period (90 days), ending with
connection.removed. - If the seller connects again later, that is a new connection with a new
pcn_…id and a newconnection.created.
4. Identifiers
All ids are strings with a type prefix. Store them as opaque strings.
| Prefix | Object | Stable? | Notes |
|---|---|---|---|
pcn_ | Connection | Forever | One seller's connection to you. Every response and event names it as connection_id. |
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 and connection. Carries sku, fnsku, asin, ean. Two sellers with the same SKU are two items. |
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. |
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"]
}5. Authentication, rate limits, conventions
Base URL: https://api.profitpath.app
Authentication. Send your provider API key as a bearer token on every request:
Authorization: Bearer ppk_3f9a…A missing key returns 401 unauthorized ("missing API key"); an unknown or revoked key returns 401 unauthorized ("invalid or revoked API key").
Rate limit. Per API key, shared by all your connections: by default 20 requests per second, with bursts up to 100 (ProfitDesk may set a different limit for you). Beyond that the API returns 429 rate_limited with Retry-After: 1; the message names your limit.
Which connection?
- Every
{id}(inb_…,out_…,ext:…) is looked up across your active connections. An id of another provider, of a seller's private connection or of a removed connection answers404. GET /v1/prep/inbounds,GET /v1/prep/itemsandGET /v1/prep/eventstakeconnection=pcn_…to narrow them to one connection. A value that is not one of your active connections answers404 connection_not_found.- A receipt belongs to the connection of its inbound. An outbound belongs to exactly one connection (§7.6).
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 max 200 characters and are scoped to you as a provider: one namespace across all your connections, also across key rotations. 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.
External ids (inbound and outbound external_id) are unique across all your active connections as well.
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.
6. Webhooks (ProfitDesk → you)
6.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 connection.created):
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a09",
"type": "connection.created",
"api_version": "2026-11-01",
"occurred_at": "2026-11-01T09:00:00Z",
"created_at": "2026-11-01T09:00:01Z",
"connection_id": "pcn_812",
"data": {
"connection": {
"id": "pcn_812",
"status": "active",
"seller": {"name": "Sample Trading", "customer_reference": "K-1001", "seller_id": "A1B2C3D4E5F6G7"},
"created_at": "2026-11-01T09:00:00Z"
},
"changes": null
}
}Every retry of an event sends the same envelope with the same id; only X-Webhook-Timestamp and the signature change. Your endpoint can receive several requests at the same time (for different inbounds and connections, currently up to five).
6.2 Event types and subscription
| Event | When | data | Default |
|---|---|---|---|
connection.created | A seller connected to you | connection, changes (null) | on |
connection.updated | The seller changed the name you see or their customer number | connection, changes | on |
connection.removed | The seller removed the connection. Nothing of it is reachable afterwards (§3.4). | connection, changes (null) | on |
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 (§6.4). | inbound, traces | on |
inbound.received | Echo of your own receipt via the API | inbound, traces | off |
outbound.recorded | Echo of your own outbound via the API | outbound, traces | off |
Your subscription is set by ProfitDesk. A new provider receives the seven events marked on. The two echo events are available on request and off by default: they only repeat what you yourself just reported through the API, and an integration that reacts to them naively loops. Ask support@profitpath.com to change the selection. There is no test event (ping) for providers: test with a test connection.
GET /v1/prep/events returns every event type, whether or not you are 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.
changes values on connection.updated: seller_name, customer_reference (one or both, in this order).
6.3 Responding and retries
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 across all your sellers' connections. If another of your inbounds 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 inbound.announced shows as failed to the seller. Your endpoint stays enabled. |
Your 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 410 is a failed delivery like any other 4xx). It is then disabled for all your sellers at once:
- Deliveries waiting to be sent are skipped. They are not re-sent when the endpoint is enabled again, and you cannot retry them yourself.
- Events created while the endpoint is off are stored, but not queued for delivery.
- New announcements to you fail for your sellers with "<your name> is not receiving announcements: its webhook endpoint was disabled (failing)". Their purchases stay unannounced until the seller announces them again.
- Your API key keeps working.
Ask support@profitpath.com to enable the endpoint again, then catch up with GET /v1/prep/events from the last event you processed.
6.4 Ordering, skips and duplicates
Order.
- Events of the same inbound are delivered in order, one at a time. An
inbound.cancellednever overtakes theinbound.announcedof the same inbound. - Per connection,
connection.*events never overtake and are never overtaken by the connection's other events:connection.createdarrives before anything else of that connection, andconnection.removedafter everything else. - Apart from that, events of different inbounds and different connections may arrive in parallel and in any order.
- 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.cancelled included. 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, not inbound.restored. Treat it like any announcement.
Never sent after a cancel. An event of an inbound is never delivered after an inbound.cancelled of that inbound that you already received (unless an inbound.restored reached you after it). 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/events holds every event, delivered or not. A feed reader therefore also sees inbounds that were cancelled before delivery, and can see inbound.announced more than once for the same inbound: treat it as an upsert.
Duplicates. Delivery is at least once. Deduplicate by X-Webhook-Event / id.
Missed something? GET /v1/prep/events replays every event (§7.8).
6.5 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 you received it (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
}6.6 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. |
6.7 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 | connection.created, connection.updated, connection.removed, inbound.announced, inbound.updated, inbound.cancelled, inbound.restored, inbound.received or outbound.recorded |
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. Stays set after the connection was removed. |
data | object | Depends on type; see below |
Connection events
data of connection.created, connection.updated and connection.removed:
| Field | Type | Description |
|---|---|---|
connection | Connection | The connection after the change; on connection.removed as it was, with status: "removed" |
changes | array of string | null | null except on connection.updated: seller_name and/or customer_reference |
Connection
| Field | Type | Description |
|---|---|---|
id | string | pcn_… |
status | string | active, or removed on connection.removed |
seller.name | string | null | The name the seller chose for you to see (max 50 characters), else their workspace name in ProfitDesk; null if neither is set |
seller.customer_reference | string | null | The seller's customer number at your company, as they entered it (max 100 characters). Not verified by ProfitDesk. |
seller.seller_id | string | null | The Amazon selling partner id of the account owner, null if no Amazon account is connected |
created_at | string (date-time) | When the seller connected |
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 |
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. |
6.8 Example per event
connection.created: see §6.1.
connection.updated: the seller changed their customer number
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a10",
"type": "connection.updated",
"api_version": "2026-11-01",
"occurred_at": "2026-11-20T09:00:00Z",
"created_at": "2026-11-20T09:00:01Z",
"connection_id": "pcn_812",
"data": {
"connection": {
"id": "pcn_812",
"status": "active",
"seller": {"name": "Sample Trading", "customer_reference": "K-1001", "seller_id": "A1B2C3D4E5F6G7"},
"created_at": "2026-11-01T09:00:00Z"
},
"changes": ["customer_reference"]
}
}connection.removed
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a11",
"type": "connection.removed",
"api_version": "2026-11-01",
"occurred_at": "2026-12-01T09:00:00Z",
"created_at": "2026-12-01T09:00:01Z",
"connection_id": "pcn_812",
"data": {
"connection": {
"id": "pcn_812",
"status": "removed",
"seller": {"name": "Sample Trading", "customer_reference": "K-1001", "seller_id": "A1B2C3D4E5F6G7"},
"created_at": "2026-11-01T09:00:00Z"
},
"changes": null
}
}inbound.announced
{
"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, on request)
{
"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, on request)
{
"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"]
}
]
}
}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; your endpoint stays enabled |
5xx, 408, 429, timeout (10 s), connection error | Retried for up to 72 h (§6.3) |
7. REST API (you → ProfitDesk)
| Method | Path | Purpose |
|---|---|---|
GET | /v1/prep/connections | List your active connections |
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/events | Replay webhook events |
GET | /v1/prep/items | List items (one per SKU and connection) |
Every inbound, receipt, outbound and item response carries connection_id.
7.1 List connections
GET /v1/prep/connections?limit=50&cursor=
Your active connections, ordered by id (oldest first).
{
"data": [
{
"id": "pcn_812",
"status": "active",
"seller": {"name": "Sample Trading", "customer_reference": "K-1001", "seller_id": "A1B2C3D4E5F6G7"},
"created_at": "2026-11-01T09:00:00Z"
},
{
"id": "pcn_905",
"status": "active",
"seller": {"name": "Nordlicht Handel", "customer_reference": null, "seller_id": null},
"created_at": "2026-11-08T14:30:00Z"
}
],
"next_cursor": null
}Removed connections are not listed; you learn about them through connection.removed. Use this list to reconcile your customer mapping, for example nightly.
7.2 List inbounds
GET /v1/prep/inbounds?connection=pcn_812&status=open&updated_since=2026-11-01T00:00:00Z&limit=50&cursor=
| Query | Meaning |
|---|---|
connection | pcn_…: only this connection's inbounds. Without it, all your connections. |
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.
7.3 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 §7.2), 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 one of your active connections.
7.4 Set your reference
PUT /v1/prep/inbounds/inb_2000000123/external-id
{ "external_id": "WMS-IN-5521" }- The value is unique across all your active connections (
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": []
}
]
}7.5 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 one of your active connections | 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.
7.6 Report an outbound
POST /v1/prep/outbounds
{
"event_id": "out-311-v3",
"external_id": "OUT-311",
"connection_id": "pcn_812",
"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, unique across all your connections. The request is an upsert on this value: send the same outbound again whenever it changes. |
connection_id | Optional pcn_…. Pins the outbound to this connection; lines are only matched there. |
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 the outbound's 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. |
One connection per outbound. ProfitDesk decides the connection in this order:
connection_idfrom the body, if sent. If theexternal_idalready belongs to an outbound of another connection, the request is refused with409 external_id_conflict.- Otherwise the connection of the existing outbound with this
external_id, together with the connections that the lines'line_id,purchase_idoritem_idname. - Only if neither names one: the connections that the lines' bare
skuvalues name.
If this yields more than one connection, the request is refused with 422 mixed_connections, whose details list the connection_ids: send one outbound per connection, or connection_id. A bare sku is read within the connection that steps 1 or 2 found, so two sellers with the same SKU are only a problem when nothing else names the connection. If nothing names a connection, the outcome depends on how many active connections your key has:
| Active connections | Result |
|---|---|
| One | That connection is used. |
| Several, the outbound has lines | No line matches any connection: 422 line_not_found. |
Several, no lines (for example cancelled for an unknown external_id) | 400 invalid_request: send connection_id. |
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"
}
]
}7.7 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"}
]
}
}7.8 Replay events
GET /v1/prep/events?since=evt_0193a1b2-…&types=inbound.announced,inbound.cancelled&connection=pcn_812&limit=100
Returns the same envelopes that are sent to your URL, in feed order (see below). Your feed spans all your connections and includes every event: types you are not subscribed to, events whose delivery failed or was skipped, and the events of connections removed since (ending with their connection.removed). Events are kept for 90 days.
| Query | Meaning |
|---|---|
since | An event id of your feed (exclusive, recommended) or an RFC 3339 instant. An unknown event id answers 400 invalid_request. |
types | Comma-separated event types |
connection | pcn_…: only this active connection's events |
limit | 1–100, default 50 |
{
"data": [
{
"id": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a12",
"type": "connection.created",
"api_version": "2026-11-01",
"occurred_at": "2026-11-08T14:30:00Z",
"created_at": "2026-11-08T14:30:01Z",
"connection_id": "pcn_905",
"data": {
"connection": {
"id": "pcn_905",
"status": "active",
"seller": {"name": "Nordlicht Handel", "customer_reference": null, "seller_id": null},
"created_at": "2026-11-08T14:30:00Z"
},
"changes": null
}
}
],
"next_cursor": "evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a12",
"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.
7.9 List items
GET /v1/prep/items?connection=pcn_812&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
}Items are per connection: two sellers with the same SKU are two items. sku filters by exact SKU. image_url is not filled in this list; the inbound payloads carry it.
8. Errors
Every non-2xx response has this body:
{
"error": {
"code": "mixed_connections",
"message": "the outbound's lines belong to more than one connection; send one outbound per connection, or connection_id",
"details": [{"connection_id": "pcn_812"}, {"connection_id": "pcn_905"}]
}
}| 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 your active connections (for inbounds: never announced to them) |
| 404 | connection_not_found | connection / connection_id is not one of your active connections |
| 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, or an outbound of another connection, 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 |
| 422 | mixed_connections | An outbound's lines belong to more than one connection |
| 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), mixed_connections (connection_ids) and inbound_cancelled for one line (line_id, sku).
9. Go-live checklist
- Get access (§2): send us your endpoint, company name and address. Store the signing secret and API key you receive in a secret store.
- Receive webhooks:
- an HTTPS endpoint on a public address that verifies the signature (§6.5), with both secrets during a rotation
- answers
2xxwithin 10 s and does the work afterwards - deduplicates by
X-Webhook-Event - copes with parallel requests
- On
connection.created, create or link the seller in your software, keyed byconnection_id. Confirm the match with the customer;customer_referenceis only a hint (§3.2). - On
connection.updated, refresh the seller's name and customer number. Onconnection.removed, stop expecting all of that connection's open inbounds. - Keep
connection_idwith every inbound you store, and send outbounds per connection. - On
inbound.announced, create an expected inbound, keyed byinbound.id, with one expected line perlines[].id. Return your id as{"external_id": "…"}if you have one; keep it unique across all sellers. 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). - Rate limit: stay within your key's limit across all sellers; on
429, waitRetry-Afterseconds. - Safety net: poll
GET /v1/prep/eventsfrom your last processed event id, and reconcileGET /v1/prep/connectionsandGET /v1/prep/inbounds?status=openonce in a while. You also need the feed to catch up after your endpoint was disabled (§6.3). - Test with a test connection:
connection.created, an announcement, a receipt, an outbound, a cancellation and the removal.
10. FAQ
How do I tell which seller an event belongs to?
By the envelope's connection_id. connection.created introduced it, with the seller's name, customer number and Amazon seller id; GET /v1/prep/connections lists all current ones.
Can I disconnect a seller? No. Only the seller can remove the connection. If you stop working with a seller, they remove you in ProfitDesk.
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 (§6.3), and your sellers see their announcements as "Delivering". After that, deliveries that are still failing are marked failed, the sellers see failed announcements and can announce the purchases again. After 72 hours without a single successful delivery your endpoint is disabled for all sellers. You can 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.
Two sellers ship me the same SKU. How do I report an outbound?
Send one outbound per seller. Name the lines by line_id or purchase_id, or send connection_id; then a bare sku is read within that seller's connection.
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?
No separate one. Ask us for a test connection from a test seller account and use a test warehouse and test purchases there. 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.