Inbound and withdrawals
Offers in fulfilment mode stocked live in a Wamia facility: the warehouse counts them, shops top them up by appointment and ask for stock back with a withdrawal. This page is the integration side of that flow — Wamia Shop and the mobile app drive the same operations.
Which offers
Only offers whose fulfilmentMode is stocked can be declared, topped up or withdrawn. Cross-dock offers stay in the shop’s own stock and travel with each order; a top-up that names one is refused whole with VALIDATION_FAILED and the offending SKUs in the message.
For stocked offers the warehouse count is the truth. apiDeclareStocks accepts the item but writes nothing: the item comes back ok: true with skipped: true and skipReason: stocked_offer. Mirror apiStockedPositions.availableQty into the shop instead.
Declare a top-up
apiDeclareInbound takes the lines (offerId, qty), the facility (optional: the default facility of the store applies), the inboundMode — SELLER_DROPOFF when the shop brings the goods, WAMIA_PICKUP when a Wamia driver collects them, with pickup contact and address — a sellerReference and a note. It is keyed: replaying the same idempotencyKey returns the same shipment. The answer is an InboundShipmentView with origin: SELLER_DECLARED in status DRAFT.
apiInboundShipments(origin: SELLER_DECLARED) lists top-ups; ORDER_DRIVEN lists the cross-dock transfers Wamia created from orders. apiInboundShipment returns the lines, the appointment, the pickup task and the discrepancies of one shipment.
Book and reschedule the appointment
Drop-offs need a dock slot. apiInboundSlotGrid(facilityId, from, to) returns the slots of each day over a window of at most 14 days with capacity, booked and remaining; apiInboundSlots(facilityId, day) is the one-day form. Book with apiBookInboundAppointment(shipmentId, dockId, startsAt): the shipment moves to SCHEDULED and inbound.scheduled fires. Slots in the past, outside dock hours, inside the facility’s lead time or already full are refused with VALIDATION_FAILED.
To move a booked appointment call apiRescheduleInboundAppointment with the new slot and an optional note; the previous slot is released, inbound.rescheduled fires with previousAppointmentId. Both calls are unkeyed and never retried by the SDK: after a timeout read apiInboundShipment and look at appointment before calling again. The shop receives a reminder the day before; a missed slot becomes inbound.no_show and the shipment goes back to DRAFT to be rebooked.
Withdrawals
apiRequestWithdrawal asks the warehouse to hand stock back: facilityCode, lines by sku or offerId with qty, an optional reason and note. It is keyed. The request starts in REQUESTED; the warehouse approves or rejects it (withdrawal.decided; approval reserves the units), you book a dock slot with apiBookWithdrawalAppointment (slots from apiInboundSlots; apiRescheduleWithdrawalAppointment moves it; withdrawal.scheduled), the floor prepares it (withdrawal.ready when it waits at the dock) and records the hand-over (withdrawal.handed_over, with a shortfall reason per line when it is partial). The pick-up slip is signed on paper. apiWithdrawals(status) lists the requests of the store with their appointment. Quantities above availableQty are refused: reserved units belong to open orders.
Mirror positions, do not declare them
apiStockedPositions(updatedSince, facilityCode) is cursor-paginated like the listings. Poll it on the same schedule as your stock sync and write availableQty to the shop; onHandQty minus reservedQty is what the storefront may sell. inbound.received and withdrawal.handed_over are the moments positions move; a webhook of either family is a good trigger for an incremental pull.
Example
Declare a top-up
mutation DeclareTopUp($input: DeclareInboundInput!, $idempotencyKey: String!) {
apiDeclareInbound(input: $input, idempotencyKey: $idempotencyKey) {
id reference status origin inboundMode facilityId facilityCode expectedUnits dueAt
}
}{
"idempotencyKey": "inbound:PO-77:3f9c…",
"input": {
"inboundMode": "SELLER_DROPOFF",
"sellerReference": "PO-77",
"lines": [{ "offerId": "0f6a…", "qty": 120 }]
}
}Slots, booking and rescheduling
query Slots($facilityId: String!, $from: DateTime!, $to: DateTime!) {
apiInboundSlotGrid(facilityId: $facilityId, from: $from, to: $to) {
day
slots { dockId dockCode startsAt endsAt capacity booked remaining closedReason }
}
}
mutation Book($shipmentId: String!, $dockId: String!, $startsAt: DateTime!) {
apiBookInboundAppointment(shipmentId: $shipmentId, dockId: $dockId, startsAt: $startsAt) {
id status scheduledAt appointmentId
}
}
mutation Move($shipmentId: String!, $dockId: String!, $startsAt: DateTime!, $note: String) {
apiRescheduleInboundAppointment(shipmentId: $shipmentId, dockId: $dockId, startsAt: $startsAt, note: $note) {
id status scheduledAt appointmentId
}
}Request a withdrawal
mutation Withdraw($input: ApiWithdrawalInput!, $idempotencyKey: String!) {
apiRequestWithdrawal(input: $input, idempotencyKey: $idempotencyKey) {
id status facilityId reason createdAt
}
}{
"idempotencyKey": "withdrawal:2026-10-10:8a1b…",
"input": {
"facilityCode": "TUN-1",
"reason": "end_of_season",
"lines": [{ "sku": "TSHIRT-RED-M", "qty": 30 }]
}
}Read the warehouse count
query StockedPositions($cursor: String, $updatedSince: DateTime) {
apiStockedPositions(cursor: $cursor, limit: 200, updatedSince: $updatedSince) {
items { sku offerId facilityCode onHandQty reservedQty availableQty updatedAt }
nextCursor
}
}With the Node SDK
@wamia/connect-sdk ships typed helpers for every operation above, keyed where the API is keyed, plus isInboundEvent and isWithdrawalEvent to narrow webhook payloads.
import { ConnectClient, idempotencyKeyFor } from '@wamia/connect-sdk';
const client = new ConnectClient({ apiKey: process.env.WAMIA_API_KEY! });
// 1. The warehouse count wins for stocked offers: mirror it, never declare it.
for await (const position of client.allStockedPositions({ updatedSince: lastSync })) {
await setShopStock(position.sku, position.availableQty);
}
const result = await client.declareStocks(crossDockRows, idempotencyKeyFor('stock', crossDockRows));
for (const item of result.items) {
if (item.skipped) log(`${item.sku}: kept the warehouse count (${item.skipReason})`);
}
// 2. Declare a top-up, then book the first free slot of the next two weeks.
const shipment = await client.declareInbound(
{ lines: [{ offerId, qty: 120 }], inboundMode: 'SELLER_DROPOFF', sellerReference: 'PO-77' },
idempotencyKeyFor('inbound:PO-77', [offerId]),
);
const days = await client.inboundSlotGrid(shipment.facilityId, from, to);
const slot = days.flatMap((day) => day.slots).find((s) => s.remaining > 0);
if (slot) await client.bookInboundAppointment(shipment.id, slot.dockId, slot.startsAt);
// 3. Ask for stock back; the warehouse approves, picks, then hands it over.
await client.requestWithdrawal(
{ facilityCode: 'TUN-1', lines: [{ sku: 'TSHIRT-RED-M', qty: 30 }], reason: 'end_of_season' },
idempotencyKeyFor('withdrawal:2026-10-10', ['TSHIRT-RED-M']),
);Inbound statuses
| State | Meaning |
|---|---|
| DRAFT | Declared, no appointment yet (or the last one was missed). |
| SCHEDULED | A dock slot is booked; scheduledAt and appointmentId are set. |
| PICKED_UP | WAMIA_PICKUP only: the driver collected the goods. |
| DROPPED_OFF | The goods arrived at the dock and wait for counting. |
| RECEIVING | The warehouse is counting; receivedUnits grows line by line. |
| RECEIVED | Counted; discrepancies, if any, are open for the shop to answer. |
| CLOSED | Stock is sellable; positions moved. |
| CANCELLED | Cancelled by the shop before arrival or by the warehouse. |
Withdrawal statuses
| State | Meaning |
|---|---|
| REQUESTED | Waiting for the warehouse to decide. |
| APPROVED | Accepted; units are reserved. Book a dock slot. |
| SCHEDULED | A dock slot is booked; appointment holds it. |
| PREPARING | The warehouse is staging the units. |
| READY | Staged and waiting at the dock for the shop. |
| ARRIVED | The shop is at the dock; hand-over under way. |
| CLOSED | Handed over in full to shopRepresentative; positions moved. |
| HANDED_OVER | Handed over in part; each line carries a shortfallReason. |
| NO_SHOW | The slot passed without the shop; rebook before holdUntil (stagedHoldHours). |
| REJECTED | Refused; read decisionNote. |
| CANCELLED | Cancelled by the shop before preparation, by the warehouse, or after the hold expired. |
Events
| Type | Meaning |
|---|---|
| inbound.declared | A shipment was declared (shipmentId). |
| inbound.scheduled | First appointment booked (appointmentId). |
| inbound.rescheduled | Moved to another slot (previousAppointmentId). |
| inbound.slot_closed | The facility closed over the booked slot; rebook (appointmentId). |
| inbound.arrived | Checked in at the dock. |
| inbound.received | Counted; pull positions. |
| inbound.discrepancy | A shortage, surplus or damage needs an answer. |
| inbound.no_show | The slot passed without arrival; rebook. |
| inbound.cancelled | Cancelled. |
| withdrawal.requested | A withdrawal was requested (withdrawalId). |
| withdrawal.decided | Approved or rejected; fetch the request for status and decisionNote. |
| withdrawal.scheduled | A dock slot was booked or moved (appointmentId). |
| withdrawal.slot_closed | The facility closed over the booked slot; rebook (appointmentId). |
| withdrawal.ready | Staged and waiting at the dock. |
| withdrawal.handed_over | Handed over (in full or with a shortfall); pull positions. |
| withdrawal.no_show | The slot passed without the shop; rebook. |
| withdrawal.cancelled | Cancelled; the units are released. |