Fulfillment API · v1

Ihr Shop verkauft. Wir fertigen und versenden.

Ihr Kunde lädt bei Ihnen eine CAD-Datei hoch und bezahlt bei Ihnen. Über diese Schnittstelle holen Sie sich den verbindlichen Preis, legen den Auftrag an und bekommen jeden Statuswechsel als signierten Webhook. Ihre Kunden-Mails verschicken Sie selbst, unter Ihrer Marke.

Zur Einbau-Anleitung mit fertigem Code

Grundlagen

Jede Anfrage trägt Authorization: Bearer <Schlüssel>. Ihre Brand bekommt einen Live- und einen Testschlüssel. Testaufträge laufen durch denselben Ablauf, werden aber nie gedruckt, nie berechnet und tauchen nicht in unserer Produktion auf. Alle Webhooks enthalten "test": true.

Beträge sind Cent-Beträge in EUR, netto. wholesale ist, was wir Ihnen berechnen. retail ist der empfohlene Endkundenpreis mit dem Aufschlag, den wir für Ihre Brand hinterlegt haben.

externalOrderId ist pro Brand eindeutig. Senden Sie denselben Auftrag noch einmal, legen wir keinen zweiten an und antworten mit "duplicate": true.

Alle Endpunkte
EndpunktZweck
GET/api/v1/catalogProdukttypen, Materialien und Lieferzeiten Ihrer Brand.
POST/api/v1/filesDatei anmelden. Antwort enthält ein Upload-Token (30 Min.) oder übernimmt eine fileUrl direkt.
GET/api/v1/files/{fileId}Prüfstatus: pending, ready oder invalid, dazu Maße und Volumen.
POST/api/v1/quotesVerbindliches Angebot über eine oder mehrere Dateien, 14 Tage gültig.
GET/api/v1/quotes/{quoteId}Angebot erneut abrufen.
POST/api/v1/ordersAuftrag aus einem Angebot anlegen oder alles in einem Aufruf (Dateien per URL).
GET/api/v1/orders/{externalOrderId}Status aller Positionen inkl. Tracking.
POST/api/v1/orders/{externalOrderId}/cancelStorno, solange noch keine Position in Produktion ist.

Schritt 1

Datei hochladen

Erlaubt sind STEP, IGES, STL, GLB und OBJ bis 200 MB. Sie melden die Datei an und laden sie mit dem Einmal-Token direkt in unseren privaten Speicher. Die Datei läuft also nicht durch Ihren Server.

GLB und OBJ, wie sie KI-Generatoren ausgeben, haben keine verlässliche Maßeinheit. Für diese Formate ist im Angebot targetHeightMm Pflicht (die Datei meldet requiresTargetHeight: true). Wir drehen das Modell auf Z-oben, skalieren gleichmäßig und erzeugen beim Auftrag die fertige STL-Druckdatei in mm. Ein Modell mit offener Hülle (nicht wasserdicht) lehnen wir als invalid ab.

Danach messen wir die Geometrie auf unserer Seite. Maße und Volumen werden nie von Ihnen übernommen. Liegt die Datei schon öffentlich erreichbar vor, schicken Sie stattdessen fileUrl mit, dann holen wir sie selbst ab.

Anmelden
POST /api/v1/files
Authorization: Bearer <API-Schlüssel>
Content-Type: application/json

{ "fileName": "halterung.step" }

201 Created
{
  "file": { "id": "6f1c…", "fileName": "halterung.step", "status": "pending" },
  "upload": {
    "pathname": "partner/…/halterung.step",
    "clientToken": "vercel_blob_client_…",
    "access": "private",
    "expiresAt": "2026-10-06T12:30:00.000Z"
  }
}
Hochladen (Node / Browser)
import { put } from "@vercel/blob"

await put(upload.pathname, datei, {
  access: "private",
  token: upload.clientToken,
})

// danach GET /api/v1/files/{id}, bis status = "ready"

Schritt 2

Preis verbindlich anfragen

Bündeln Sie bis zu 20 Dateien mit Stückzahl in einem Angebot. Wir rechnen mit derselben Formel wie unser eigener Shop. Das Angebot gilt 14 Tage. Diesen Preis zeigen Sie Ihrem Kunden im Checkout.

Gültige Werte für productType und materialId liefert GET /api/v1/catalog.

