Vorgio

Webhooks

Vorgio sendet per POST JSON-Events an Ihren Endpoint, sobald sich der Status einer Rechnung ändert. Ihr Shop nutzt diese Events, um Bestellungen abzuwickeln, Zahlungen abzugleichen und dem Händler den Status anzuzeigen.

#Einrichtung

Öffnen Sie in den Team-Einstellungen Ihres Vorgio-Kontos den Bereich Webhooks. Jedes Team kann mehrere Endpoints registrieren; jeder Endpoint abonniert eine Teilmenge der Event-Typen.

Für jeden Endpoint geben Sie an:

  • URL — muss https:// sein. Vorgio verweigert reines HTTP.
  • Events — wählen Sie aus dem unten stehenden Katalog. Sie können diese Auswahl später anpassen.

Nach dem Anlegen des Endpoints zeigt Vorgio das Signing Secret ein einziges Mal an. Kopieren Sie es — Sie können es nicht erneut einsehen. Falls Sie es verlieren, löschen Sie den Endpoint und legen Sie einen neuen an (die URL bleibt gleich; nur das Secret wird rotiert).

#Den Request entgegennehmen: Framework-Stolperfallen

Vorgio sendet Webhooks als Cross-Origin-POST ohne Session — kein Cookie, kein CSRF-Token, kein _token-Feld. Wenn Ihr Framework CSRF-Schutz standardmäßig auf POST-Routen anwendet (Laravel, Django, Rails, ASP.NET MVC, …), wird der Request mit einer 419/403-Fehler-HTML-Seite abgewiesen, bevor Ihr Handler überhaupt läuft. Vorgio verbucht das als fehlgeschlagene Zustellung und versucht es erneut — Ihr 2xx zurückgebender Handler wird nie erreicht.

Die robuste Lösung ist, die Webhook-Route von vornherein in eine Route-Gruppe ohne Session-/CSRF-Middleware zu legen. Eine punktuelle Ausnahme funktioniert auch, schleppt die Stolperfalle aber durch jede Stelle, an der die Route registriert wird. Konkrete Rezepte:

Laravel 11/12/13 — bevorzugt: eigenes routes/api.php. Die API-Gruppe enthält weder Session noch CSRF. In bootstrap/app.php:

 1->withRouting(
 2    web: __DIR__.'/../routes/web.php',
 3    api: __DIR__.'/../routes/api.php',
 4    commands: __DIR__.'/../routes/console.php',
 5    health: '/up',
 6)

Dann in routes/api.php:

 1use App\Http\Controllers\Webhooks\VorgioWebhookController;
 2use Illuminate\Support\Facades\Route;
 3
 4Route::post('/webhooks/vorgio', VorgioWebhookController::class)
 5    ->name('webhooks.vorgio');

Der Default-apiPrefix ist api, die URL wird also https://your-shop.example/api/webhooks/vorgio — diese URL registrieren Sie auf der Vorgio-Webhooks-Seite. Übergeben Sie apiPrefix: '' an withRouting, falls die Route am bloßen /webhooks/vorgio-Pfad liegen soll.

Laravel — Alternative: Route in routes/web.php belassen und von CSRF ausnehmen. Im withMiddleware-Block in bootstrap/app.php:

 1$middleware->preventRequestForgery(except: [
 2    'webhooks/vorgio',
 3]);

(Ältere Doku spricht von validateCsrfTokens — diese Methode ist in Laravel 13 zugunsten von preventRequestForgery deprecated. Gleiches Verhalten, neuer Name.)

Django — die View mit @csrf_exempt dekorieren:

 1from django.views.decorators.csrf import csrf_exempt
 2
 3@csrf_exempt
 4def vorgio_webhook(request): ...

Rails — den Authenticity-Token-Check für die Controller-Action überspringen:

 1class VorgioWebhooksController < ApplicationController
 2  skip_before_action :verify_authenticity_token, only: :create
 3end

ASP.NET MVC[IgnoreAntiforgeryToken] an der Action.

Wenn Ihr Vorgio-Webhook-Zustellprotokoll response_status: 419 (Laravel), 403 mit einer Django-Fehlerseite oder allgemein einen HTML-Body anstelle Ihrer eigenen Response zeigt, ist das fast immer die Ursache.

