JS-Widget — vorgio.js
Ein browserseitiger Flow, der eine Vorgio-Rechnung bestätigt, ohne jemals Ihren geheimen API-Token zu halten. Setzen Sie ihn ein, wenn:
- Ihr Shop überwiegend clientseitiges JavaScript ist und Sie nicht die gesamte Bestellung über Ihren Server schicken wollen, ODER
- Sie eine gehostete UI zur Checkout-Bestätigung statt einer Eigenentwicklung wollen.
Das Bundle liegt unter vorgio-app/vorgio-js und wird als @vorgio-app/vorgio-js auf npm veröffentlicht. Bis zum öffentlichen Vorgio-Launch ist die kanonische URL cdn.vorgio.example/v1/vorgio.js reserviert — bis dahin pinnen Sie auf den npm-Mirror-CDN (unpkg / jsDelivr) mit einer konkreten Version.
#Warum ein separater Flow
Vorgio-API-Tokens sind geheim. Sie schalten alles in Ihrem Team frei. Würde man einen davon in browserseitiges JavaScript einbauen, könnte jeder, der den Network-Tab Ihrer Seite einsieht, in Ihrem Namen Rechnungen erstellen, Ihre Kundenliste abfragen usw. — Game over.
Das Widget kann daher POST /v1/checkouts nicht direkt aufrufen. Stattdessen:
- Ihr Server erzeugt einen kurzlebigen Checkout-Intent (mit dem geheimen Token, wie bei jedem anderen Server-zu-Server-Aufruf).
- Vorgio liefert ein einmalig nutzbares
client_secretzurück, das an genau diesen Intent gebunden ist. - Ihr Shop reicht das
client_secretan den Browser weiter. - Das browserseitige Widget ruft
POST /v1/checkout-intents/{id}/confirmnur mit demclient_secretauf (kein Vorgio-Token). Bei Erfolg führt Vorgio denselben Flow wie/v1/checkoutsaus (Client finden oder anlegen, Rechnung erstellen, Mail versenden) und gibt die Rechnung zurück.
Das ist dasselbe Muster, das Stripe mit PaymentIntents populär gemacht hat.
#Der Flow
1Shop server Browser Vorgio
2 │ │ │
3 │ POST /v1/checkout-intents │ │
4 │ Bearer act_… │ │
5 │ body: { client, invoice, │ │
6 │ send, return_url, │ │
7 │ metadata } │ │
8 ├────────────────────────────────────────────────────────────▶│
9 │ ← 201 { id, client_secret, │ │
10 │ expires_at } │ │
11 │ │ │
12 │ render the page with the │ │
13 │ client_secret in scope │ │
14 │ │ │
15 │ POST /v1/checkout-intents/{id}/confirm │
16 │ body: { client_secret } │
17 │ ├──────────────────────────▶│
18 │ │ ← 200 { invoice, status, │
19 │ │ return_url } │
20 │ │ │
21 │ optionaler Redirect auf return_url │
22 │ ← invoice.sent Webhook ──────────────────────────────────────┤
23 │ ← invoice.paid Webhook (später, auf Mark-Paid) ──────────────┤
#Schritt 1 — Intent serverseitig erstellen
In Ihrem bestehenden Checkout-Handler rufen Sie dort, wo Sie sonst POST /v1/checkouts aufgerufen hätten, stattdessen Folgendes auf:
1// Node/TypeScript
2const intent = await fetch('https://app.vorgio.example/api/v1/checkout-intents', {
3 method: 'POST',
4 headers: {
5 'Authorization': `Bearer ${process.env.VORGIO_TOKEN}`,
6 'Content-Type': 'application/json',
7 'Idempotency-Key': `order-${order.id}-intent`,
8 },
9 body: JSON.stringify({
10 client: { /* … gleiche Form wie /v1/checkouts */ },
11 invoice: { /* … */ },
12 send: { /* … */ },
13 return_url: `https://shop.example/order/${order.id}/thanks`,
14 metadata: { shop_order_id: String(order.id) },
15 }),
16}).then(r => r.json());
17
18return res.json({
19 clientSecret: intent.data.client_secret, // an den Browser weitergeben
20 expiresAt: intent.data.expires_at,
21});
Die Antwort:
1{
2 "data": {
3 "id": "01jr7q9a4z3sx8b2yhf6e0wk5j",
4 "client_secret": "cs_01jr7q9a4z3sx8b2yhf6e0wk5j_secret_8h3Jf2pQwR…(27 Zeichen)…",
5 "expires_at": "2026-05-11T08:30:00+00:00",
6 "return_url": "https://shop.example/order/1234/thanks"
7 }
8}
Eigenschaften des client_secret:
- Form:
cs_{intent_id}_secret_{27 zufällige Zeichen}(Stripe-Stil). Die Intent-ID ist im Token eingebettet, sodass das JS-Widget die URL-Pfadkomponente aus einem einzigen Token ableiten kann — Ihr HTML muss nurclient_secretweiterreichen. - An einen Intent, ein Team und einen erfolgreichen Confirm gebunden. Ein zweiter Confirm liefert 409.
- Läuft 30 Minuten nach Erstellung ab. Ein abgelaufener Confirm liefert 410.
- Behandeln Sie es wie eine kurzfristige URL — es ist nicht so geheim wie ein API-Token, aber auch keine öffentliche ID.
#Schritt 2 — Browser bestätigt den Intent mit vorgio.js
Bundle laden und Vorgio.checkouts.create(...) aufrufen:
1<script src="https://unpkg.com/@vorgio-app/vorgio-js@0.1.0/dist/vorgio.js"></script>
2<div id="vorgio-checkout"></div>
3<script>
4 Vorgio.checkouts.create({
5 clientSecret: 'cs_…_secret_…', // vom Server
6 container: '#vorgio-checkout',
7 locale: 'de', // de | en — überschreibt client.language für die Widget-UI
8 returnUrl: 'https://shop.example/order/1234/thanks',
9 onConfirmed: ({ invoiceId, invoiceNumber }) => { /* analytics, inventory */ },
10 onError: (error) => { console.error(error); }, // RFC-7807-Form: type, title, status, detail
11 });
12</script>
ESM-Nutzer können stattdessen import { checkouts } from '@vorgio-app/vorgio-js' schreiben und checkouts.create({...}) mit denselben Optionen aufrufen.
#Was das Widget rendert
Eine kleine Karte im Container in einem von vier Zuständen:
- idle — ein lokalisierter „Bestellung abschließen"-Button.
- pending — deaktivierter Button + Spinner, während die Confirm-Anfrage läuft.
- success — grüner Haken + „Rechnung an {email} verschickt". Ruft
onConfirmedauf und leitet 1,5 s später aufreturnUrlweiter, falls gesetzt. - error — rote Banderole mit lokalisierter Meldung und RFC-7807-Details. Ruft
onError(problem)auf.
Das Widget injiziert eigene, gescopte CSS-Regeln pro Seite (Selector-Präfix .vorgio-checkout), sodass es nicht mit den Styles des Shops kollidiert.
#Endpoint direkt aufrufen (ohne Bundle)
Wenn Sie das Bundle nicht laden möchten, können Sie den Confirm-Endpoint direkt aus Ihrem Frontend aufrufen — genau das macht das Widget intern auch:
1<script>
2 async function confirmCheckout(clientSecret, intentId) {
3 const res = await fetch(`https://app.vorgio.example/api/v1/checkout-intents/${intentId}/confirm`, {
4 method: 'POST',
5 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
6 body: JSON.stringify({ client_secret: clientSecret }),
7 });
8
9 if (!res.ok) {
10 const problem = await res.json();
11 throw new Error(problem.detail || problem.title);
12 }
13
14 const { data } = await res.json();
15 if (data.return_url) {
16 window.location.href = data.return_url;
17 }
18 return data;
19 }
20</script>
CORS für /api/v1/checkout-intents/*/confirm ist offen für alle Origins (*). Der Endpoint braucht trotzdem ein gültiges client_secret, um überhaupt etwas zu tun — dazu unten mehr.
#Fehlerfälle
Alle Antworten folgen der RFC-7807-Form (application/problem+json).
| Status | type |
Bedeutung |
|---|---|---|
| 404 | not-found |
Intent-ID existiert nicht, ODER client_secret passt nicht. (Diese Fälle werden bewusst zusammengefasst, damit IDs nicht durch Enumeration erratbar sind.) |
| 409 | conflict |
Intent wurde bereits einmal bestätigt. Ein zweiter Confirm ist immer Fehler. |
| 410 | gone |
Intent ist abgelaufen (30 min seit Erstellung). Erstellen Sie einen neuen. |
| 422 | validation-failed |
client_secret fehlt oder ist leer. |
| 429 | rate-limit-exceeded |
IP-basiertes Rate-Limit erreicht (mind. 60 Anfragen / Minute pro IP). |
#Was das Widget NICHT tut
- Sammelt keine Zahlungsmittel-Daten. Keine Kartenfelder, kein SCA, kein PSD2. Der Kunde zahlt per Banküberweisung über die IBAN auf der Rechnung.
- Sammelt keine Adressdaten. Diese stehen bereits im
client-Block des Intent (den Sie serverseitig setzen). - Verarbeitet kein „Für später speichern" / „Anders bezahlen". Es ist eine einmalige Bestätigungsoberfläche. Möchte der Kunde eine andere Bezahlmethode, kümmert sich die Checkout-UI Ihres Shops darum, nicht das Widget.
- Fragt nicht aktiv den Zahlungsstatus ab. Verwenden Sie serverseitig
invoice.paid-Webhooks.
#Sicherheits-Notizen
- Das Vorgio-API-Token verlässt niemals Ihren Server. Es taucht ausschließlich in Schritt 1 (Intent-Erstellung) auf — niemals im Browser.
- Das
client_secretist insgesamt 64 Zeichen lang (cs_+ 26-Zeichen-ULID +_secret_+ 27 base62-Zeichen), allein der Zufallsteil trägt ca. 161 Bit Entropie. Es ist mitUNIQUEindiziert und wird atomar verbraucht (UPDATE … SET confirmed_at = now() WHERE confirmed_at IS NULL). Race-Bedingungen sind also nicht möglich. - Confirm-Antworten verraten keinen Unterschied zwischen „Intent existiert nicht" und „
client_secretpasst nicht" (beide liefern 404). Damit lassen sich Intent-IDs nicht durch Probieren entdecken. - Abgelaufene Intents (älter als 30 min + 24 h Karenz) werden täglich um 03:15 von
accounting:purge-checkout-intentsgelöscht.
#Quellcode
Die serverseitigen Endpoints stecken in App\Http\Controllers\Api\V1\CheckoutIntentController in diesem Repository. Das Browser-Bundle liegt unter vorgio-app/vorgio-js und erscheint als @vorgio-app/vorgio-js auf npm. Die geplante URL https://cdn.vorgio.example/v1/vorgio.js ist für den öffentlichen Vorgio-Launch reserviert — bis dahin pinnen Sie auf unpkg oder jsDelivr an eine konkrete Version.