Naar de inhoud
AAN DE SLAG

Quickstart

Zet in drie calls een sandbox-order in beweging: vraag een quote op, maak de order aan en luister naar de webhooks. Alles werkt precies hetzelfde met een live-key.

BASE URL https://fivefoldprints.com/api/v1

Liever eerst klikken dan typen? In de API-playground probeer je elke call met je sandbox-key en volg je je testorder live.

Authenticatie & keys

Stuur je geheime key als Bearer-token mee bij elk request: Authorization: Bearer ffp_test_…. Het voorvoegsel van de key bepaalt waar de order heen gaat. Keys horen op je server; zet ze nooit in een browser of app.

Je keys beheer en vernieuw je in het portaal onder API-keys. Per key mag je 120 requests per minuut doen; daarboven krijg je een 429.

Voorvoegsel Omgeving Wat er gebeurt
ffp_test_ Sandbox Gesimuleerde tijdlijn, echte webhooks. Er wordt niets geprint of afgeschreven.
ffp_live_ Live Geprint, verzonden en betaald uit je saldo.
STAP 1

Vraag een quote op

Vraag op wat een mandje kost en wanneer het aankomt. Gebruik het in je checkout om een echte leverdatum te tonen. Quotes zijn gratis en maken niets aan.

  • production_date volgt de cutoff van 09:00 (Europe/Amsterdam) en werkdagen
  • Bedragen in eurocenten; unit_price is jouw prijs, inclusief staffel of prijsafspraak
  • price_source is tier of agreement
  • vat_rate hangt af van waar de order heen gaat: binnen Nederland altijd 21%, ook voor buitenlandse bedrijven. 0% (btw verlegd) alleen bij levering naar een ander EU-land met een gecontroleerd btw-nummer uit een ander EU-land
POST /v1/quotes
curl https://fivefoldprints.com/api/v1/quotes \
  -H "Authorization: Bearer ffp_test_…" \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "product": "mug-11oz-white", "quantity": 3 }],
       "country": "NL" }'

# 200 OK
{
  "object": "quote",
  "items": [{
    "product": "mug-11oz-white",
    "quantity": 3,
    "unit_price": 395,
    "price_source": "tier"
  }],
  "costs": {
    "items": 1185,
    "shipping": 795,
    "vat": 416,
    "vat_rate": 21,
    "total": 2396,
    "currency": "eur"
  },
  "production_date": "2026-10-12",
  "estimated_delivery": {
    "earliest": "2026-10-13",
    "latest": "2026-10-14"
  }
}
STAP 2

Maak de order aan

Stuur de items, een publieke URL naar elk printbestand en het adres van je klant. Voeg een Idempotency-Key toe, zodat een retry na een time-out dezelfde order teruggeeft in plaats van een tweede te printen.

external_id
Je eigen ordernummer. Doorzoekbaar in het portaal.
items[]
Product, aantal (1–100) en print_file_url (HTTPS).
recipient
Naam en adres. email en phone zijn optioneel.
sender_name
Optioneel. Overschrijft de merknaam op het label voor deze order (max. 35 tekens).

Live-orders worden direct van je saldo afgeschreven. Is je saldo te laag, dan gaat de order in de wacht met hold_reason: insufficient_balance en loopt hij door zodra je saldo is aangevuld.

POST /v1/orders
STAP 3

Luister naar webhooks

Voeg een endpoint toe in het portaal of via POST /v1/webhook_endpoints. Bij elke statuswijziging sturen we een POST met de volledige order in data. Antwoordt je server niet binnen 10 seconden met een 2xx, dan proberen we het 8 keer opnieuw met oplopende tussenpozen, verspreid over ongeveer 69 uur.

Sandbox- en live-endpoints zijn gescheiden: een sandbox-endpoint krijgt alleen sandbox-events.

