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.
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. |
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_datevolgt de cutoff van 09:00 (Europe/Amsterdam) en werkdagen- Bedragen in eurocenten;
unit_priceis jouw prijs, inclusief staffel of prijsafspraak price_sourceistierofagreementvat_ratehangt 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
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"
}
}
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.
emailenphonezijn 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.
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.
{
"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.receivedorder.on_holdorder.in_productionorder.shippedorder.deliveredorder.cancelledbalance.lowbalance.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.
$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,
);
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.
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
| 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_…"
}