#Event-Katalog (v1)

type Wann er ausgelöst wird Typischer Einsatz
invoice.sent Nachdem eine Rechnungs-E-Mail zur Zustellung in die Queue eingereiht wurde — also unmittelbar nach einem erfolgreichen POST /v1/checkouts (oder nachdem der Händler in der Vorgio-UI auf "Senden" geklickt hat). Verschieben Sie die Shop-Bestellung je nach Ihrer Fulfillment-Strategie auf on-hold oder processing.
invoice.paid Wenn der Händler die Rechnung als bezahlt markiert (in der Vorgio-UI oder via POST /v1/invoices/{id}/mark-paid). Verschieben Sie die Shop-Bestellung auf processing (oder completed) und gleichen Sie Ihre Buchhaltung ab.

Der Katalog wird im Lauf der Zeit additiv erweitert. Neue Event-Typen erscheinen als Opt-in-Subscriptions; bestehende Event-Namen werden nicht umbenannt.

#Aufbau des Payloads

 1{
 2  "id": "evt_01HX9F2K7N8R5SQT3WYPJ4M6V0",
 3  "type": "invoice.sent",
 4  "created_at": "2026-05-10T12:34:56+00:00",
 5  "data": {
 6    "invoice": { /* full InvoiceResource — same shape as GET /v1/invoices/{id} */ },
 7    "client":  { /* full ClientResource — same shape as GET /v1/clients/{id}  */ }
 8  },
 9  "metadata": {
10    "shop_order_id": "1234"      // exactly what you sent on the original POST /v1/checkouts
11  }
12}

Das zurückgespiegelte metadata ist der empfohlene Weg, um Events mit den Datensätzen Ihres Shops zu verknüpfen. Siehe Metadata für gängige Muster.

#Signaturprüfung (verpflichtend)

Jeder Request enthält einen Vorgio-Signature-Header:

 1Vorgio-Signature: t=1715342400,v1=4f8d2c...hex...
 2Vorgio-Event: invoice.sent

Der v1-Wert ist ein HMAC-SHA256 über <timestamp>.<raw_request_body> mit dem Signing Secret Ihres Endpoints als Schlüssel. Das Schema ist bewusst identisch zu Stripe, sodass jedes der zahlreichen "Stripe webhook verify"-Snippets im Netz funktioniert, nachdem Sie global Stripe-Signature durch Vorgio-Signature ersetzt haben.

Verifizieren Sie immer, bevor Sie dem Payload vertrauen. Ein Angreifer, der Ihre URL kennt, aber nicht Ihr Secret, kann keine gültige Signatur fälschen.

#PHP

 1function vorgioVerify(string $payload, string $sigHeader, string $secret, int $tolerance = 300): array
 2{
 3    if (! preg_match('/^t=(\d+),v1=([a-f0-9]+)$/', $sigHeader, $m)) {
 4        throw new RuntimeException('Malformed Vorgio-Signature');
 5    }
 6    [$_, $ts, $hex] = $m;
 7
 8    if (abs(time() - (int) $ts) > $tolerance) {
 9        throw new RuntimeException('Stale Vorgio-Signature');
10    }
11
12    $expected = hash_hmac('sha256', $ts . '.' . $payload, $secret);
13    if (! hash_equals($expected, $hex)) {
14        throw new RuntimeException('Invalid Vorgio-Signature');
15    }
16
17    return json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
18}
19
20// In your endpoint:
21$event = vorgioVerify(
22    file_get_contents('php://input'),
23    $_SERVER['HTTP_VORGIO_SIGNATURE'] ?? '',
24    getenv('VORGIO_WEBHOOK_SECRET'),
25);

#Node.js

 1import { createHmac, timingSafeEqual } from 'node:crypto';
 2
 3export function vorgioVerify(rawBody: string, sigHeader: string, secret: string, toleranceSec = 300) {
 4  const m = sigHeader.match(/^t=(\d+),v1=([a-f0-9]+)$/);
 5  if (!m) throw new Error('Malformed Vorgio-Signature');
 6  const [, ts, hex] = m;
 7
 8  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) {
 9    throw new Error('Stale Vorgio-Signature');
10  }
11  const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
12  const a = Buffer.from(expected, 'hex');
13  const b = Buffer.from(hex, 'hex');
14  if (a.length !== b.length || !timingSafeEqual(a, b)) {
15    throw new Error('Invalid Vorgio-Signature');
16  }
17  return JSON.parse(rawBody);
18}

