Vorgio

POST /v1/checkouts

Der einzelne Aufruf, den Ihr Shop-Server pro Checkout durchführt. Er fasst drei Dinge zu einer atomaren, idempotenten Operation zusammen:

  1. Find-or-Create des Clients in Ihrem Vorgio-Team, gekeyt auf client.external_id.
  2. Erstellen einer nummerierten Rechnung für diesen Client mit den von Ihnen übergebenen Positionen und dem Steuersatz.
  3. 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 aus external_id ab (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 und tax_rate.
  • invoice.billing_date — auf heute gesetzt, falls Sie es nicht übergeben (Checkouts gehen sofort raus).
  • invoice.due_atbilling_date + due_offset_days, falls Sie es nicht übergeben.
  • position.amount_cents für stundenbasierte Zeilen — hours × client.rate.
  • send.subject / send.body — wenn nicht angegeben, fällt Vorgio auf seine lokalisierten Default-Templates zurück (Sprache aus client.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.id eine 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.999999999999 in IEEE 754. Verwenden Sie immer int(round(value * 100)) (Python) oder (int) round($value * 100) (PHP).
  • send.body ist 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/checkouts nicht unterstützt. Dieser Endpoint ist für einmalige Shop-Checkouts. Für wiederkehrende Abrechnung verwenden Sie POST /v1/invoices direkt mit every: monthly etc. — siehe /api-reference.