Angebot
POST /api/v1/quotes

{
  "productType": "industrial-sla",
  "items": [
    { "fileId": "6f1c…", "quantity": 25 },
    { "fileId": "9a2e…", "quantity": 1, "targetHeightMm": 100 }
  ]
}
// targetHeightMm: Höhe (Z) in mm, X/Y skalieren gleichmäßig mit.
// Pflicht für GLB/OBJ, optional für STL/STEP/IGES.

201 Created
{
  "quote": {
    "id": "a83e…",
    "test": false,
    "leadTimeWorkdays": 5,
    "expiresAt": "2026-10-20T10:00:00.000Z",
    "items": [ … ],
    "wholesale": { … },
    "retail": { "positionsNetCents": 24990 },
    "currency": "EUR"
  }
}

Schritt 3

Auftrag anlegen

Sobald Ihr Kunde bezahlt hat, legen Sie den Auftrag mit der quoteId an. Jede Datei wird eine eigene Position und geht in unsere Produktionsplanung. Berechnet wird genau der Angebotspreis.

Für einfache Anbindungen geht es auch in einem Aufruf: Statt quoteId schicken Sie productType und items mit fileUrl, fileName und quantity. Das Angebot wird dabei im Hintergrund erstellt.

Stornieren können Sie mit POST …/cancel, solange keine Position in Produktion ist. Danach antworten wir mit 409.

Auftrag aus Angebot
POST /api/v1/orders

{
  "quoteId": "a83e…",
  "externalOrderId": "SHOP-48213",
  "dueDate": "2026-10-20",
  "customer": {
    "firstName": "Max",
    "lastName": "Mustermann",
    "email": "kunde@example.com",
    "street": "Musterweg 5",
    "zip": "10115",
    "city": "Berlin",
    "country": "DE"
  }
}

Schritt 4

Status per Webhook

Wir rufen Ihre Webhook-URL bei jedem Statuswechsel auf. Darauf versenden Sie Ihre eigenen Kunden-Mails, etwa „Ihr Teil wird gedruckt“ oder die Versandbestätigung mit Tracking. Antwortet Ihr Server nicht mit 2xx, versuchen wir es mit wachsendem Abstand erneut.

Prüfen Sie jede Zustellung mit Ihrem Signatur-Secret. Den Header X-BilTech-Delivery können Sie zum Entdoppeln verwenden.

  • order.status_changedEine Position wechselt den Status (fromStatus, toStatus).
  • order.shippedPaket ist unterwegs (carrier, trackingNumber).
  • order.cancelledPosition storniert.
  • order.rejectedPosition abgelehnt, Ihr Shop sollte erstatten.
  • pingTest-Zustellung aus dem Admin-Bereich.
Zustellung
POST https://ihr-shop.de/webhooks/biltech
X-BilTech-Event: order.shipped
X-BilTech-Delivery: 2b9d…
X-BilTech-Signature: t=1791288000,v1=5c0e…

{
  "test": false,
  "orderNumber": 10428,
  "externalOrderId": "SHOP-48213",
  "position": 1,
  "status": "shipped",
  "carrier": "DHL",
  "trackingNumber": "00340434…",
  "occurredAt": "2026-10-09T14:02:11Z"
}
Signatur prüfen
import { createHmac, timingSafeEqual } from "node:crypto"

const [t, v1] = header.split(",").map((p) => p.split("=")[1])
const expected = createHmac("sha256", WEBHOOK_SECRET)
  .update(`${t}.${rawBody}`)
  .digest("hex")

const ok =
  Math.abs(Date.now() / 1000 - Number(t)) < 300 &&
  timingSafeEqual(Buffer.from(v1), Buffer.from(expected))

Statuswerte je Position

  • validationDatei und Angaben werden geprüft.
  • queuedWartet auf den nächsten Batch.
  • printingLiegt auf einer Bauplatte im Druck.
  • post_processingWaschen, Aushärten, Qualitätskontrolle.
  • shippingLabel erstellt, Paket wird gepackt.
  • shippedVersendet, Tracking-Nummer liegt vor.
  • rejectedAbgelehnt, z. B. nicht druckbare Geometrie.
  • cancelledStorniert.

Sie brauchen Live- und Testschlüssel für Ihren Shop? Wir richten Ihre Brand mit Aufschlag und Webhook ein.

Zugang anfragen