Appearance
Webhook Events Reference
Upvendo receives webhooks from multiple payment and delivery platform providers. All webhook routes are unauthenticated (guest middleware) but use signature verification middleware specific to each provider.
Key Files
| File | Purpose |
|---|---|
app/Http/Controllers/Api/WebhookController.php | Webhook route handlers (thin controller) |
app/Services/Orchestrators/StripeWebhookOrchestrator.php | Stripe event processing |
app/Services/Orchestrators/VivaWebhookOrchestrator.php | Viva Wallet event processing |
app/Services/Orchestrators/DeliverooWebhookOrchestrator.php | Deliveroo event processing |
app/Services/Orchestrators/UberEatsWebhookOrchestrator.php | Uber Eats event processing |
app/Services/ThirdParty/SquareWebhookService.php | Square event processing |
app/Services/Payment/WebhookService.php | Shared webhook utilities (Stripe/Viva) |
app/Http/Middleware/VerifyDeliverooWebhook.php | Deliveroo signature verification |
app/Http/Middleware/VerifyShopifyWebhook.php | Shopify HMAC verification |
app/Http/Middleware/VerifySquareWebhook.php | Square signature verification |
app/Http/Middleware/VerifyUberEatsWebhook.php | Uber Eats signature verification |
Webhook Routes
| Provider | Method | Route | Verification |
|---|---|---|---|
| Stripe | POST | /api/stripe-webhook/{countryCode} | Stripe signature (in orchestrator) |
| Viva Wallet | GET | /api/viva-webhook/{countryCode}/{eventTypeId} | Returns verification key |
| Viva Wallet | POST | /api/viva-webhook/{countryCode}/{eventTypeId} | Event type in URL |
| Shopify | POST | /api/shopify-webhook | verify.shopify-webhook middleware |
| Deliveroo | POST | /api/webhook/deliveroo/orders | verify.deliveroo-webhook middleware |
| Deliveroo | POST | /api/webhook/deliveroo/menu | verify.deliveroo-webhook middleware |
| Uber Eats | POST | /api/webhook/uber-eats | verify.uber-eats-webhook middleware |
| Square | POST | /api/webhook/square | verify.square-webhook middleware |
Stripe Webhooks
Route: POST /api/stripe-webhook/{countryCode}Orchestrator: StripeWebhookOrchestrator
The countryCode parameter determines which Stripe API key and webhook secret to use (multi-country support). Webhook secrets are configured per country: config("services.stripe.{countryCode}.webhook_secret").
Signature Verification
Verification happens inside the orchestrator (not middleware):
WebhookService::constructEvent()validates theStripe-Signatureheader- Uses
\Stripe\Webhook::constructEvent()for production - In local environment, constructs event directly from payload (no signature check)
Handled Event Types
| Event Type | Handler | Description |
|---|---|---|
payment_intent.succeeded | handleSuccessfulPayment() | Online ordering payment completed |
terminal.reader.action_succeeded | handleSuccessfulTerminalPayment() | Terminal/kiosk payment completed |
customer.subscription.created | handleCustomerSubscriptionUpdated() | New subscription created |
customer.subscription.updated | handleCustomerSubscriptionUpdated() | Subscription modified |
customer.subscription.paused | handleCustomerSubscriptionUpdated() | Subscription paused |
customer.subscription.resumed | handleCustomerSubscriptionUpdated() | Subscription resumed |
customer.subscription.deleted | handleSubscriptionDeleted() | Subscription cancelled (with SEPA recovery) |
customer.subscription.trial_will_end | handleCustomerSubscriptionUpdated() | Trial ending notification |
account.updated | handleAccountUpdated() | Connected account updated |
invoice.payment_failed | handleInvoicePaymentFailed() | Invoice payment failed (SEPA retry) |
Payment Flow (Terminal)
When terminal.reader.action_succeeded fires:
- Extract
payment_intentID from reader action - Resolve tenant database from the reader's device ID
- Call
PaymentCaptureService::capturePayment()with the payment intent ID - This triggers the full capture flow (inventory, loyalty, KDS, etc.)
Subscription Management
When subscription events fire:
WebhookService::updateLocalSubscription()syncs the Stripe subscription to local DB- Updates device or location constants via
DeviceService::updateD1Constants() - Triggers Firebase location update for real-time sync
SEPA Direct Debit Recovery
Special handling for SEPA payment failures:
invoice.payment_failedstores failed invoice data in cache (10 min TTL)- Attempts immediate retry with mandate data via
retrySubscriptionPaymentWithMandate() - If
customer.subscription.deletedfires due to payment failure:- Checks cache for recent SEPA failure
- Attempts to recreate subscription with proper mandate data
- Uses database locks to prevent race conditions between the two event handlers
Viva Wallet Webhooks
Route: POST /api/viva-webhook/{countryCode}/{eventTypeId}Orchestrator: VivaWebhookOrchestrator
Verification
- GET requests: Return the Viva Wallet webhook verification key (for initial setup)
- POST requests: Process actual webhook events
Duplicate Detection
The orchestrator implements sophisticated duplicate webhook detection:
- Generates SHA-256 hash from event data fields (OrderCode, CustomerTrns, Amount, StatusId, etc.)
- Uses database cache lock with 60-second TTL
- Checks if identical webhook was processed within last 30 seconds
- Stores recent webhook data for 5 minutes for future comparison
Event Types
Event types are mapped via Constants::$VIVA_EVENT_TYPES:
| Event Type | Handler | Description |
|---|---|---|
TRANSACTION_PAYMENT_CREATED | handlePaymentCreated() | Payment completed |
ACCOUNT_CONNECTED | handleAccountConnected() | Merchant account connected |
ACCOUNT_VERIFICATION_STATUS_CHANGED | handleAccountVerificationStatusChanged() | KYC status changed |
Payment Created Processing
When TRANSACTION_PAYMENT_CREATED fires with StatusId: "F" (Finalized):
- Resolve Location: Extract location ID from
Tags[0]orMerchantTrns - Set Tenant Database: Load location, get vendor, set tenant DB
- Determine Channel: Check
Tags[1]for order channel
Online Ordering Path:
- Find transaction by
payment_snapshot.orderCode - Acquire database lock:
payment_processing_online_{idempotencyKey}(30s TTL) - Call
OnlineOrderingOrchestrator::verifyPayment()
Kiosk Path:
- Find transaction by
order_no(fromCustomerTrnsorMerchantTrns) - Acquire database lock:
payment_processing_kiosk_{idempotencyKey}(30s TTL) - Call
PaymentCaptureService::capturePayment()with webhook data
Deliveroo Webhooks
Route: POST /api/webhook/deliveroo/ordersMiddleware: verify.deliveroo-webhook (signature verification) Orchestrator: DeliverooWebhookOrchestrator
The orchestrator delegates to DeliverooWebhookService::handleEvent().
Event Types
Deliveroo sends order lifecycle events:
- New order created
- Order accepted/rejected
- Order preparation updates
- Order pickup/delivery status changes
Menu Webhook
Route: POST /api/webhook/deliveroo/menu
Currently logged but not actively processed (menu sync is push-based from Upvendo to Deliveroo).
Uber Eats Webhooks
Route: POST /api/webhook/uber-eatsMiddleware: verify.uber-eats-webhook (signature verification) Orchestrator: UberEatsWebhookOrchestrator
The orchestrator delegates to UberEatsWebhookService::handleEvent().
Related Jobs
Uber Eats webhook processing dispatches these jobs:
ProcessUberEatsOrderNotificationJob-- Process new order notificationsProcessUberEatsCancelNotificationJob-- Process order cancellationsProcessUberEatsScheduledNotificationJob-- Process scheduled order updates
Square Webhooks
Route: POST /api/webhook/squareMiddleware: verify.square-webhook (signature verification) Handler: SquareWebhookService::handleEvent()
Square webhooks are handled directly by a service (not an orchestrator pattern) since the webhook service itself is a third-party service.
Event Types
Square sends events for:
- Terminal checkout status changes (payment completed/cancelled)
- Order updates
- Inventory changes (when two-way sync is enabled)
- Catalog updates
Shopify Webhooks
Route: POST /api/shopify-webhookMiddleware: verify.shopify-webhook (HMAC verification) Handler: ShopifyIntegrationService::processWebhook()
Processing Flow
- Extract shop domain from
X-Shopify-Shop-Domainheader - Resolve shop name (strip
.myshopify.com) - Find matching
ThirdPartyIntegrationrecord by shop name - Delegate to
ShopifyIntegrationService::processWebhook()
OAuth Callbacks
These are not webhooks but related authorization callbacks:
| Provider | Route | Description |
|---|---|---|
| Shopify | GET /api/shopify-callback | OAuth authorization callback |
| Square | GET /api/square-callback | OAuth authorization callback |
| Uber Eats | GET /api/uber-eats/callback | OAuth authorization callback |
All handled by ThirdPartyAuthService.
Webhook Processing Patterns
Idempotency
All payment webhook handlers use database locks to prevent duplicate processing:
Lock key: "payment_verification_{idempotencyKey}" or "payment_processing_{channel}_{key}"
Lock TTL: 20-30 secondsError Handling
All webhook controllers follow the same pattern:
php
try {
app(SomeOrchestrator::class)->handle($request);
} catch (\Throwable $th) {
$this->handleException($th);
}
return $this->sendSuccess();Webhooks always return 200/success to the provider to prevent retries, even on internal errors. Errors are logged for investigation.
Tenant Resolution
Webhooks must resolve the tenant database before processing:
- Stripe: Resolves from payment intent metadata (
location_id) or device reader ID - Viva Wallet: Resolves from
Tags[0](location ID) orMerchantTrnsfield - Deliveroo/Uber Eats: Resolved from the integration's stored location reference
- Square: Resolved from the webhook payload's merchant/location reference
Debugging Webhooks
Common Issues
Signature verification failures
- Ensure webhook secrets match between provider dashboard and
.env - For Stripe:
STRIPE_{COUNTRY}_WEBHOOK_SECRET - Check that the raw request body is used for verification (not parsed JSON)
- Ensure webhook secrets match between provider dashboard and
Duplicate processing
- Check database cache locks for stuck locks
- Viva Wallet duplicate detection may reject legitimate retries within 30s
Tenant database not found
- Webhook contains location ID that doesn't exist in the system
- Location was deleted but webhook still fires
- Check logs for "Location not found" warnings
Payment capture fails after webhook
- Check
PaymentCaptureService::capturePayment()logs - Look for write conflict retries (up to 5 attempts)
- Verify transaction exists with matching idempotency key
- Check
Log Patterns
Search for these log messages:
"Error processing Viva webhook"-- Viva Wallet failures"Error processing Uber Eats webhook"-- Uber Eats failures"Error processing Deliveroo webhook"-- Deliveroo failures"Stripe webhook event not handled"-- Unhandled Stripe event types"Unhandled Viva Wallet webhook event"-- Unhandled Viva event types"Payment capture failed"-- Payment capture failures"Transaction not found for"-- Missing transaction during webhook processing