A webhook prep-center connection links a ProfitDesk account to warehouse software you run yourself.
Building software for many sellers? See prep-providers-v1.openapi.yaml (external prep providers).
ProfitDesk → you: announcements, cancellations and updates arrive as signed webhook events
at the URL you configured (see webhooks below). Missed events can be replayed with GET /v1/prep/events.
You → ProfitDesk: report goods received (POST /v1/prep/inbounds/{id}/receipts) and what you
ship out (POST /v1/prep/outbounds), and pull inbounds, items and events.
Every id carries a type prefix: pur_ purchase, itm_ item (one per SKU), inb_ inbound,
inl_ inbound line (one per purchase in an inbound), out_ outbound, pcn_ connection,
wh_ warehouse, evt_ event. Wherever a path takes an inbound or outbound {id}, you may
pass ext:<external_id> instead to address it by your own reference.
A seller can hold several webhook connections, for example one per warehouse setup; each has its
own pcn_ id, signing secret and API key, and a key only sees its own connection.
Every POST/PUT may carry an idempotency key, either event_id in the body or the
Idempotency-Key header (the body wins). It is required for receipts and outbounds.
Repeating a request with the same key and the identical method, path and body returns the
stored response with "duplicate": true and has no further effect. The same key with a
different request is 409 idempotency_conflict. Failed requests are not stored, so a key
that got a 4xx can be retried after fixing the request. Keys are scoped to the connection and kept for 30 days.
Receipts and outbounds carry occurred_at (RFC 3339, default: now). A line or outbound is only
changed by an event newer than the last one applied to it; older events are reported as
stale and change nothing, so out-of-order delivery is safe.
ProfitDesk POSTs a signed JSON envelope to your endpoint URL. The full request and response definition of every
event is in the Webhooks section at the bottom of this page; the schemas are under Schemas.
Event
Sent when
data
Default
inbound.announced
Goods are on their way to you
inbound, traces
on
inbound.updated
Tracking numbers added, an FNSKU became known, your external_id changed, or lines of a shared inbound were cancelled/restored
inbound, traces, changes (+ reason when a line was cancelled)
on
inbound.cancelled
The whole inbound is cancelled (purchase cancelled or deactivated, warehouse changed, re-announced)
inbound, traces, reason, replaced_by_inbound_id?
on
inbound.restored
A cancelled inbound is active again (if you never received the inbound, a reactivation arrives as inbound.announced)
inbound, traces
on
inbound.received
Echo of a receipt you reported
inbound, traces
off
outbound.recorded
Echo of an outbound you reported
outbound, traces
off
ping
"Send test" in ProfitDesk
message
always
The seller picks the events in the connection settings; the echo events are off by default because they only
repeat what you reported yourself. GET /v1/prep/events returns every event type regardless of the subscription.
Your response: any 2xx within 10 s counts as delivered; for inbound.announced the body may be
{"external_id": "<your id>"}; it is stored without an inbound.updated event. Timeouts, 408, 429 and 5xx
are retried until 72 h after the event was queued, 3xx and other 4xx (410 included) fail immediately; only a failed
attempt after 72 h without any successful delivery disables the endpoint. A purchase counts as announced
only once its inbound.announced was delivered.
Order and skips: events of the same inbound are delivered in order, one at a time.
If an inbound is cancelled before you received any of its events, all its open deliveries, the cancel included,
are skipped; a later reactivation then arrives as inbound.announced. An event is never delivered after an
inbound.cancelled of its inbound that you already received. Skipped events stay in GET /v1/prep/events,
which is ordered so that a cursor never skips an event.
Inbounds ordered by id. An inbound whose lines were all removed (the purchase moved to another warehouse or was re-announced) is not listed; find it through GET /v1/prep/events.
For an inbound whose lines were removed (the purchase moved to another warehouse or was
re-announced), returns the inbound as of its latest event with status: cancelled and every line
cancelled. 404 inbound_not_found means the id was never announced to this connection.
Unique per connection (409 external_id_conflict); afterwards the inbound
is addressable as ext:<external_id>. A changed value emits inbound.updated with
changes: ["external_id"]. A cancelled inbound answers 409 inbound_cancelled. The idempotency key is
optional here; without one, event_id is null in the response.
quantity_received is the absolute count for the line, so a correction downwards is just a
smaller number. Address a line by line_id (exact) or by sku within this inbound; several lines
of the same SKU are filled in order up to their expected quantity, any surplus goes to the last.
status is required. receiving is the count so far: a line closes when it reaches its expected
quantity and opens again if a later receiving count is lower. status: received closes every line
of the inbound, also lines counted short or not at all.
Only announced goods are accepted: an inbound never announced to the key's connections is 404 inbound_not_found,
a line or SKU that is not part of the inbound is 422 line_not_found, a cancelled inbound (also one whose
lines were removed because the purchase moved or was re-announced) is 409 inbound_cancelled.
A receipt that changed anything emits inbound.received.
Upserts by external_id; the lines you send replace the stored ones, and cancelled clears them.
Each line is matched to a purchase, in this priority: line_id (exact), purchase_id (exact,
must belong to an inbound of the outbound's connection), item_id or sku (first received first, capped at
what was received minus what other open or shipped outbounds already hold). Quantities that cannot be
matched are returned in unresolved; only when nothing at all matches is the request refused with
422 line_not_found.
Outbounds are a trace: they do not change any stock or forwarded quantity (FBA shipments are booked from
Amazon). Every request that is not stale emits outbound.recorded.
Returns the same envelopes that are delivered to your URL, whether or not your endpoint is subscribed to
the type and whether the delivery succeeded, failed or was skipped. Events are kept for 90 days.
The feed is ordered by the transaction that created each event, and an event shows up once every
transaction that started before it has finished (usually within a second, at most ten minutes), so no
event is ever inserted behind a next_cursor you already have. Poll with since set to the last
next_cursor you processed and deduplicate by id.
Resume with an evt_… id: that position is exact. An RFC 3339 instant is compared with the time the
event was stored, which can differ from the envelope's created_at by milliseconds, so the window starts
one second early and can repeat events you already have. Always deduplicate by id.
Returns the key's connection in the list shape shared with prep providers. seller.name is the workspace name, customer_reference is always null for an own-software connection.
ProfitDesk sends these events as signed POST requests to the endpoint URL of the connection. Headers, body and the responses ProfitDesk expects from you.
Delivered as POST to your endpoint URL with these headers:
Header
Value
X-Webhook-Event
the envelope id (evt_…), for deduplication
X-Webhook-Event-Type
e.g. inbound.announced
X-Webhook-Timestamp
Unix seconds of the attempt
X-Webhook-Signature
sha256=<hex>; during a secret rotation two, comma-separated
User-Agent
ProfitDesk-Webhooks/1
Verify the signature before trusting the body: compute
hex(HMAC-SHA256(secret, timestamp + "." + rawBody)) with your signing secret (whsec_…, used
as-is as the HMAC key) over the raw request body, and accept the request if it equals anysha256= entry of the header (compare in constant time). Reject timestamps more than five minutes
from your clock. After a rotation the previous secret stays valid for 24 hours.
Respond with any 2xx within 10 seconds. Timeouts, 408, 429 and 5xx are 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; 3xx and other 4xx
fail at once (410 included); only 72 h without any success disables the endpoint.
You may answer {"external_id": "<your id>"}; it is stored like PUT …/external-id, but without an
inbound.updated event. It must be unique within your connection; a value another inbound already uses is not stored
(the delivery still counts). The purchase only counts as announced once you
answered 2xx. Events of the same inbound are delivered in order, one at a time. If the inbound is
cancelled before you received any of its events, they are all skipped; a reactivation of such an inbound
also arrives as inbound.announced.
inbound.received and outbound.recorded echo your own API calls and are off unless you
subscribe to them.
3xx (redirects are not followed) and any other 4xx except 408/429, 410 included: the delivery fails at once, without retry; the endpoint stays enabled. For inbound.announced the seller sees the announcement as failed.
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. Timeouts (10 s), connection errors, 408 and 429 are treated the same. After 72 h without any successful delivery, a failed attempt disables the endpoint.
3xx (redirects are not followed) and any other 4xx except 408/429, 410 included: the delivery fails at once, without retry; the endpoint stays enabled. For inbound.announced the seller sees the announcement as failed.
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. Timeouts (10 s), connection errors, 408 and 429 are treated the same. After 72 h without any successful delivery, a failed attempt disables the endpoint.
3xx (redirects are not followed) and any other 4xx except 408/429, 410 included: the delivery fails at once, without retry; the endpoint stays enabled. For inbound.announced the seller sees the announcement as failed.
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. Timeouts (10 s), connection errors, 408 and 429 are treated the same. After 72 h without any successful delivery, a failed attempt disables the endpoint.
Same headers, signature, response and retry rules as inbound.announced. If you never received the inbound, a reactivation arrives as inbound.announced instead.
3xx (redirects are not followed) and any other 4xx except 408/429, 410 included: the delivery fails at once, without retry; the endpoint stays enabled. For inbound.announced the seller sees the announcement as failed.
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. Timeouts (10 s), connection errors, 408 and 429 are treated the same. After 72 h without any successful delivery, a failed attempt disables the endpoint.
3xx (redirects are not followed) and any other 4xx except 408/429, 410 included: the delivery fails at once, without retry; the endpoint stays enabled. For inbound.announced the seller sees the announcement as failed.
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. Timeouts (10 s), connection errors, 408 and 429 are treated the same. After 72 h without any successful delivery, a failed attempt disables the endpoint.
3xx (redirects are not followed) and any other 4xx except 408/429, 410 included: the delivery fails at once, without retry; the endpoint stays enabled. For inbound.announced the seller sees the announcement as failed.
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. Timeouts (10 s), connection errors, 408 and 429 are treated the same. After 72 h without any successful delivery, a failed attempt disables the endpoint.
One of: inbound.announced, inbound.updated, inbound.cancelled, inbound.restored, inbound.received, outbound.recorded, ping.
api_version
string
Yes
Example: 2026-11-01.
occurred_at
string (date-time)
Yes
created_at
string (date-time)
Yes
connection_id
string
Yes
Example: pcn_812.
data
object
Yes
data.message
string
Yes
Example request
{"id":"evt_0193a1b2-7c4d-7e8f-9a0b-1c2d3e4f5a08","type":"ping","api_version":"2026-11-01","occurred_at":"2026-11-03T09:00:00Z","created_at":"2026-11-03T09:00:00Z","connection_id":"pcn_812","data":{"message":"Test event from ProfitDesk"}}
null except on inbound.updated: the fields that changed. lines means some lines of a shared inbound were cancelled or restored while the inbound stays open; read each line's status. external_id is sent only for a changed value set with PUT …/external-id.
reason
string | null
Yes
null except on inbound.cancelled, and on inbound.updated when a line was cancelled. offer_deleted is reserved. One of: purchase_cancelled, purchase_deactivated, warehouse_changed, reannounced, offer_deleted.
replaced_by_inbound_id
string | null
Yes
null except on inbound.cancelled / inbound.updated with reason reannounced: the new inbound that takes over the goods.
received once every line is received or cancelled and at least one is received; cancelled once every line is cancelled. One of: open, received, cancelled.
name
string
Yes
<supplier> | <product> | <order numbers>; the product (first 30 characters, +N for more lines) only if the seller transfers product names. If none is known: the inbound id. Max length 255.
notes
string
Yes
Seller notes (if transferred) plus a final line Fulfillment: FBA or Fulfillment: FBM.
fulfillment
string
Yes
One of: FBA, FBM.
warehouse
object | null
Yes
The seller's warehouse the goods are sent to. A move between warehouses of the same connection sends no event; the next payload carries the new warehouse.
warehouse.id
string
Yes
Example: wh_12.
warehouse.name
string
Yes
order
object
Yes
order.numbers
array of string
Yes
order.supplier_host
string | null
Yes
Example: amazon.de.
order.purchased_at
string | null (date)
Yes
tracking_numbers
array of string
Yes
Inbound parcel tracking codes known so far, sorted; new ones arrive with inbound.updated. Codes are normalised: the text is split at whitespace (also Unicode spaces) and at , ; : | / \; every character other than A–Z, a–z, 0–9 is removed from each part (DE-LPLV-0GLI → DELPLV0GLI); parts without a digit (carrier labels such as DHL) are dropped; duplicates are compared case-insensitively, the first spelling is kept.