#Python

 1import hashlib, hmac, json, os, time
 2
 3def vorgio_verify(raw_body: bytes, sig_header: str, secret: str, tolerance: int = 300) -> dict:
 4    parts = dict(p.split("=") for p in sig_header.split(","))
 5    ts, hex_sig = parts["t"], parts["v1"]
 6    if abs(time.time() - int(ts)) > tolerance:
 7        raise ValueError("Stale Vorgio-Signature")
 8    expected = hmac.new(secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
 9    if not hmac.compare_digest(expected, hex_sig):
10        raise ValueError("Invalid Vorgio-Signature")
11    return json.loads(raw_body)

#Zustell-Semantik

  • At-least-once. Ihr Endpoint kann dasselbe Event (selten) zweimal empfangen. Seien Sie auf der event.id idempotent, wenn Sie etwas Unumkehrbares tun (z. B. die Bestellung versenden).
  • Der erste Zustellversuch erfolgt sofort. Vorgio dispatcht den Webhook-Job unmittelbar nachdem das In-App-Event ausgelöst wurde.
  • Wiederholungsversuche bei 4xx-Response (≥ 400), 5xx-Response oder bei jedem Netzwerkfehler. Zeitplan der Wiederholungsversuche:
    • Versuch 2: 1 Minute nach dem ersten
    • Versuch 3: 5 Minuten
    • Versuch 4: 30 Minuten
    • Versuch 5: 2 Stunden
    • Versuch 6: 12 Stunden
    • Versuch 7: 24 Stunden
  • Nach dem 7. fehlgeschlagenen Versuch (insgesamt etwa 2 Tage) deaktiviert Vorgio den Endpoint automatisch und blendet ein Banner in der Webhooks-UI des Teams ein. Der Händler muss ihn nach Behebung des Problems am Empfänger manuell wieder aktivieren. Für einen deaktivierten Endpoint werden keine weiteren Events in die Queue gestellt.

Antworten Sie innerhalb von 30 Sekunden (das Timeout) mit 2xx. Alles andere wird als Fehler gewertet und löst einen Wiederholungsversuch aus.

#Best Practices

  • Antworten Sie schnell. Bestätigen Sie sofort mit 200 OK und verarbeiten Sie den Event anschließend asynchron (Queue, Background-Job). Ein langsamer Handler frisst Vorgios 30-Sekunden-Timeout auf und riskiert doppelte Zustellungen.
  • Verifizieren Sie die Signatur, bevor Sie den JSON-Body parsen — der Verifier benötigt die rohen Bytes.
  • Behandeln Sie ungeordnete Zustellungen. In seltenen Fällen kann der zweite Versuch von Event A nach Event B eintreffen. Nutzen Sie event.id für Idempotenz und created_at für Reihenfolge-Entscheidungen.
  • Vertrauen Sie nicht den Host- oder User-Agent-Headern zur Authentifizierung — ausschließlich der Signatur.
  • Verwenden Sie pro Umgebung einen eigenen Webhook-Endpoint (dev / staging / prod). Jeder erhält ein eigenes Secret.

#Testen

Für einen schnellen Test können Sie ein echtes Event aus der Webhooks-UI Ihres Teams erneut abspielen: Öffnen Sie die Detailseite des Endpoints → letzte Zustellungen → klicken Sie auf "Replay". Derselbe Payload wird mit einem frischen Signatur-Timestamp erneut per POST gesendet.

Für vollständige End-to-End-Tests — bei denen echte (aber unterdrückte) Rechnungen echte Webhooks an eine separate Menge von Test-Endpoints auslösen — verwenden Sie den Test-Modus. Test-Mode-Rechnungen lösen ausschließlich Test-Webhooks aus; Live-Rechnungen ausschließlich Live-Webhooks. HTTP-Request, Payload und Signaturschema sind zwischen den Modi byte-identisch.