Fivefold-Signature: t=1791560400,v1=5257a869e7ec…
{
  "id": "evt_3KpQ8sWm2N7vYb1c",
  "type": "order.shipped",
  "mode": "sandbox",
  "created_at": "2026-10-12T15:05:11Z",
  "data": {
    "id": "ord_7Hq2LmX9pK",
    "object": "order",
    "external_id": "1042",
    "status": "shipped",
    "tracking": {
      "carrier": "sandbox",
      "number": "3SFFP0012345678",
      "url": null
    },
    …
  }
}

Eventtypes

Kies per endpoint welke events je wilt ontvangen. Met order.* krijg je alle order-events.

  • order.received
  • order.on_hold
  • order.in_production
  • order.shipped
  • order.delivered
  • order.cancelled
  • balance.low
  • balance.topped_up

balance.low komt zodra je saldo onder je drempel zakt; balance.topped_up na elke geslaagde aanvulling. Beide sturen balance in eurocenten mee.

Handtekening controleren

Controleer elke call voordat je hem verwerkt. Splits de header Fivefold-Signature in t (Unix-tijd) en v1. Bereken een HMAC-SHA256 van t + "." + body met de signing secret van je endpoint (whsec_…) en vergelijk die in constante tijd met v1.

Weiger events waarvan t meer dan 5 minuten afwijkt, zodat een onderschepte call niet opnieuw kan worden afgespeeld. Gebruik de ruwe body, niet een opnieuw gecodeerde versie.

PHP · Laravel
$header = $request->header('Fivefold-Signature', '');
parse_str(str_replace(',', '&', $header), $parts);

$expected = hash_hmac('sha256', $parts['t'].'.'.$request->getContent(), $secret);

abort_unless(
    hash_equals($expected, $parts['v1'] ?? '')
        && abs(time() - (int) $parts['t']) < 300,
    400,
);
Node.js
import crypto from 'node:crypto';

const parts = Object.fromEntries(
  header.split(',').map((part) => part.split('=')),
);
const expected = crypto
  .createHmac('sha256', secret)
  .update(`${parts.t}.${rawBody}`)
  .digest('hex');

const valid = expected.length === parts.v1?.length
  && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  && Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;

Orderstatussen

Elke statuswijziging is één webhook (order.received, order.in_production, …). Een order kun je annuleren tot hij om 09:00 in een productiebatch gaat.

received in_production shipped delivered
on_hold Actie nodig. Het veld hold_reason zegt waarom: print_file_resolution_too_low of print_file_rejected (vervang het printbestand in het portaal) of insufficient_balance (vul je saldo aan). Daarna loopt de order door.
cancelled Door jou geannuleerd vóór productie (POST /v1/orders/:id/cancel), of door ons met een reden. Een betaalde order gaat volledig terug naar je saldo.

Idempotency

Stuur bij POST /v1/orders een header Idempotency-Key mee, bijvoorbeeld je eigen ordernummer (max. 255 tekens). Herhaal je het request met dezelfde key en dezelfde body, dan krijg je de oorspronkelijke response terug met de header Idempotent-Replayed: true, en wordt er niets dubbel geprint.

Dezelfde key met een andere body geeft een 409 idempotency_conflict. Keys gelden per account en per omgeving.

Foutcodes

Fouten gebruiken standaard HTTP-codes en geven altijd een machineleesbare code, een leesbare message en bij validatie het field dat niet klopt, plus alle meldingen per veld in errors. Berichten zijn Engelstalig.

# 422 Unprocessable Content
{
  "error": {
    "code": "unsupported_country",
    "message": "We do not ship to this country yet.",
    "field": "recipient.country",
    "errors": {
      "recipient.country": ["We do not ship to this country yet."]
    }
  }
}
Status Code Betekenis
400 invalid_idempotency_key Idempotency-Key is langer dan 255 tekens
401 invalid_api_key Key ontbreekt, is ingetrokken of klopt niet
403 live_mode_locked Betaal- of bedrijfsgegevens ontbreken
404 resource_missing Order of endpoint bestaat niet (of is van een ander account)
409 idempotency_conflict Zelfde key, andere request-body
409 order_not_cancellable Order is al in productie of verder
422 validation_failed Een veld ontbreekt of klopt niet; zie field
422 invalid_address Adres is niet bezorgbaar
422 unsupported_country Daar verzenden we nog niet naartoe
429 rate_limited Te veel requests; zie de header Retry-After

