Menu

Réceptions et retraits

Les offres en mode de préparation stocked vivent dans un entrepôt Wamia : l’entrepôt les compte, la boutique les réapprovisionne sur rendez-vous et récupère du stock par un retrait. Cette page est le versant intégration de ce flux — Wamia Shop et l’application mobile pilotent les mêmes opérations.

Quelles offres

Seules les offres dont le fulfilmentMode est stocked peuvent être déclarées, réapprovisionnées ou retirées. Les offres cross-dock restent dans le stock de la boutique et voyagent avec chaque commande ; un réapprovisionnement qui en nomme une est refusé en bloc avec VALIDATION_FAILED et les SKU fautifs dans le message.

Pour les offres stockées, le comptage de l’entrepôt fait foi. apiDeclareStocks accepte la ligne mais n’écrit rien : la ligne revient ok: true avec skipped: true et skipReason: stocked_offer. Reflétez plutôt apiStockedPositions.availableQty dans la boutique.

Déclarer un réapprovisionnement

apiDeclareInbound prend les lignes (offerId, qty), l’entrepôt (optionnel : l’entrepôt par défaut de la boutique s’applique), le inboundMode — SELLER_DROPOFF quand la boutique apporte la marchandise, WAMIA_PICKUP quand un chauffeur Wamia la collecte, avec le contact et l’adresse dans pickup — une sellerReference et une note. L’appel est idempotent : rejouer la même idempotencyKey renvoie la même expédition. La réponse est une InboundShipmentView d’origin: SELLER_DECLARED au statut DRAFT.

apiInboundShipments(origin: SELLER_DECLARED) liste les réapprovisionnements ; ORDER_DRIVEN liste les transferts cross-dock que Wamia a créés à partir des commandes. apiInboundShipment renvoie les lignes, le rendez-vous, la tâche d’enlèvement et les écarts d’une expédition.

Réserver et déplacer le rendez-vous

Un dépôt a besoin d’un créneau de quai. apiInboundSlotGrid(facilityId, from, to) renvoie les créneaux de chaque jour sur une fenêtre de 14 jours au plus, avec capacity, booked et remaining ; apiInboundSlots(facilityId, day) est la forme à un jour. Réservez avec apiBookInboundAppointment(shipmentId, dockId, startsAt) : l’expédition passe en SCHEDULED et inbound.scheduled part. Les créneaux passés, hors horaires du quai, dans le délai de prévenance de l’entrepôt ou déjà pleins sont refusés avec VALIDATION_FAILED.

Pour déplacer un rendez-vous réservé, appelez apiRescheduleInboundAppointment avec le nouveau créneau et une note facultative ; l’ancien créneau est libéré et inbound.rescheduled part avec previousAppointmentId. Ces deux appels n’ont pas de clé et le SDK ne les rejoue jamais : après un délai dépassé, relisez apiInboundShipment et regardez appointment avant de rappeler. La boutique reçoit un rappel la veille ; un créneau manqué devient inbound.no_show et l’expédition repasse en DRAFT pour être re-réservée.

Retraits

apiRequestWithdrawal demande à l’entrepôt de rendre du stock : facilityCode, des lignes par sku ou offerId avec qty, un reason et une note facultatifs. L’appel est idempotent. La demande démarre en REQUESTED ; l’entrepôt l’approuve ou la refuse (withdrawal.decided ; l’approbation réserve les unités), vous réservez un créneau de quai avec apiBookWithdrawalAppointment (créneaux de apiInboundSlots ; apiRescheduleWithdrawalAppointment le déplace ; withdrawal.scheduled), le terrain la prépare (withdrawal.ready quand elle attend au quai) et enregistre la remise (withdrawal.handed_over, avec une raison d’écart par ligne si elle est partielle). Le bon de retrait est signé sur papier. apiWithdrawals(status) liste les demandes de la boutique avec leur appointment. Les quantités au-dessus de availableQty sont refusées : les unités réservées appartiennent aux commandes ouvertes.

Refléter les positions, ne pas les déclarer

