ProfitGuard is here. AI repricing for Amazon, fully protected and on autopilot. Try it free
Guideapi_version 2026-11-01https://api.profitpath.app

ProfitDesk Prep API v1 for prep providers

Serve many ProfitDesk sellers from one warehouse integration.

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 as connection_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 2xx to its inbound.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

ItemNotes
Company nameThe name sellers see in the prep-center list, max 100 characters
Country and addressCountry (ISO code or name) is required. Address line(s), postal code and city are shown to sellers.
Webhook endpoint URLThe 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 POST with a JSON body and answers 2xx within 10 seconds (connect 5 s, TLS handshake 5 s, the whole request 10 s).
  • Redirects are not followed. A 3xx answer 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:

CredentialFormatUsed for
Signing secretwhsec_ + 64 hex charactersVerifying webhook signatures (§6.5)
Provider API keyppk_ + 48 hex charactersCalling 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:

ChangeEffect
Webhook URLUsed from the next delivery attempt on, including retries already waiting
Link templateChanged or removed on request; the placeholders are {inbound_id} and {external_id} (§2.1)
Event typesApplies to events created afterwards. Deliveries already queued are still sent.
Signing secret rotationThe 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 rotationThe old key is revoked at once; API servers that cached it accept it for up to one more minute.
Rate limitSet together with a key rotation, so it comes with a new key
DeactivationYou 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-activationDeliveries 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

  1. 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.
  2. 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.
  3. The seller changes the name you see or their customer number. You receive connection.updated with changes.
  4. 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:

FieldMeaning
idpcn_…, the connection
statusactive; removed only on connection.removed
seller.nameThe 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_referenceTheir customer number at your company exactly as they entered it (max 100 characters), null if none
seller.seller_idThe 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_atWhen 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_id says 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_id of 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 same inb_… 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.cancelled each. connection.removed voids 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 new connection.created.

4. Identifiers

All ids are strings with a type prefix. Store them as opaque strings.

PrefixObjectStable?Notes
pcn_ConnectionForeverOne seller's connection to you. Every response and event names it as connection_id.
pur_PurchaseForeverThe 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_ItemForever per SKUOne article per seller SKU and connection. Carries sku, fnsku, asin, ean. Two sellers with the same SKU are two items.
inb_InboundPer announcementA re-announcement creates a new inbound; the old one gets inbound.cancelled with replaced_by_inbound_id.
inl_Inbound linePer announcementOne line per purchase inside an inbound. Use it in receipts and outbounds for exact matching.
out_OutboundForeverOur id for an outbound. You address outbounds by your own external_id.
wh_WarehouseForeverThe seller's warehouse the goods are sent to
evt_EventForeverWebhook 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.

JSON
{
  "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:

HTTP
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 answers 404.
  • GET /v1/prep/inbounds, GET /v1/prep/items and GET /v1/prep/events take connection=pcn_… to narrow them to one connection. A value that is not one of your active connections answers 404 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 are YYYY-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 sendResult
Same key, identical method, path and bodyThe stored response is replayed with "duplicate": true. Nothing changes.
Same key, different request409 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:

HeaderValue
Content-Typeapplication/json
X-Webhook-EventThe event id (evt_…), the same as the envelope's id. Use it to deduplicate.
X-Webhook-Event-Typee.g. inbound.announced
X-Webhook-TimestampUnix seconds of this delivery attempt
X-Webhook-Signaturesha256=<hex>. During a secret rotation it holds two values, separated by a comma.
User-AgentProfitDesk-Webhooks/1

Envelope (here of a connection.created):

JSON
{
  "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

EventWhendataDefault
connection.createdA seller connected to youconnection, changes (null)on
connection.updatedThe seller changed the name you see or their customer numberconnection, changeson
connection.removedThe seller removed the connection. Nothing of it is reachable afterwards (§3.4).connection, changes (null)on
inbound.announcedGoods are on their way to youinbound, traceson
inbound.updatedTracking numbers were added, an FNSKU became known, your external_id changed, or some lines of a shared inbound were cancelled or restoredinbound, traces, changes (see below); reason when a line was cancelledon
inbound.cancelledThe whole inbound is cancelled: the purchase was cancelled or deactivated, moved to another warehouse, or re-announcedinbound, traces, reason, optionally replaced_by_inbound_idon
inbound.restoredA cancelled inbound is active again. If you never received the inbound, the reactivation arrives as inbound.announced instead (§6.4).inbound, traceson
inbound.receivedEcho of your own receipt via the APIinbound, tracesoff
outbound.recordedEcho of your own outbound via the APIoutbound, tracesoff

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:

ValueMeaning
tracking_numbersNew inbound tracking codes, in inbound.tracking_numbers
fnskuAn item got its FNSKU, in lines[].item.fnsku
external_idYour 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.
linesSome 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 2xx within 10 seconds. Do slow work asynchronously after answering.
  • For inbound.announced you may answer with {"external_id": "<your id>"} (a string or a number, max 200 characters). ProfitDesk stores it as the inbound's external_id, like PUT …/external-id, but sends no inbound.updated for it: you already know the value. Later events carry it. It follows the same rule as PUT: 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 with PUT …/external-id to get the 409 external_id_conflict error explicitly.
  • Any other body, and the body of any other event, is ignored.

Retries.

Your responseWhat ProfitDesk does
2xxDelivered
Timeout, connection error, 408, 429, 5xxRetried 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.cancelled never overtakes the inbound.announced of the same inbound.
  • Per connection, connection.* events never overtake and are never overtaken by the connection's other events: connection.created arrives before anything else of that connection, and connection.removed after 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:

  1. Read the raw request body. Do not parse and re-serialize it first.
  2. Compute hex(HMAC-SHA256(secret, timestamp + "." + rawBody)). The key is your signing secret exactly as you received it (including whsec_), and the timestamp comes from the X-Webhook-Timestamp header.
  3. The header X-Webhook-Signature holds one or more sha256=<hex> values separated by commas. Accept if any of them equals yours. Compare in constant time.
  4. 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);
  });
}

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):

