Menu

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

GraphQL
mutation DeclareTopUp($input: DeclareInboundInput!, $idempotencyKey: String!) {
  apiDeclareInbound(input: $input, idempotencyKey: $idempotencyKey) {
    id reference status origin inboundMode facilityId facilityCode expectedUnits dueAt
  }
}
Variables
{
  "idempotencyKey": "inbound:PO-77:3f9c…",
  "input": {
    "inboundMode": "SELLER_DROPOFF",
    "sellerReference": "PO-77",
    "lines": [{ "offerId": "0f6a…", "qty": 120 }]
  }
}

Slots, booking and rescheduling

GraphQL
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

GraphQL
mutation Withdraw($input: ApiWithdrawalInput!, $idempotencyKey: String!) {
  apiRequestWithdrawal(input: $input, idempotencyKey: $idempotencyKey) {
    id status facilityId reason createdAt
  }
}
Variables
{
  "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

GraphQL
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.

TypeScript
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

StateMeaning
DRAFTDeclared, no appointment yet (or the last one was missed).
SCHEDULEDA dock slot is booked; scheduledAt and appointmentId are set.
PICKED_UPWAMIA_PICKUP only: the driver collected the goods.
DROPPED_OFFThe goods arrived at the dock and wait for counting.
RECEIVINGThe warehouse is counting; receivedUnits grows line by line.
RECEIVEDCounted; discrepancies, if any, are open for the shop to answer.
CLOSEDStock is sellable; positions moved.
CANCELLEDCancelled by the shop before arrival or by the warehouse.

Withdrawal statuses

StateMeaning
REQUESTEDWaiting for the warehouse to decide.
APPROVEDAccepted; units are reserved. Book a dock slot.
SCHEDULEDA dock slot is booked; appointment holds it.
PREPARINGThe warehouse is staging the units.
READYStaged and waiting at the dock for the shop.
ARRIVEDThe shop is at the dock; hand-over under way.
CLOSEDHanded over in full to shopRepresentative; positions moved.
HANDED_OVERHanded over in part; each line carries a shortfallReason.
NO_SHOWThe slot passed without the shop; rebook before holdUntil (stagedHoldHours).
REJECTEDRefused; read decisionNote.
CANCELLEDCancelled by the shop before preparation, by the warehouse, or after the hold expired.

Events

TypeMeaning
inbound.declaredA shipment was declared (shipmentId).
inbound.scheduledFirst appointment booked (appointmentId).
inbound.rescheduledMoved to another slot (previousAppointmentId).
inbound.slot_closedThe facility closed over the booked slot; rebook (appointmentId).
inbound.arrivedChecked in at the dock.
inbound.receivedCounted; pull positions.
inbound.discrepancyA shortage, surplus or damage needs an answer.
inbound.no_showThe slot passed without arrival; rebook.
inbound.cancelledCancelled.
withdrawal.requestedA withdrawal was requested (withdrawalId).
withdrawal.decidedApproved or rejected; fetch the request for status and decisionNote.
withdrawal.scheduledA dock slot was booked or moved (appointmentId).
withdrawal.slot_closedThe facility closed over the booked slot; rebook (appointmentId).
withdrawal.readyStaged and waiting at the dock.
withdrawal.handed_overHanded over (in full or with a shortfall); pull positions.
withdrawal.no_showThe slot passed without the shop; rebook.
withdrawal.cancelledCancelled; the units are released.