Vorgio

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:

  1. Ihr Server erzeugt einen kurzlebigen Checkout-Intent (mit dem geheimen Token, wie bei jedem anderen Server-zu-Server-Aufruf).
  2. Vorgio liefert ein einmalig nutzbares client_secret zurück, das an genau diesen Intent gebunden ist.
  3. Ihr Shop reicht das client_secret an den Browser weiter.
  4. Das browserseitige Widget ruft POST /v1/checkout-intents/{id}/confirm nur mit dem client_secret auf (kein Vorgio-Token). Bei Erfolg führt Vorgio denselben Flow wie /v1/checkouts aus (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 nur client_secret weiterreichen.
  • 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 onConfirmed auf und leitet 1,5 s später auf returnUrl weiter, 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_secret ist insgesamt 64 Zeichen lang (cs_ + 26-Zeichen-ULID + _secret_ + 27 base62-Zeichen), allein der Zufallsteil trägt ca. 161 Bit Entropie. Es ist mit UNIQUE indiziert 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_secret passt 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-intents gelö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.