JSON
{
  "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
}
FieldNotes
statusopen, received (every line closed) or cancelled (every line cancelled). Each line has its own status as well.
batch_idSet when several purchases were announced together and will physically arrive together
name, notesBuilt 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.
fulfillmentFBA or FBM: where the goods go after your prep
item.fnskuCan be null at announcement time. It follows in an inbound.updated event once Amazon has assigned it.
tracking_numbersInbound 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)

FieldTypeDescription
idstringEvent id evt_<uuid>, equal to the X-Webhook-Event header. Unique; use it to deduplicate.
typestringconnection.created, connection.updated, connection.removed, inbound.announced, inbound.updated, inbound.cancelled, inbound.restored, inbound.received or outbound.recorded
api_versionstringPayload version, currently 2026-11-01
occurred_atstring (date-time)When the change happened in ProfitDesk; for the echo events, the occurred_at you reported
created_atstring (date-time)When the event was created. Delivery may be later because of retries.
connection_idstringpcn_… of the seller's connection. Stays set after the connection was removed.
dataobjectDepends on type; see below

Connection events

data of connection.created, connection.updated and connection.removed:

FieldTypeDescription
connectionConnectionThe connection after the change; on connection.removed as it was, with status: "removed"
changesarray of string | nullnull except on connection.updated: seller_name and/or customer_reference

Connection

FieldTypeDescription
idstringpcn_…
statusstringactive, or removed on connection.removed
seller.namestring | nullThe name the seller chose for you to see (max 50 characters), else their workspace name in ProfitDesk; null if neither is set
seller.customer_referencestring | nullThe seller's customer number at your company, as they entered it (max 100 characters). Not verified by ProfitDesk.
seller.seller_idstring | nullThe Amazon selling partner id of the account owner, null if no Amazon account is connected
created_atstring (date-time)When the seller connected

data of inbound.* events (InboundEventData)

FieldTypeDescription
inboundInboundThe inbound after the change
tracesarray of TraceOne entry per line, in the same order as inbound.lines
changesarray of string | nullnull except on inbound.updated: tracking_numbers, fnsku, external_id, lines
reasonstring | nullnull except on inbound.cancelled, and on inbound.updated when a line was cancelled: purchase_cancelled, purchase_deactivated, warehouse_changed, reannounced
replaced_by_inbound_idstring | nullnull except on inbound.cancelled/inbound.updated with reason reannounced: the new inb_… that takes over the goods

Inbound

