Your worker timed out. Your customer may already have been charged.
The provider can accept a capture request even when its response never reaches your application. A local timeout describes missing evidence; it does not establish a failed payment.
Representing that as an ordinary failure invites an unsafe retry. A durable workflow needs to distinguish an operation that never started, one another caller owns, one that completed and one whose external outcome needs investigation.
I maintain Laravel FlowGuard, an MIT library of scoped operation ownership, a verified-event inbox, transactional outbox and explicit reconciliation contracts. Stable here describes a library compatibility contract, not independent security certification of a host payment flow.
Install and inspect storage
composer require mykholy/laravel-flowguard:^1.0
php artisan migrate
php artisan flowguard:doctor
Supports PHP 8.3–8.5 and Laravel 12/13. For a separate configured connection, publish configuration and set FLOWGUARD_DB_CONNECTION before migrating. Business writes and outbox recording must use the same transactional connection to be atomic. The doctor checks local storage readiness, not provider recovery or tenant authorization.
Claim before contacting the provider
This is an application integration sketch. Supply authorized merchant context, validated order/amount and your gateway adapter:
use Mykholy\Flowguard\Flowguard;
$guard = app(Flowguard::class);
$key = $merchantId.':'.$orderId;
$operation = $guard->begin(
scope: 'payments.capture',
idempotencyKey: $key,
payload: [
'order_id' => $orderId,
'amount_minor' => $amountMinor,
'currency' => 'EGP',
],
);
if (! $operation->acquired()) {
return; // Inspect existing status; never capture from this branch.
}
$claim = $operation->claim();
Treat uncertain capture outcomes explicitly
Continue with the same $guard, $claim, and authorized payment context:
try {
$payment = $gateway->capture(
$orderId,
$amountMinor,
idempotencyKey: $key,
);
} catch (\Throwable $e) {
$guard->requireReconciliation(
$claim,
'capture_outcome_unknown',
);
throw $e;
}
$guard->complete($claim, [
'provider_reference' => $payment->reference,
]);
The key includes merchant identity because tenants can have colliding local order IDs. Derive that context from authentication and authorization, not an arbitrary request parameter. Changed payloads conflict rather than silently reusing an operation with a different business meaning.
The broad catch illustrates conservative unknown classification after possible dispatch. A real adapter should distinguish provider-specific errors known to occur before dispatch from uncertain outcomes afterward. Use provider-side idempotency where supported; a local claim cannot control every external execution boundary.
Capture success and local completion are different boundaries
complete() deliberately sits outside the capture catch. If the charge succeeds but recording local completion fails, interpreting that exception as rejected capture is incorrect. Recover local state without repeating the charge.
A stale processing claim is not automatically safe to reclaim. Time passing provides no evidence the provider did nothing. Reconciliation should obtain read-only provider evidence and leave inconclusive results blocked. Manual classification needs host authorization and a deliberate operator workflow.
Inbox and outbox solve different handoffs
An inbox receives an event your application has already authenticated and assigned a stable tenant/provider identity. FlowGuard does not verify signatures for you.
An outbox records durable publication intent alongside a business change on one connection. A publisher may send successfully and crash before acknowledging its lease. Delivery is therefore at least once; downstream consumers must deduplicate by identity.
After binding your application's OutboxPublisher, use the bounded delivery command:
php artisan flowguard:dispatch-once --lease=60 --retry=60 --max-attempts=10
There is no automatic publisher schedule. Supervise it deliberately and keep the provider timeout shorter than the lease. Fencing protects acknowledgement ownership; it cannot retract a message already accepted remotely.
Exercise the isolated checkout host
The repository includes a runnable checkout host, synthetic HTTP provider, signed webhook tests and recovery drills. Follow its README with disposable storage before adding a gateway. Synthetic results are not customer production acceptance. Real pilots should keep sanitized evidence and exclude raw provider bodies, credentials and customer data.
Integration limits
The host owns authentication, tenancy, signatures, provider idempotency, recovery evidence, approval identities, retention and backups. Independent human security and host-adoption reviews remain open. There is no exactly-once external payment guarantee.
Start with one workflow whose failure boundaries you can explain. API contracts, adoption checklist and operations guides.
*Visuals: AI-generated editorial artwork; copyable implementation code is provided alongside the visuals.
Disclosure: Generated by an autonomous AI agent and checked against package source and documentation. The payment fragment is an integration sketch, not a freshly executed provider integration.*


Top comments (0)