Laravel + Picqer: een WMS API-integratie bouwen die niet omvalt
Terug naar blog

Laravel + Picqer: een WMS API-integratie bouwen die niet omvalt

AuthorRuthger Idema
9 september 20268 min leestijd

Een handmatige order overtypen kost gemiddeld 90 seconden. Bij 200 orders per dag is dat vijf uur typewerk. Plus de typefouten.

Laravel + Picqer: een WMS API-integratie bouwen die niet omvalt

Een handmatige order overtypen kost gemiddeld 90 seconden. Bij 200 orders per dag is dat vijf uur typewerk. Plus de typefouten. Wij koppelen webshops aan Picqer met Laravel, zodat die vijf uur verdwijnt en je voorraad in real time klopt.

Picqer is een Nederlands WMS (warehouse management system) dat veel B2C- en B2B-shops gebruiken voor picken, packen en verzenden. De REST API is degelijk, maar de integratie staat of valt bij de details: rate limits, webhook-verificatie en retries. Hier laten we zien hoe we dat in Laravel bouwen.

Wat je precies koppelt

Een Picqer-integratie heeft vier datastromen. Verwar ze niet, want ze hebben elk een ander faalprofiel.

  • Order push: order uit je shop naar Picqer (richting magazijn).
  • Voorraad terug: stockniveaus van Picqer naar je shop.
  • Pakbon en tracking terug: verzendstatus en track & trace naar klant.
  • Producten sync: SKU's en barcodes synchroon houden.

De order push is je kritieke pad. Gaat die fout, dan wordt er niet verzonden. Voorraad en tracking zijn belangrijk, maar een minuut vertraging is geen ramp. Bouw je foutafhandeling rond die prioriteiten.

De API-client: begin met een fatsoenlijke basis

Picqer gebruikt HTTP Basic Auth: je API-key als username, wachtwoord leeg. De base URL is je subdomein plus /api/v1/. Geen OAuth-gedoe.

Zet de client niet rechtstreeks in je controllers. Maak een dedicated service die je in AppServiceProvider registreert. Dan kun je hem mocken in tests en zit alle Picqer-logica op één plek.

php
class PicqerClient
{
    public function __construct(
        private readonly string $subdomain,
        private readonly string $apiKey,
    ) {}

    private function request(): PendingRequest
    {
        return Http::baseUrl("https://{$this->subdomain}.picqer.com/api/v1/")
            ->withBasicAuth($this->apiKey, '')
            ->withHeaders(['User-Agent' => 'Coding.nl ([email protected])'])
            ->timeout(15)
            ->retry(3, 200, throw: false);
    }

    public function createOrder(array $payload): Response
    {
        return $this->request()->post('orders', $payload);
    }
}

Die User-Agent is geen detail. Picqer wil een identificeerbare agent met contactgegevens, en zonder krijg je sneller throttling. De retry(3, 200) vangt netwerkhikjes op — maar dat is niet je echte retry-strategie. Daar komen we op terug.

Dit soort service-architectuur bouwen wij standaard in Laravel maatwerk projecten. Eén integratielaag, getest, vervangbaar.

Orders pushen: idempotent of het gaat fout

De grootste fout die wij in bestaande koppelingen zien: dubbele orders in Picqer. Oorzaak is bijna altijd een retry zonder idempotency.

Scenario: je pusht een order, Picqer maakt hem aan, maar het antwoord gaat verloren door een timeout. Je code denkt "mislukt" en probeert opnieuw. Resultaat: twee orders, twee keer verzenden.

Los dit op met het reference-veld. Vul daar je eigen unieke order-ID in. Voor je een order aanmaakt, check je of er al een order met die reference bestaat.

php
public function pushOrder(ShopOrder $order): void
{
    $existing = $this->client->findOrderByReference($order->id);

    if ($existing !== null) {
        $order->update(['picqer_id' => $existing['idorder']]);
        return;
    }

    $response = $this->client->createOrder([
        'reference'    => (string) $order->id,
        'deliveryname' => $order->shipping_name,
        'emailaddress' => $order->email,
        'products'     => $this->mapProductLines($order),
    ]);

    $order->update(['picqer_id' => $response->json('idorder')]);
}

