For external prep providers: warehouse or prep software that serves many ProfitDesk sellers. ProfitDesk
registers you and lists you in every seller's prep-center picker; each seller who picks you is one
connection (pcn_…). One endpoint, one signing secret and one provider API key cover all of them.
A seller's own software (one private connection) is documented separately in prep-v1.openapi.yaml.
ProfitDesk → you: new sellers, announcements, cancellations and updates arrive as signed webhook events
at your endpoint (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 connections, inbounds, items and events.
There is no self-service sign-up and no dashboard. Send ProfitDesk (support@profitpath.com) your company name,
country and address (shown to sellers), your webhook endpoint (https, public address, answers 2xx within
10 s, redirects are not followed) and optionally a deep-link template and the events you want. ProfitDesk
registers you and hands you the signing secret (whsec_…) and the provider API key (ppk_…) once, over a
secure channel. URL, event selection, secret and key rotation, rate limit and deactivation are changed by
ProfitDesk on request through the same contact. After a secret rotation the old secret signs as well for 24 hours;
a key rotation revokes the old key at once (cached for up to a minute).
A seller picks you, may enter the name you see them under and their customer number at your company, and
links warehouses to the connection: connection.created. Changes of name or customer number send
connection.updated, the removal connection.removed. You cannot create or end a connection yourself.
customer_reference is entered by the seller and not verified by ProfitDesk.
Your key reaches the data of all your active connections and nothing else: every {id} is looked up across
them (404 otherwise), list endpoints and the event feed span them and take connection=pcn_… to narrow
down, and every response carries connection_id. An outbound belongs to exactly one connection
(422 mixed_connections otherwise). After a removal the connection's objects answer 404; its open inbounds
get no inbound.cancelled each; its events stay in your feed.
Every id carries a type prefix: pcn_ connection, pur_ purchase, itm_ item (one per SKU and connection),
inb_ inbound, inl_ inbound line (one per purchase in an inbound), out_ outbound, wh_ warehouse,
evt_ event. Wherever a path takes an inbound or outbound {id}, you may pass ext:<external_id> instead.
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 you as a provider,
across all your connections, and kept for 30 days. External ids are unique across your connections too.
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
connection.created
A seller connected to you
connection, changes
on
connection.updated
The seller changed the name you see or their customer number
connection, changes
on
connection.removed
A seller removed the connection
connection, changes
on
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
ProfitDesk sets your subscription on request. A new provider gets the seven events marked on; the echo events are
available on request and off by default because they only repeat what you reported yourself. There is no ping
for providers. 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.
Per connection, connection.created comes first and connection.removed last. Other events may arrive in
parallel and in any order.
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 one of your active connections.
Unique across all your active connections (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.
An outbound belongs to one connection: connection_id from the body (409 external_id_conflict if the
external_id belongs to another connection); else the existing outbound's connection together with the
connections the lines' line_id, purchase_id or item_id name; only if none: the connections the bare
sku values name. More than one is 422 mixed_connections. If none is named: with a single active
connection that one is used; with several, lines that match nothing are 422 line_not_found and a
request without lines (e.g. a cancel of an unknown external_id) is 400 invalid_request.
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.
Replay the webhook events of all your connections#
GET/v1/prep/events
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.
Your feed spans all your connections, including the events of connections removed since (ending with
connection.removed).
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.
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. connection.created
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 your endpoint, for all your sellers.
This is the first event of a connection. customer_reference is entered by the seller and not verified by
ProfitDesk: confirm the match with your customer.
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 connection.created. The last event of the
connection: afterwards nothing of it is reachable through the API, and its inbounds that were still open
are void (they get no inbound.cancelled each). Its events stay in GET /v1/prep/events.
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 and retry rules as connection.created.
You may answer {"external_id": "<your id>"}; it is stored like PUT …/external-id, but without an
inbound.updated event. It must be unique across all your sellers' connections; 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.
Empty unless the code fills it: line_not_found (unknown lines, or the unresolved entries of an outbound), mixed_connections (connection_ids), inbound_cancelled for one line.
The name the seller chose for you to see, else their workspace name in ProfitDesk; null if neither is set. A workspace rename changes it without a connection.updated. Max length 50.
seller.customer_reference
string | null
Yes
The seller's customer number at your company, as they entered it; not verified by ProfitDesk. Max length 100.
seller.seller_id
string | null
Yes
The Amazon selling partner id of the account owner; null if no Amazon account is connected.
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.