apiStockedPositions(updatedSince, facilityCode) est paginé par curseur comme les listes. Interrogez-le au rythme de votre synchronisation de stock et écrivez availableQty dans la boutique ; onHandQty moins reservedQty est ce que la vitrine peut vendre. inbound.received et withdrawal.handed_over sont les moments où les positions bougent ; un webhook de l’une ou l’autre famille est un bon déclencheur de rafraîchissement incrémental.

Exemple

Déclarer un réapprovisionnement

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 }]
  }
}

Créneaux, réservation et déplacement

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
  }
}

Demander un retrait

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 }]
  }
}

Lire le comptage de l’entrepôt

GraphQL
query StockedPositions($cursor: String, $updatedSince: DateTime) {
  apiStockedPositions(cursor: $cursor, limit: 200, updatedSince: $updatedSince) {
    items { sku offerId facilityCode onHandQty reservedQty availableQty updatedAt }
    nextCursor
  }
}

Avec le SDK Node

@wamia/connect-sdk fournit des méthodes typées pour chaque opération ci-dessus, idempotentes là où l’API l’est, plus isInboundEvent et isWithdrawalEvent pour typer les charges utiles des webhooks.

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']),
);

Statuts des réceptions

ÉtatSignification
DRAFTDéclarée, sans rendez-vous (ou le dernier a été manqué).
SCHEDULEDUn créneau de quai est réservé ; scheduledAt et appointmentId sont posés.
PICKED_UPWAMIA_PICKUP seulement : le chauffeur a collecté la marchandise.
DROPPED_OFFLa marchandise est au quai et attend le comptage.
RECEIVINGL’entrepôt compte ; receivedUnits monte ligne par ligne.
RECEIVEDComptée ; les écarts éventuels attendent la réponse de la boutique.
CLOSEDLe stock est vendable ; les positions ont bougé.
CANCELLEDAnnulée par la boutique avant l’arrivée ou par l’entrepôt.

Statuts des retraits

ÉtatSignification
REQUESTEDEn attente de décision de l’entrepôt.
APPROVEDAcceptée ; les unités sont réservées. Réservez un créneau de quai.
SCHEDULEDUn créneau de quai est réservé ; appointment le décrit.
PREPARINGL’entrepôt prépare les unités.
READYPréparée et en attente au quai pour la boutique.
ARRIVEDLa boutique est au quai ; remise en cours.
CLOSEDRemise en totalité à shopRepresentative ; les positions ont bougé.
HANDED_OVERRemise en partie ; chaque ligne porte un shortfallReason.
NO_SHOWLe créneau est passé sans la boutique ; re-réservez avant holdUntil (stagedHoldHours).
REJECTEDRefusée ; lisez decisionNote.
CANCELLEDAnnulée par la boutique avant la préparation, par l’entrepôt, ou après l’expiration de la garde.

Événements

TypeSignification
inbound.declaredUne expédition a été déclarée (shipmentId).
inbound.scheduledPremier rendez-vous réservé (appointmentId).
inbound.rescheduledDéplacée vers un autre créneau (previousAppointmentId).
inbound.slot_closedL’entrepôt a fermé sur le créneau réservé ; re-réservez (appointmentId).
inbound.arrivedPointée au quai.
inbound.receivedComptée ; relisez les positions.
inbound.discrepancyUn manquant, un surplus ou une casse attend une réponse.
inbound.no_showLe créneau est passé sans arrivée ; re-réservez.
inbound.cancelledAnnulée.
withdrawal.requestedUn retrait a été demandé (withdrawalId).
withdrawal.decidedApprouvé ou refusé ; relisez la demande pour status et decisionNote.
withdrawal.scheduledUn créneau de quai a été réservé ou déplacé (appointmentId).
withdrawal.slot_closedL’entrepôt a fermé sur le créneau réservé ; re-réservez (appointmentId).
withdrawal.readyPréparé et en attente au quai.
withdrawal.handed_overRemis (en totalité ou avec un écart) ; relisez les positions.
withdrawal.no_showLe créneau est passé sans la boutique ; re-réservez.
withdrawal.cancelledAnnulé ; les unités sont libérées.