FieldTypeDescription
idstringinb_…
external_idstring | nullYour reference, if set
batch_idstring | nullSame value for purchases announced together, which arrive together
statusstringopen; received when every line is received or cancelled and at least one is received; cancelled when every line is cancelled
namestring<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.
notesstringThe seller's notes (if transferred, each distinct note once), plus a final line Fulfillment: FBA or Fulfillment: FBM
fulfillmentstringFBA or FBM
warehouseobject | 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.numbersarray of stringSupplier order numbers of all purchases in the inbound
order.supplier_hoststring | nullShop domain, e.g. amazon.de; null if unknown
order.purchased_atstring (date) | nullPurchase date YYYY-MM-DD; null if unknown
tracking_numbersarray of stringInbound parcel tracking codes known so far, sorted ascending. Codes are normalised (see below).
linesarray of InboundLineOne line per purchase
created_atstring (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:

  1. The text is split at whitespace (including non-breaking and other Unicode spaces) and at , ; : | / \.
  2. Inside each part, every character other than A–Z, a–z and 0–9 is removed. Hyphens and dots are dropped, not split on: DE-LPLV-0GLI becomes DELPLV0GLI.
  3. Parts without a digit are dropped. This removes carrier labels such as DHL.
  4. 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

FieldTypeDescription
idstringinl_…. Use it as line_id in receipts and outbounds.
purchase_idstringpur_…, the stable trace key
itemItemThe article
quantity_expectedintegerUnits announced
quantity_receivedintegerAbsolute count reported via receipts, 0 before the first one
quantity_damagedintegerDamaged units reported via receipts
statusstringopen, received or cancelled

Item

FieldTypeDescription
idstring | nullitm_…; one item per SKU and connection
skustringThe seller's Amazon SKU
fnskustring | nullAmazon label barcode. Can be null at announcement; then it follows with inbound.updated and changes: ["fnsku"].
asinstring | nullAmazon ASIN
eanstring | nullEAN/GTIN
titlestring | nullProduct title
image_urlstring | nullProduct image

Trace

FieldTypeDescription
purchase_idstringpur_…
inbound_idstring | nullinb_…
inbound_line_idstring | nullinl_…
item_idstring | nullitm_…
sku, fnsku, asinstring | nullAs on the item
order_numberstring | nullSupplier order number of this purchase
batch_idstring | nullAs on the inbound
outbound_idsarray of stringout_… of non-cancelled outbounds that carry units of this purchase
amazon_shipment_idsarray of stringFBA shipment ids (FBA…) of those outbounds

data of outbound.recorded

FieldTypeDescription
outboundOutboundThe outbound as stored after your request
tracesarray of TraceOne per purchase on the outbound

Outbound

FieldTypeDescription
idstringout_…
external_idstringYour outbound id
statusstringcreated, shipped or cancelled
destination.typestringfba, fbm or other
destination.amazon_shipment_idstring | nullFBA shipment id
destination.marketplacestring | nulle.g. DE
tracking_numbersarray of stringOutbound tracking codes
shipped_atstring (date-time) | nullSet the first time the status is shipped; kept afterwards
lines[].purchase_idstringpur_… the units were matched to
lines[].inbound_line_idstring | nullinl_…, when known
lines[].quantityintegerUnits of this purchase in the outbound
lines[].resolved_bystringHow the line was matched: line_id, purchase_id, item_id or sku_fifo

Your response

FieldTypeDescription
external_idstring or number, optionalOnly 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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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
HTTP/1.1 200 OK
Content-Type: application/json

{"external_id": "WMS-IN-5521"}

inbound.updated: new tracking code and FNSKU

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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)

