POST /v1/checkouts
Der einzelne Aufruf, den Ihr Shop-Server pro Checkout durchführt. Er fasst drei Dinge zu einer atomaren, idempotenten Operation zusammen:
- Find-or-Create des Clients in Ihrem Vorgio-Team, gekeyt auf
client.external_id. - Erstellen einer nummerierten Rechnung für diesen Client mit den von Ihnen übergebenen Positionen und dem Steuersatz.
- Einreihen der Rechnungs-E-Mail an die Adresse des Clients (gerendert in der Sprache des Clients).
Sobald die DB-Transaktion committed ist, dispatcht Vorgio das InvoiceSent-Event, welches Ihre invoice.sent-Webhook-Subscriber auslöst (siehe Webhooks).
Endpoint-Struktur, Validierungsregeln und exakte Response-Keys werden automatisch generiert und stehen unter /api-reference. Diese Seite ist die Erzählung — das Warum, die ausgearbeiteten Beispiele, die Fallstricke. Nutzen Sie beide nebeneinander.
#Authentifizierung und Header
| Header | Erforderlich | Hinweise |
|---|---|---|
Authorization: Bearer <token> |
ja | Token muss die Ability checkouts:write besitzen. |
Content-Type: application/json |
ja | |
Idempotency-Key: <unique-string> |
ja | Ein stabiler Identifier für diesen Checkout. Best Practice: aus der Order-ID des Shops ableiten, z. B. wc-order-1234. Die Wiederverwendung eines Keys mit demselben Body gibt die ursprüngliche Response zurück, mit Idempotency-Replay: true in den Headern. Die Wiederverwendung mit einem abweichenden Body führt zu 409 Conflict. |
#Request-Body
Drei erforderliche verschachtelte Objekte (client, invoice, send) plus optionale metadata:
1{
2 "client": {
3 "external_id": "shop_customer_42", // recommended — without it every call creates a new client
4 "name": "Erika Mustermann",
5 "name_addition": null, // optional 2nd line
6 "address": "Musterstraße 1",
7 "address_addition": null, // optional 2nd address line
8 "zip": "10115",
9 "city": "Berlin",
10 "country": "DE", // ISO 3166-1 alpha-2
11 "email": "erika@example.com", // required to send the invoice email
12 "email_cc": [], // optional list
13 "language": "de", // de | en — drives the email + PDF language
14 "rate": 0, // hourly rate in cents (0 if you don't bill by the hour)
15 "vat": 19.0, // default VAT for this client (the per-invoice tax_rate overrides)
16 "default_position_mode": "fixed" // hourly | fixed
17 },
18 "invoice": {
19 "tax_rate": 19.0, // VAT % applied to this invoice
20 "billing_date": "2026-05-10", // optional, defaults to today
21 "due_offset_days": 14, // due_at = billing_date + this
22 "subject": "Order #1234", // optional — appears at the top of the PDF
23 "description": null, // optional long text above the positions
24 "note": null, // optional footer below totals
25 "positions": [
26 {
27 "id": "0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e",
28 "date": "2026-05-10",
29 "mode": "fixed", // fixed → amount_cents required
30 "description": "1× Widget Pro",
31 "amount_cents": 4990 // €49.90 in cents
32 },
33 {
34 "id": "0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f40",
35 "date": "2026-05-10",
36 "mode": "hourly", // hourly → hours required, amount derived from client.rate
37 "description": "Setup support",
38 "hours": 0.5
39 }
40 ]
41 },
42 "send": { // optional — wenn Sie alles weglassen, nutzt Vorgio seine lokalisierten Default-Templates
43 "subject": "Your invoice from Acme Shop", // optional
44 "body": "Hi {client.name}, your invoice {invoice.number} is attached. — Acme Shop", // optional
45 "cc": [] // optional CC list
46 },
47 "metadata": { // optional, free-form, ≤ 16KB JSON
48 "shop_order_id": "1234"
49 }
50}
#Was Vorgio für Sie berechnet
Folgendes übergeben Sie nicht (und sollten es auch nicht versuchen):
client_id— Vorgio leitet dies ausexternal_idab (Find-or-Create innerhalb Ihres Teams).invoice.number— lückenlos fortlaufend pro(team, type)gemäß den Vorgaben der deutschen Finanzbehörden („fortlaufende Nummer").invoice.amount_net,tax_amount,amount_total— berechnet aus den Positionen undtax_rate.invoice.billing_date— auf heute gesetzt, falls Sie es nicht übergeben (Checkouts gehen sofort raus).invoice.due_at—billing_date + due_offset_days, falls Sie es nicht übergeben.position.amount_centsfür stundenbasierte Zeilen —hours × client.rate.send.subject/send.body— wenn nicht angegeben, fällt Vorgio auf seine lokalisierten Default-Templates zurück (Sprache ausclient.language). Übergeben Sie nur das, was Sie überschreiben möchten.
#Body-Templating in send.body
Der send.body-String unterstützt eine kleine Menge an Platzhaltern, die zum Versandzeitpunkt pro Empfänger ersetzt werden:
{client.name}→ der Name des Clients{invoice.number}→ die ausgestellte Rechnungsnummer (z. B.2026-0042){invoice.amount_total}→ formatierter Gesamtbetrag in der Locale des Clients (z. B.59,38 €){invoice.due_at}→ Fälligkeitsdatum in der Locale des Clients (z. B.24.05.2026)
#Response
201 Created mit:
1{
2 "data": {
3 "client_id": "0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e",
4 "invoice": {
5 "id": "0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f99",
6 "number": "1",
7 "amount_total": 5938,
8 "metadata": { "shop_order_id": "1234" }
9 // … full InvoiceResource shape; see /api-reference
10 },
11 "mail_event_id": "12345"
12 }
13}
Plus einen Location-Header, der für nachgelagerte Reads auf GET /v1/invoices/{id} zeigt.
#Ausgearbeitete Beispiele
#PHP (rohes HTTP, ohne SDK)
1$response = (new GuzzleHttp\Client)->post('https://app.vorgio.example/api/v1/checkouts', [
2 'headers' => [
3 'Authorization' => 'Bearer ' . getenv('VORGIO_TOKEN'),
4 'Idempotency-Key' => 'wc-order-' . $order->get_id(),
5 'Accept' => 'application/json',
6 ],
7 'json' => [
8 'client' => [
9 'external_id' => 'wc_customer_' . $order->get_customer_id(),
10 'name' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
11 'address' => $order->get_billing_address_1(),
12 'zip' => $order->get_billing_postcode(),
13 'city' => $order->get_billing_city(),
14 'country' => $order->get_billing_country(),
15 'email' => $order->get_billing_email(),
16 'language' => 'de',
17 'rate' => 0,
18 'vat' => 19.0,
19 'default_position_mode' => 'fixed',
20 ],
21 'invoice' => [
22 'tax_rate' => 19.0,
23 'due_offset_days' => 14,
24 'subject' => 'Bestellung #' . $order->get_id(),
25 'positions' => array_map(function ($item) {
26 return [
27 'id' => Ramsey\Uuid\Uuid::uuid4()->toString(),
28 'date' => date('Y-m-d'),
29 'mode' => 'fixed',
30 'description' => $item->get_name() . ' × ' . $item->get_quantity(),
31 'amount_cents' => (int) round($item->get_total() * 100),
32 ];
33 }, $order->get_items()),
34 ],
35 'send' => [
36 'subject' => 'Ihre Rechnung — Bestellung #' . $order->get_id(),
37 'body' => "Hallo {client.name},\n\nanbei Ihre Rechnung {invoice.number}.\n\n— Acme Shop",
38 ],
39 'metadata' => [
40 'shop_order_id' => (string) $order->get_id(),
41 ],
42 ],
43]);
#Node.js / TypeScript
1import { randomUUID } from 'node:crypto';
2
3const response = await fetch('https://app.vorgio.example/api/v1/checkouts', {
4 method: 'POST',
5 headers: {
6 'Authorization': `Bearer ${process.env.VORGIO_TOKEN}`,
7 'Content-Type': 'application/json',
8 'Idempotency-Key': `order-${order.id}-checkout`,
9 },
10 body: JSON.stringify({
11 client: { /* … */ },
12 invoice: {
13 tax_rate: 19,
14 due_offset_days: 14,
15 positions: order.lineItems.map(item => ({
16 id: randomUUID(),
17 date: new Date().toISOString().slice(0, 10),
18 mode: 'fixed',
19 description: `${item.quantity}× ${item.name}`,
20 amount_cents: Math.round(item.totalCents),
21 })),
22 },
23 send: { subject: 'Your invoice', body: 'Hi {client.name}, …' },
24 metadata: { shop_order_id: String(order.id) },
25 }),
26});
27
28if (!response.ok) {
29 const err = await response.json();
30 throw new Error(`Vorgio ${response.status}: ${err.title} — ${err.detail}`);
31}
#Python
1import os, requests, uuid, datetime as dt
2
3resp = requests.post(
4 "https://app.vorgio.example/api/v1/checkouts",
5 headers={
6 "Authorization": f"Bearer {os.environ['VORGIO_TOKEN']}",
7 "Idempotency-Key": f"order-{order.id}-checkout",
8 },
9 json={
10 "client": { ... },
11 "invoice": {
12 "tax_rate": 19.0,
13 "due_offset_days": 14,
14 "positions": [
15 {
16 "id": str(uuid.uuid4()),
17 "date": dt.date.today().isoformat(),
18 "mode": "fixed",
19 "description": f"{item.qty}× {item.name}",
20 "amount_cents": int(round(item.total_cents)),
21 }
22 for item in order.line_items
23 ],
24 },
25 "send": {"subject": "Your invoice", "body": "Hi {client.name}, …"},
26 "metadata": {"shop_order_id": str(order.id)},
27 },
28 timeout=15,
29)
30resp.raise_for_status()
#Häufige Fallstricke
- Übergeben Sie immer
client.external_id. Ohne diese legt jeder Aufruf einen brandneuen Vorgio-Client an, selbst wenn dieselbe Person gestern bereits bestellt hat — Ihre Client-Liste bläht sich auf. Siehe Clients & external_id. - Erzeugen Sie für jede
position.ideine frische UUID. Sie müssen nicht UUIDv7 / sortierbar sein —uuid4()reicht aus. Sie werden auf der Rechnung gespeichert und vom UI zur Bearbeitung verwendet. - Übergeben Sie Cent-Beträge nicht als Floats.
49.90 * 100 = 4989.999999999999in IEEE 754. Verwenden Sie immerint(round(value * 100))(Python) oder(int) round($value * 100)(PHP). send.bodyist Plain Text, kein HTML. Das Mail-Template umschließt den Inhalt. Wenn Sie Formatierung benötigen, nutzen Sie Leerzeilen für Absätze.- Idempotency-Keys sind pro Team gescopt und leben 24 h. Die Wiederverwendung desselben Keys im selben Team innerhalb von 24 h nach dem Original-Request spielt die Response erneut ab, selbst wenn die ursprünglichen Seiteneffekte längst abgeschlossen sind. Wählen Sie Keys, die pro logischem Checkout stabil sind (eine Order-ID ist perfekt), und recyceln Sie sie nicht.
- Wiederkehrende Rechnungen werden auf
/v1/checkoutsnicht unterstützt. Dieser Endpoint ist für einmalige Shop-Checkouts. Für wiederkehrende Abrechnung verwenden SiePOST /v1/invoicesdirekt mitevery: monthlyetc. — siehe /api-reference.