Sandbox-testscenario’s

Sandbox-orders lopen vanzelf door: na 1 min naar in_production, na 3 min naar shipped (met een test-trackingnummer) en na 5 min naar delivered. Met deze waarden test je de foutpaden:

Stuur dit mee Wat er gebeurt
print_file_url met lowres erin De order gaat direct in de wacht met print_file_resolution_too_low. Vervang het bestand in het portaal om hem vrij te geven.
external_id die begint met cancel_ Wij annuleren de order na 1 min, zoals bij een order die we niet kunnen maken.
postal_code 0000 AA 422 invalid_address
country US 422 unsupported_country

Endpoints

Alle endpoints
GET /v1/products Producten met prijzen, staffels en printspecificaties
POST /v1/quotes Prijs en leverdata voor een mandje
POST /v1/orders Order aanmaken
GET /v1/orders Orders ophalen, filter op status of external_id
GET /v1/orders/:id Eén order met tijdlijn en track & trace
POST /v1/orders/:id/cancel Annuleren vóór de productie start
GET /v1/balance Huidig saldo en instellingen voor automatisch aanvullen
GET /v1/webhook_endpoints Webhook-endpoints van deze omgeving
POST /v1/webhook_endpoints Webhook-endpoint toevoegen
DELETE /v1/webhook_endpoints/:id Webhook-endpoint verwijderen

Producten

GET /v1/products geeft elk actief product met de productcode die je in items[].product gebruikt, de basisprijs, de staffels en de eisen aan het printbestand. Haal de lijst op in plaats van productcodes hard te coderen: zo werkt je integratie meteen met nieuwe producten.

{
  "object": "list",
  "data": [{
    "id": "mug-11oz-white",
    "object": "product",
    "unit_price": 395,
    "currency": "eur",
    "price_tiers": [
      { "min_monthly_quantity": 500, "unit_price": 365 }
    ],
    "print_file": {
      "width_mm": 200, "height_mm": 85,
      "width_px": 2362, "height_px": 1004,
      "min_dpi": 150
    },
    …
  }]
}

Orders ophalen

GET /v1/orders geeft je orders van nieuw naar oud, per pagina van limit (standaard 20, max. 100). Filter met status of external_id. Staat er een next_cursor in de response, stuur die dan mee als cursor voor de volgende pagina.

Saldo

GET /v1/balance geeft je beschikbare prepaid saldo en je instellingen voor automatisch aanvullen. Alle bedragen in eurocenten. Aanvullen doe je in het portaal; de API leest alleen.

{
  "object": "balance",
  "available": 21240,
  "currency": "eur",
  "auto_top_up": {
    "enabled": true,
    "threshold": 5000,
    "amount": 10000
  },
  "payment_method": "Visa •••• 4242"
}

Webhook-endpoints

Beheer je endpoints ook vanuit je eigen code. Een endpoint hoort bij de omgeving van de key waarmee je hem aanmaakt. De url moet HTTPS zijn. De signing secret krijg je alleen in de response van het aanmaken terug; bewaar hem direct.

Verwijderen: DELETE /v1/webhook_endpoints/we_42 geeft 204.

curl https://fivefoldprints.com/api/v1/webhook_endpoints \
  -H "Authorization: Bearer ffp_test_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://jouwshop.nl/webhooks/fivefold",
       "events": ["order.*", "balance.low"] }'

# 201 Created
{
  "id": "we_42",
  "object": "webhook_endpoint",
  "url": "https://jouwshop.nl/webhooks/fivefold",
  "events": ["order.*", "balance.low"],
  "enabled": true,
  "secret": "whsec_…"
}