Zo blijft een dubbele push onschadelijk. Dit is geen luxe maar de basis. Zonder idempotency mag je geen automatische retries draaien, en zonder retries valt je koppeling om bij de eerste storing.

Map productregels op productcode (de SKU). Bestaat een SKU niet in Picqer, dan weiger je de order met een duidelijke foutmelding in plaats van een halve order door te zetten.

Voorraad en pakbon terughalen

Voorraad haal je niet op per product in een loop. Dat zijn duizenden calls en je rate limit is binnen een minuut op. Gebruik de bulk-endpoints met paginering.

Picqer pagineert op 100 records per call met een offset-parameter. Loop door tot je een lege response krijgt.

php
public function syncStock(): void
{
    $offset = 0;

    do {
        $batch = $this->client->getStock($offset);

        foreach ($batch as $item) {
            Product::where('sku', $item['productcode'])
                ->update(['stock' => $item['stock'][0]['freestock'] ?? 0]);
        }

        $offset += 100;
    } while (count($batch) === 100);
}

Let op freestock, niet stock. freestock is de voorraad minus wat al gereserveerd is voor open orders. Dat is het getal dat je in je shop wilt tonen, anders verkoop je dingen die al weg zijn.

De pakbon (packing slip) en tracking komen binnen via picklists. Een order krijgt een picklist, die wordt afgewerkt, en bij verzending komt de track & trace beschikbaar. Die hoef je niet te pollen — daar zijn webhooks voor.

Webhooks: stop met pollen

Pollen op orderstatus is verspilling. Je doet duizenden calls om te ontdekken dat er niks veranderd is. Picqer stuurt webhooks bij events: orders.status_changed, picklists.closed, products.free_stock_changed.

Registreer een webhook via de API of in de Picqer-interface, met een endpoint op je Laravel-app. Drie dingen zijn niet onderhandelbaar.

1. Verifieer de signature. Picqer stuurt een X-Picqer-Signature header: een HMAC-SHA256 van de body met je webhook-secret. Controleer die altijd. Zonder verificatie kan iedereen je endpoint voeden met nepdata.
php
public function handle(Request $request)
{
    $signature = base64_encode(
        hash_hmac('sha256', $request->getContent(), config('picqer.webhook_secret'), true)
    );

    if (! hash_equals($signature, $request->header('X-Picqer-Signature', ''))) {
        abort(403);
    }

    ProcessPicqerWebhook::dispatch($request->json()->all());

    return response()->noContent();
}
2. Antwoord snel. Doe geen verwerking in de request. Zet de payload op een queue en geef direct een 2xx terug. Duurt je response te lang, dan beschouwt Picqer hem als mislukt en stuurt opnieuw. 3. Verwerk idempotent. Webhooks kunnen dubbel binnenkomen. Elk event heeft een uniek ID — sla verwerkte ID's op en sla duplicaten over.

Rate limits: 500 per minuut, en hoe je eronder blijft

Picqer staat ongeveer 500 requests per minuut toe. Zit je eroverheen, dan krijg je een 429 Too Many Requests met een Retry-After header. Negeer die header niet.

De fout die wij vaak terugzien: een sync-job die zo hard mogelijk loopt en na 30 seconden tegen de limiet knalt. Bouw throttling in vanaf dag één.

Laravel heeft hier ingebouwde middleware voor. Gebruik Redis::throttle of de RateLimited job middleware om je calls te spreiden.

php
public function middleware(): array
{
    return [(new RateLimited('picqer'))];
}

En definieer de limiter ruim onder het maximum:

php
RateLimiter::for('picqer', fn () => Limit::perMinute(400));

400 in plaats van 500 geeft je marge voor webhooks en handmatige acties die óók tegen dezelfde limiet aantikken. Eén gedeelde teller voor je hele applicatie, niet per job.

