Skip to content

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

FilePurpose
app/Http/Controllers/Api/WebhookController.phpWebhook route handlers (thin controller)
app/Services/Orchestrators/StripeWebhookOrchestrator.phpStripe event processing
app/Services/Orchestrators/VivaWebhookOrchestrator.phpViva Wallet event processing
app/Services/Orchestrators/DeliverooWebhookOrchestrator.phpDeliveroo event processing
app/Services/Orchestrators/UberEatsWebhookOrchestrator.phpUber Eats event processing
app/Services/ThirdParty/SquareWebhookService.phpSquare event processing
app/Services/Payment/WebhookService.phpShared webhook utilities (Stripe/Viva)
app/Http/Middleware/VerifyDeliverooWebhook.phpDeliveroo signature verification
app/Http/Middleware/VerifyShopifyWebhook.phpShopify HMAC verification
app/Http/Middleware/VerifySquareWebhook.phpSquare signature verification
app/Http/Middleware/VerifyUberEatsWebhook.phpUber Eats signature verification

Webhook Routes

ProviderMethodRouteVerification
StripePOST/api/stripe-webhook/{countryCode}Stripe signature (in orchestrator)
Viva WalletGET/api/viva-webhook/{countryCode}/{eventTypeId}Returns verification key
Viva WalletPOST/api/viva-webhook/{countryCode}/{eventTypeId}Event type in URL
ShopifyPOST/api/shopify-webhookverify.shopify-webhook middleware
DeliverooPOST/api/webhook/deliveroo/ordersverify.deliveroo-webhook middleware
DeliverooPOST/api/webhook/deliveroo/menuverify.deliveroo-webhook middleware
Uber EatsPOST/api/webhook/uber-eatsverify.uber-eats-webhook middleware
SquarePOST/api/webhook/squareverify.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):

  1. WebhookService::constructEvent() validates the Stripe-Signature header
  2. Uses \Stripe\Webhook::constructEvent() for production
  3. In local environment, constructs event directly from payload (no signature check)

Handled Event Types

Event TypeHandlerDescription
payment_intent.succeededhandleSuccessfulPayment()Online ordering payment completed
terminal.reader.action_succeededhandleSuccessfulTerminalPayment()Terminal/kiosk payment completed
customer.subscription.createdhandleCustomerSubscriptionUpdated()New subscription created
customer.subscription.updatedhandleCustomerSubscriptionUpdated()Subscription modified
customer.subscription.pausedhandleCustomerSubscriptionUpdated()Subscription paused
customer.subscription.resumedhandleCustomerSubscriptionUpdated()Subscription resumed
customer.subscription.deletedhandleSubscriptionDeleted()Subscription cancelled (with SEPA recovery)
customer.subscription.trial_will_endhandleCustomerSubscriptionUpdated()Trial ending notification
account.updatedhandleAccountUpdated()Connected account updated
invoice.payment_failedhandleInvoicePaymentFailed()Invoice payment failed (SEPA retry)

Payment Flow (Terminal)

When terminal.reader.action_succeeded fires:

  1. Extract payment_intent ID from reader action
  2. Resolve tenant database from the reader's device ID
  3. Call PaymentCaptureService::capturePayment() with the payment intent ID
  4. This triggers the full capture flow (inventory, loyalty, KDS, etc.)

Subscription Management

When subscription events fire:

  1. WebhookService::updateLocalSubscription() syncs the Stripe subscription to local DB
  2. Updates device or location constants via DeviceService::updateD1Constants()
  3. Triggers Firebase location update for real-time sync

SEPA Direct Debit Recovery

Special handling for SEPA payment failures:

  1. invoice.payment_failed stores failed invoice data in cache (10 min TTL)
  2. Attempts immediate retry with mandate data via retrySubscriptionPaymentWithMandate()
  3. If customer.subscription.deleted fires 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:

  1. Generates SHA-256 hash from event data fields (OrderCode, CustomerTrns, Amount, StatusId, etc.)
  2. Uses database cache lock with 60-second TTL
  3. Checks if identical webhook was processed within last 30 seconds
  4. Stores recent webhook data for 5 minutes for future comparison

Event Types

Event types are mapped via Constants::$VIVA_EVENT_TYPES:

Event TypeHandlerDescription
TRANSACTION_PAYMENT_CREATEDhandlePaymentCreated()Payment completed
ACCOUNT_CONNECTEDhandleAccountConnected()Merchant account connected
ACCOUNT_VERIFICATION_STATUS_CHANGEDhandleAccountVerificationStatusChanged()KYC status changed

Payment Created Processing

When TRANSACTION_PAYMENT_CREATED fires with StatusId: "F" (Finalized):

  1. Resolve Location: Extract location ID from Tags[0] or MerchantTrns
  2. Set Tenant Database: Load location, get vendor, set tenant DB
  3. 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 (from CustomerTrns or MerchantTrns)
  • 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

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().

Uber Eats webhook processing dispatches these jobs:

  • ProcessUberEatsOrderNotificationJob -- Process new order notifications
  • ProcessUberEatsCancelNotificationJob -- Process order cancellations
  • ProcessUberEatsScheduledNotificationJob -- 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

  1. Extract shop domain from X-Shopify-Shop-Domain header
  2. Resolve shop name (strip .myshopify.com)
  3. Find matching ThirdPartyIntegration record by shop name
  4. Delegate to ShopifyIntegrationService::processWebhook()

OAuth Callbacks

These are not webhooks but related authorization callbacks:

ProviderRouteDescription
ShopifyGET /api/shopify-callbackOAuth authorization callback
SquareGET /api/square-callbackOAuth authorization callback
Uber EatsGET /api/uber-eats/callbackOAuth 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 seconds

Error 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) or MerchantTrns field
  • 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

  1. 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)
  2. Duplicate processing

    • Check database cache locks for stuck locks
    • Viva Wallet duplicate detection may reject legitimate retries within 30s
  3. 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
  4. 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

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