vorgio-app/vorgio-php
Composer 安装命令:
composer require vorgio-app/vorgio-php
包简介
Official PHP SDK for the Vorgio API. Issue invoices, send them to clients, and verify webhooks.
README 文档
README
Official PHP SDK for the Vorgio invoicing API.
Thin, framework-agnostic wrapper. Optional Laravel auto-discovery (service provider + facade + publishable config) loads automatically when Laravel is installed; in vanilla PHP / WordPress the SDK works without it.
- PHP 8.2+
- Zero retries / no caching — callers control retry policy explicitly
- HMAC-SHA256 webhook verification with replay protection
- Auto-generated UUIDv7 idempotency keys when you don't supply one
Installation
composer require vorgio-app/vorgio-php
Quick start
use Vorgio\VorgioClient; $vorgio = new VorgioClient( token: 'act_…', // from Vorgio → Team → API tokens baseUrl: 'https://vorgio.app', ); $result = $vorgio->checkouts()->create([ 'client' => [ 'external_id' => 'wc_customer_42', 'name' => 'Jane Customer', 'email' => 'jane@example.com', 'address' => 'Musterstraße 1', 'zip' => '10115', 'city' => 'Berlin', 'country' => 'DE', 'language' => 'de', // optional; defaults to the team's locale when omitted 'rate' => 0, 'vat' => 19, 'default_position_mode' => 'fixed', ], 'invoice' => [ 'tax_rate' => 19, 'due_offset_days' => 14, 'positions' => [ [ 'id' => '0193f7b0-1b8a-7b7d-9ad0-0c7b5b1d5f3e', 'date' => '2026-05-10', 'mode' => 'fixed', 'description' => 'One unit of widget', 'amount_cents' => 9900, ], ], ], 'send' => [ 'subject' => 'Your invoice from Demo Shop', 'body' => "Hello,\n\nplease find your invoice attached.", ], 'metadata' => ['order_id' => 'wc_order_999'], // round-tripped on every webhook ]); echo $result['data']['invoice']['number']; // INV-2026-0042
See examples/checkout.php for a runnable script.
Resources
| Surface | Method |
|---|---|
POST /v1/checkouts |
$vorgio->checkouts()->create($body) |
GET /v1/invoices |
$vorgio->invoices()->list($query) |
POST /v1/invoices |
$vorgio->invoices()->create($body) |
GET /v1/invoices/{id} |
$vorgio->invoices()->retrieve($id) |
PATCH /v1/invoices/{id} |
$vorgio->invoices()->update($id, $body) |
DELETE /v1/invoices/{id} |
$vorgio->invoices()->delete($id) |
POST /v1/invoices/{id}/send |
$vorgio->invoices()->send($id, $body) |
POST /v1/invoices/{id}/mark-paid |
$vorgio->invoices()->markPaid($id, $body) |
GET /v1/invoices/{id}/pdf |
$vorgio->invoices()->pdf($id) |
GET /v1/clients |
$vorgio->clients()->list($query) |
POST /v1/clients |
$vorgio->clients()->create($body) |
GET /v1/clients/{id} |
$vorgio->clients()->retrieve($id) |
PATCH /v1/clients/{id} |
$vorgio->clients()->update($id, $body) |
DELETE /v1/clients/{id} |
$vorgio->clients()->delete($id) |
GET /v1/connection |
$vorgio->connection()->retrieve() |
POST /v1/connection/test-webhook |
$vorgio->connection()->sendTestWebhook() |
Every method returns the decoded JSON response as a PHP array. The full
contract for each request/response shape lives at
vorgio.app/api-reference.
Idempotency
POST / PATCH / PUT / DELETE requests automatically receive a UUIDv7
Idempotency-Key header when you don't provide one — that's the right
default for genuinely new one-shot requests.
For multi-step flows (a queued job, a checkout that fires three sequential
POSTs, anything that retries) pass an operationId instead. The SDK then
derives a stable, per-purpose Idempotency-Key from your operation id —
the same operation id produces the same key every time, so queue retries
and midnight-cross retries replay the server's cached 2xx instead of
generating a fresh side-effect.
use Vorgio\Util\Uuid; $opId = Uuid::v7(); // persist this before the call $vorgio->checkouts()->create($body, operationId: $opId); // Later, on retry — same op id, same Idempotency-Key, same cached response: $vorgio->checkouts()->create($body, operationId: $opId);
If you ship the same idempotency key twice within the server's retention
window, you get back the original response with an
Idempotency-Replay: true header.
Internal retry
By default the SDK retries up to three times on transport errors and 5xx
responses (with exponential backoff: 200ms, 800ms, 3200ms), replaying the
same Idempotency-Key and body on each attempt. 4xx responses are never
retried; 429 keeps its caller-driven Retry-After-aware flow. Disable per
client when you'd rather handle retries yourself:
use Vorgio\Support\RetryPolicy; $vorgio = new VorgioClient(token: '…', retry: RetryPolicy::disabled());
Recurring billing
Use subscriptions() for any flow with an every cadence — membership
billing, SaaS, anything that issues invoices on a schedule. The resource
maps cleanly onto Vorgio's recurring-template verbs:
use Vorgio\Util\Uuid; $opId = Uuid::v7(); // Provision the client + recurring template + send the first invoice // in one call. Powered by /v1/checkouts under the hood. $started = $vorgio->subscriptions()->start([ 'client' => [/* … */], 'every' => 'monthly', 'invoice' => [ 'subject' => 'Mitgliedsbeitrag', 'tax_rate' => 19, 'positions' => [['mode' => 'fixed', 'amount_cents' => 9900]], ], ], operationId: $opId); $templateId = $started['data']['invoice']['id']; // Change cadence in place — no new invoice issued, just the template's // `every` and `next_invoice_at` updated. $vorgio->subscriptions()->changeCycle($templateId, 'yearly', operationId: $opId); // Stop future generation without deleting the template (full audit // trail stays visible in the operator dashboard). $vorgio->subscriptions()->stop($templateId, operationId: $opId); // Issue a Stornorechnung for a finalised invoice (UStG §14c-compliant // legal cancellation). Use this for the open child invoice, not the // recurring template itself. $vorgio->invoices()->cancel('inv_abc', operationId: $opId);
See examples/recurring-billing.php for an end-to-end smoke flow.
Downloading PDFs
$pdf = $vorgio->invoices()->pdf($invoiceId); file_put_contents("/tmp/{$invoiceId}.pdf", $pdf->bytes); $cachedEtag = $pdf->etag; // stash this alongside your record // Later — pass the stored ETag to short-circuit a re-render: $pdf = $vorgio->invoices()->pdf($invoiceId, ifNoneMatch: $cachedEtag); if ($pdf->notModified) { // 304 — your cached copy is still current. } else { // PDF changed — overwrite local copy and update $cachedEtag = $pdf->etag. }
The returned Vorgio\InvoicePdf is a tiny value object with bytes,
etag, and notModified. The ETag value is opaque — store and replay it
verbatim, including the surrounding quotes.
Errors
use Vorgio\Exception\VorgioApiException; use Vorgio\Exception\VorgioRateLimitedException; use Vorgio\Exception\VorgioValidationException; try { $vorgio->checkouts()->create($body); } catch (VorgioValidationException $e) { // 422 — $e->errors is the field => messages map } catch (VorgioRateLimitedException $e) { // 429 — $e->retryAfter is the seconds you should wait } catch (VorgioApiException $e) { // any other 4xx / 5xx — $e->statusCode, $e->problem (RFC 7807), // $e->requestId for log correlation }
The SDK never retries on its own; pick a strategy that fits your runtime (queue back-off, sync retry with jitter, etc.).
Verifying webhooks
use Vorgio\Webhooks; use Vorgio\Exception\VorgioSignatureException; try { $event = Webhooks::constructEvent( payload: file_get_contents('php://input'), sigHeader: $_SERVER['HTTP_VORGIO_SIGNATURE'] ?? '', secret: getenv('VORGIO_WEBHOOK_SECRET'), ); } catch (VorgioSignatureException $e) { http_response_code(400); return; } if ($event->type === 'invoice.paid') { // …fulfil the order }
The signature scheme matches the server byte-for-byte: HMAC-SHA256 over
<unix_ts>.<raw_body>, with a 5-minute default replay tolerance.
See examples/webhook.php.
Laravel
When the package is installed inside a Laravel app it auto-registers a service provider. Publish the config and set the env vars:
php artisan vendor:publish --tag=vorgio-config
VORGIO_TOKEN=act_… VORGIO_BASE_URL=https://vorgio.app VORGIO_WEBHOOK_SECRET=whsec_…
Then resolve the client wherever you need it:
use Vorgio\Laravel\Facades\Vorgio; Vorgio::checkouts()->create([...]);
…or inject Vorgio\VorgioClient via the container.
Checking the connection
Once the env vars are set, confirm the app is correctly wired up to Vorgio:
php artisan vorgio:doctor # token, mode, abilities, webhook, mirror tables php artisan vorgio:doctor --test-webhook # also fires a signed test event at your endpoint
Each line is a pass (✓), warning (!) or failure (✗); the command exits
non-zero if any check fails, so you can gate a deploy on it. Under the hood it
uses Vorgio\Laravel\ConnectionDoctor, which you can call directly to render
the same report in your own UI (e.g. an operator settings page):
use Vorgio\Laravel\ConnectionDoctor; $report = app(ConnectionDoctor::class)->run(); // cheap: introspection only // $report = app(ConnectionDoctor::class)->run(includeWebhookTest: true); // + live round-trip foreach ($report['checks'] as $check) { // $check = ['key' => 'webhook', 'label' => 'Webhook', 'status' => 'ok|warn|fail', 'detail' => '…'] } if (! $report['ok']) { // surface the failing checks }
Tune what "correctly set up" means via config/vorgio.php
(required_abilities, required_webhook_events).
Cashier-style recurring billing
For Laravel apps doing recurring billing — SaaS, membership dues, anything
on a cadence — the package ships a Cashier-style Billable trait that
absorbs every correctness primitive (idempotency keys, queue-retry safety,
midnight-cross timestamping, customer/subscription persistence) into the
package. No columns are ever added to your domain tables. All Vorgio
state lives in package-owned polymorphic tables:
vorgio_billables, vorgio_subscriptions, vorgio_operations,
vorgio_invoices.
The migrations are auto-loaded by the service provider. Run them once and add the trait to the model that represents your paying customer:
php artisan migrate
use Vorgio\Laravel\Billable; class Association extends Model { use Billable; }
Then the model gains:
// Start a recurring subscription. Idempotent: a second call returns the // existing subscription without hitting the API. $subscription = $association->subscribe('monthly', [ 'subject' => 'Mitgliedsbeitrag', 'tax_rate' => 19, 'positions' => [['mode' => 'fixed', 'amount_cents' => 9900]], 'metadata' => ['association_id' => 42], // lifted to the checkout top level; round-tripped on every webhook ]); // Change cadence in place. $association->changeBillingCycle('yearly'); // End the arrangement. Pick the child-invoice strategy that matches // your product's "cancel my subscription" semantics: // 'stop-only' — stop future invoices only. // 'storno-always' — also Storno the open child invoice. // 'storno-if-unpaid' — Storno only when the child is unpaid (SaaS default). $association->cancelSubscription('storno-if-unpaid'); // Helpers + relations: $association->hasActiveSubscription(); $association->latestOpenInvoiceId(); $association->vorgioSubscriptions; // HasManyThrough → Subscription $association->vorgioInvoices; // HasManyThrough → Invoice (webhook mirror)
Set VORGIO_WEBHOOK_SECRET in .env and the package registers
POST /vorgio/webhook for you. It verifies the signature, upserts the
local mirror tables, and dispatches typed Laravel events:
use Illuminate\Support\Facades\Event; use Vorgio\Laravel\Events\VorgioInvoicePaid; Event::listen(VorgioInvoicePaid::class, function (VorgioInvoicePaid $event): void { // $event->invoice — Vorgio\Laravel\Models\Invoice (local mirror row) // $event->webhookEvent — Vorgio\WebhookEvent (raw, full server payload) });
See examples/cashier-style.php for a sketch of the full flow.
Custom HTTP client
For testing, observability middleware, or alternative HTTP libraries, pass
your own Guzzle-compatible Psr\Http\Client\ClientInterface (or, more
specifically, GuzzleHttp\ClientInterface):
use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; $mock = new MockHandler([new Response(201, [], '{"data":{}}')]); $http = new Client(['handler' => HandlerStack::create($mock)]); $vorgio = new VorgioClient(token: 'act_test', httpClient: $http);
Development
composer install vendor/bin/pest # run tests composer format # PHP-CS-Fixer composer stan # PHPStan
License
MIT — see LICENSE.
统计信息
- 总下载量: 121
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-15