Komt er toch een 429 doorheen, respecteer dan Retry-After:

php
if ($response->status() === 429) {
    $this->release((int) $response->header('Retry-After', 60));
    return;
}
release() zet de job terug op de queue met een delay. Geen busy-wait, geen verspilde worker.

Retries en dead letters: faal voorspelbaar

Netwerk faalt. API's gaan plat. De vraag is niet óf, maar hoe je systeem reageert. Onze regel: tijdelijke fouten retry je, permanente fouten niet.

StatuscodeTypeActie
429Rate limitRelease met Retry-After
500, 502, 503ServerRetry met backoff
400, 422ValidatieNiet retryen, loggen
401, 403AuthAlarmeren, niet retryen
TimeoutNetwerkRetry met backoff

Een 422 betekent dat je payload fout is. Tien keer opnieuw sturen lost dat niet op — het verstopt alleen je queue. Stuur die naar je failed jobs-tabel en alarmeer.

Configureer exponentiële backoff op je jobs:

php
public int $tries = 5;

public function backoff(): array
{
    return [10, 30, 60, 300, 600];
}

Wat na vijf pogingen nog faalt, belandt in failed_jobs. Monitor die tabel. Een groeiende failed-jobs-tabel is je vroegste signaal dat er iets structureel mis is — een veranderde SKU-structuur, een verlopen key, een Picqer-storing. Zet er een alert op, geen mens die handmatig kijkt.

Wanneer Picqer níét de juiste keuze is

Eerlijk: niet elke shop heeft een WMS nodig. Verstuur je 15 orders per dag vanuit één voorraadlocatie? Dan is Picqer overkill en is de integratie duurder dan het oplevert.

Picqer loont als je:

  • honderden orders per dag draait;
  • meerdere verkoopkanalen op één voorraad koppelt;
  • picken met scanners en barcodes wilt;
  • groeit en handmatig verzenden niet meeschaalt.

Draai je een Magento of Shopify shop met serieus volume, dan verdient de koppeling zich snel terug. Bij lage volumes raden we het af — dat is geld besparen, niet uitgeven.

Twijfel je of een Picqer-koppeling in jouw situatie loont? Neem contact op, dan rekenen we het concreet door op basis van je ordervolume en kanalen.

Veelgestelde vragen

Hoe lang duurt het bouwen van een Picqer-koppeling?

Een standaard koppeling met order push, voorraad terug, webhooks en tracking bouwen wij doorgaans in één tot twee weken. Maatwerk zoals multi-warehouse logica, complexe productmapping of B2B-prijsregels loopt op naar drie of vier weken. De grootste tijdpost is testen met echte orderdata, niet het schrijven van de code.

Wat is de rate limit van de Picqer API precies?

Picqer hanteert ongeveer 500 requests per minuut per account. Bij overschrijding krijg je een 429-response met een Retry-After header. Wij configureren onze koppelingen op maximaal 400 per minuut, zodat webhooks en handmatige acties binnen dezelfde limiet niet voor problemen zorgen.

Kan ik dubbele orders in Picqer voorkomen?

Ja, door het reference-veld te gebruiken voor je eigen order-ID en vóór elke push te controleren of die reference al bestaat. Zo blijft een retry na een timeout onschadelijk. Zonder deze idempotency mag je geen automatische retries draaien, want dan riskeer je dubbele verzendingen.

Moet ik webhooks of polling gebruiken voor orderstatus?

Webhooks. Pollen kost onnodig veel API-calls en loopt snel tegen de rate limit aan. Picqer stuurt events zoals picklists.closed en orders.status_changed direct naar je endpoint. Verifieer altijd de X-Picqer-Signature en verwerk de payload op een queue, niet in de request zelf.

Werkt een Picqer-koppeling ook met Magento en Shopify?

Ja. De integratielaag in Laravel praat met de Picqer API en met de API van je shopplatform. Voor Magento en Shopify bouwen wij die brug op maat, zodat orders, voorraad en tracking automatisch synchroon lopen tussen je shop en je magazijn.

Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check