JSON
{
  "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)

JSON
{
  "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 responseResult
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 errorRetried for up to 72 h (§6.3)

7. REST API (you → ProfitDesk)

MethodPathPurpose
GET/v1/prep/connectionsList your active connections
GET/v1/prep/inboundsList inbounds
GET/v1/prep/inbounds/{id}Get one inbound
PUT/v1/prep/inbounds/{id}/external-idSet your reference
POST/v1/prep/inbounds/{id}/receiptsReport goods received
POST/v1/prep/outboundsReport an outbound (create, update, ship, cancel)
GET/v1/prep/outbounds/{id}Get one outbound
GET/v1/prep/eventsReplay webhook events
GET/v1/prep/itemsList 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).

JSON
{
  "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=

QueryMeaning
connectionpcn_…: only this connection's inbounds. Without it, all your connections.
statusopen, received or cancelled
updated_sinceOnly inbounds changed after this instant, including tracking and FNSKU updates
limit, cursorPagination
JSON
{
  "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

JSON
{
  "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

JSON
{ "external_id": "WMS-IN-5521" }
  • The value is unique across all your active connections (409 external_id_conflict otherwise), 1–200 printable characters.
  • Afterwards the inbound is addressable as ext:WMS-IN-5521.
  • A changed value sends inbound.updated with changes: ["external_id"].
  • An idempotency key is optional here; event_id is null in the response without one.
  • A cancelled inbound answers 409 inbound_cancelled.
  • Alternatively, return {"external_id": "…"} in your response to inbound.announced.

Response 200:

JSON
{
  "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

JSON
{
  "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}
  ]
}
FieldMeaning
event_idIdempotency key (required, or send the Idempotency-Key header)
occurred_atWhen you counted (default: now)
statusRequired. 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_idinl_…, exact match (preferred)
lines[].skuAlternative 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_receivedRequired. The absolute count for the line, not a delta. To correct a count downwards, send the smaller number.
lines[].quantity_damagedOptional, 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:

JSON
{
  "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: changed
  • unchanged: same values as before
  • stale: a receipt with the same or a later occurred_at had already been applied to the line

A receipt that changed anything sends inbound.received.

Only announced goods are accepted:

SituationResponse
Inbound never announced to one of your active connections404 inbound_not_found
line_id or SKU not part of this inbound422 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-announced409 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

JSON
{
  "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}]
}
FieldMeaning
event_idIdempotency key (required, or send the Idempotency-Key header)
external_idYour 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_idOptional pcn_…. Pins the outbound to this connection; lines are only matched there.
occurred_atWhen it happened (default: now)
statuscreated, shipped or cancelled (cancelled clears the lines)
destination.typeRequired: fba, fbm or other. Missing or empty is refused with 400 invalid_request.
destination.amazon_shipment_idThe Amazon FBA shipment id (FBA…), max 64 characters. Please always send it for FBA: it links your outbound to Amazon's shipment.
destination.marketplacee.g. DE, max 8 characters
tracking_numbersUp to 50 codes, 1–200 characters each
linesThe 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:

ReferenceMatching
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 skuFirst 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:

  1. connection_id from the body, if sent. If the external_id already belongs to an outbound of another connection, the request is refused with 409 external_id_conflict.
  2. Otherwise the connection of the existing outbound with this external_id, together with the connections that the lines' line_id, purchase_id or item_id name.
  3. Only if neither names one: the connections that the lines' bare sku values 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 connectionsResult
OneThat connection is used.
Several, the outbound has linesNo 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:

JSON
{
  "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 with index (its position in lines), the reference you sent (the others null), the part of quantity that could not be matched, and a reason: line_not_found, purchase_not_found, item_not_found, sku_not_found, insufficient_received_quantity or invalid_reference.
  • The request is refused with 422 line_not_found only if nothing matches; details then holds the unresolved entries.
  • stale: true means 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):

JSON
{
  "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

JSON
{
  "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.

QueryMeaning
sinceAn event id of your feed (exclusive, recommended) or an RFC 3339 instant. An unknown event id answers 400 invalid_request.
typesComma-separated event types
connectionpcn_…: only this active connection's events
limit1–100, default 50
JSON
{
  "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=

JSON
{
  "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:

JSON
{
  "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"}]
  }
}
HTTPcodeMeaning
400invalid_requestInvalid input; the message names the field
400invalid_jsonEmpty body or malformed JSON
400idempotency_key_requiredReceipt or outbound sent without event_id / Idempotency-Key
401unauthorizedMissing, unknown or revoked API key
404inbound_not_found, outbound_not_foundNo such object on your active connections (for inbounds: never announced to them)
404connection_not_foundconnection / connection_id is not one of your active connections
409inbound_cancelledThe inbound or line is cancelled, or its lines were removed because the purchase moved or was re-announced
409idempotency_conflictThe key was already used for a different request
409external_id_conflictAnother inbound, or an outbound of another connection, already uses this external_id
413payload_too_largeBody over 1 MB
422line_not_foundThe line, SKU or reference is not part of the inbound, or nothing matched
422mixed_connectionsAn outbound's lines belong to more than one connection
429rate_limitedSlow down. Retry after Retry-After seconds.
500internal_errorRetry 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

  1. Get access (§2): send us your endpoint, company name and address. Store the signing secret and API key you receive in a secret store.
  2. Receive webhooks:
    • an HTTPS endpoint on a public address that verifies the signature (§6.5), with both secrets during a rotation
    • answers 2xx within 10 s and does the work afterwards
    • deduplicates by X-Webhook-Event
    • copes with parallel requests
  3. On connection.created, create or link the seller in your software, keyed by connection_id. Confirm the match with the customer; customer_reference is only a hint (§3.2).
  4. On connection.updated, refresh the seller's name and customer number. On connection.removed, stop expecting all of that connection's open inbounds.
  5. Keep connection_id with every inbound you store, and send outbounds per connection.
  6. On inbound.announced, create an expected inbound, keyed by inbound.id, with one expected line per lines[].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.
  7. On inbound.updated, refresh tracking numbers and FNSKUs (label printing needs the FNSKU). With changes: ["lines"], refresh each line's status.
  8. On inbound.cancelled, stop expecting the goods. If replaced_by_inbound_id is set, the goods now belong to that inbound.
  9. When goods arrive, POST …/receipts with absolute counts per line_id. Send status: "received" when the inbound is complete.
  10. When you ship, POST /v1/prep/outbounds with your external_id, the lines by line_id, and for FBA the amazon_shipment_id. Send it again when it changes (created → shipped, or cancelled).
  11. Rate limit: stay within your key's limit across all sellers; on 429, wait Retry-After seconds.
  12. Safety net: poll GET /v1/prep/events from your last processed event id, and reconcile GET /v1/prep/connections and GET /v1/prep/inbounds?status=open once in a while. You also need the feed to catch up after your endpoint was disabled (§6.3).
  13. 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.