Skip to main content
This article explains how the app moves data between Shopify and Engage. It covers the architecture, authentication, webhook events, the Shopify and Voyado APIs called, and the field-level data mappings applied for contacts, receipts, refunds, and order notifications. This is a reference for implementers and technical consultants. If you’re setting up the app for the first time, start with the prerequisites and how-to articles.

Architecture overview

The app is a public Shopify app built on Laravel. It runs as a background sync service: it receives events from Shopify (through webhooks and scheduled polling), transforms them, and writes the results into Engage. A smaller flow runs in the opposite direction to keep marketing preferences in sync. At a high level, there are four moving parts:
  • Webhook controllers receive real-time events from Shopify and Voyado.
  • Scheduled commands poll Shopify for new or updated records every minute as a safety net for missed webhooks.
  • Queued jobs do the actual transformation and API calls, with retries and idempotency guards.
  • API clients (ShopifyApi and VoyadoApi) wrap the outbound HTTP calls to each platform.
Most records follow a two-stage pattern:
  1. An index job (for example IndexCustomer, IndexOrder, IndexRefund) pulls the full record from Shopify and stores a local copy.
  2. A sync job (for example SyncContact, SyncOrder, SyncRefund) reads the local copy, maps it to the Voyado format, and sends it to Engage.

Authentication

Each direction uses its own credentials and its own request validation.

Shopify

  • The app authenticates to the Shopify Admin API using the per-shop OAuth access token issued during installation.
  • Requests use the API version configured in SHOPIFY_API_VERSION (default 2023-04).
  • Inbound Shopify webhooks are validated by the ValidateShopifyWebhook middleware before any processing happens.
The app requests the following OAuth scopes:

Voyado Engage

  • The app authenticates to the Voyado API using the shop’s Voyado API key, sent in the apikey header against the shop’s voyado_api_domain at the /api/v2 base path.
  • Inbound Voyado webhooks are validated by the ValidateVoyadoWebhook middleware.
  • The Voyado contact webhook payload is encrypted; the app decrypts it using the shop’s voyado_contact_webhook_encryption_key.
Credentials are stored per shop. The app supports multiple merchants, and each request is scoped to a single shop resolved from the incoming request.

Data flow

Shopify to Voyado

Triggered by webhooks and by per-minute polling commands (IndexCustomersCommand, IndexOrdersCommand, and the matching SyncContactsCommand, SyncOrdersCommand, SyncRefundsCommand).
1

Event received

A Shopify webhook fires, or a scheduled command detects a new or updated record.
2

Local copy stored

An index job stores the record locally so downstream jobs have a consistent snapshot.
3

Payload built

A sync job maps the Shopify record to the Voyado format and resolves the correct Voyado store from the country-to-store mapping.
4

Sent to Voyado

The payload is posted to the Voyado API. For contacts, the returned Voyado contact ID is written back to Shopify as a customer metafield (voyado.contactId).

Voyado to Shopify

When marketing preferences change in Voyado, Voyado calls the contact webhook. If the shop has “sync marketing preferences to Shopify” enabled, the app decrypts the payload and dispatches SyncCustomerMarketingPreferences to update the matching Shopify customer. Contacts that were created by a third party are skipped.

Contact sync flow

Contact sync also runs on a schedule: the voyado:sync-contacts command finds Shopify customers that need syncing and dispatches a SyncContact job for each one. Each SyncContact job stops immediately if the shop doesn’t have an active Voyado account. Otherwise, it loads the customer record and follows one of two paths:
  • Bulk import: the payload is built from the Shopify customer data and posted through the bulk-import creation path, which creates the contact in Voyado or reuses an existing match. The customer record is marked as a bulk import and stores the resulting Voyado ID.
  • Regular sync: the app checks whether the customer is a POS customer, looks up the matching Voyado contact, and builds the payload (name, email, phone, address, language, consents, preferences, and store mapping). A matching contact is updated; otherwise, a new contact is created.
Both paths finish by creating or updating the Shopify metafield and contact type, then updating the customer record with the last synced timestamp, the Voyado ID, the metafield ID, and the POS flag.
It’s not yet documented what distinguishes a “bulk import sync” run from a regular sync run for a given job, or what defines an “active Voyado account” for the purposes of the first check. Confirm both with the implementation owner.

Webhook event reference

Shopify webhooks (inbound)

All Shopify webhooks are registered by the SyncWebhooks action from config/esc-shopify.php and are served under the /shopify/webhooks prefix.

Voyado webhooks (inbound)

The {shopDomain} path segment is encrypted, and the webhook body is encrypted with the shop’s contact webhook encryption key.

APIs mapped between Shopify and Voyado

Engage API (/api/v2)

The exact path for the order notification endpoint could not be verified from the repository. Confirm this with the implementation owner.

Shopify Admin API

The app uses both the REST Admin API and the GraphQL Admin API.

Data mappings

Contact (Shopify customer → Voyado contact)

Built in ContactService::getContactData. Empty or null values are omitted so that existing Voyado data isn’t overwritten unnecessarily. Contact matching: the app looks up an existing Voyado contact by trying each contact type (contact, member) against the shop’s configured identification sequence (email and/or phone). On a mobilePhone uniqueness conflict, the app retries without the phone number. A matched contact with a Shopify account is promoted to member. Contacts synced through the bulk-import creation path (see Contact sync flow) follow a separate creation path that reuses an existing match instead of applying this lookup sequence.

Order receipt (Shopify order → Voyado receipt)

Built in SyncOrder. Posted to POST receipts with type: PURCHASE line items.
Script discount allocations are intentionally excluded from the per-line discount total to avoid discounting the amount twice, because Shopify’s GraphQL discountedTotalSet already deducts them.

Refund (Shopify refund → Voyado return receipt)

Built in SyncRefund. Posted to POST receipts with type: RETURN line items and negative quantities. The refund job releases itself back to the queue if the related order hasn’t been synced yet, so refunds always follow their order. Shipping-only refunds with no line items are marked as ignored.

Order notification (Shopify order/fulfillment → Voyado notification)

Built in SyncOrderNotification and NotificationFormatterService. These are sent only when order notifications are enabled for the shop. Notification types are: order_created, order_fulfilled, order_partially_fulfilled, order_cancelled, and refund_created. Each type is sent at most once per order unless a resend is explicitly requested.

Promotion and voucher redemption

During order sync, if promotion or voucher sync is enabled for the shop, the app compares the discount codes used on the order against the contact’s available Voyado promotions and loyalty vouchers.
  • Matched promotions are redeemed through POST contacts/{id}/promotions/{promoId}/redeem and added to the receipt as usedPromotions.
  • Matched vouchers are redeemed through POST contacts/{id}/bonuschecks/{voucherId}/redeem and added to the receipt as usedBonusChecks.
Redemption uses the ECOM channel for online orders and POS for point-of-sale orders.

Operational behavior

Scheduling and idempotency

  • Polling commands run every minute as a backstop for missed webhooks.
  • Sync jobs are unique (ShouldBeUnique) per record, so duplicate dispatches don’t produce duplicate work.
  • Orders and refunds that have already synced (last_synced_at is set) are skipped, and notifications are guarded by per-type “already sent” checks.
  • The Voyado uniqueReceiptId prevents duplicate receipts on the Voyado side.

Retries and resilience

Error handling

  • Every job runs inside a task that logs each API call and payload for traceability.
  • Failed syncs record the error against the record (last_sync_error or last_bulk_import_sync_error) and mark it as ignored where appropriate to avoid retry loops.
  • Missing country-to-store mappings create a visible mapping error rather than silently failing.

Technical considerations for implementers

  • Store mapping is required. Receipts, refunds, and notifications all resolve a Voyado store from the order’s country. Without a mapping, records are skipped and a mapping error is raised.
  • Currency conversion uses a third-party rate cached for 24 hours, converting the order currency to the shop’s configured Voyado group currency.
  • Marketing preference direction is configurable per shop, in both directions (Shopify checkout to Voyado, and Voyado to Shopify). Third-party-created contacts are treated carefully to avoid overwriting preferences.
  • Order and bulk-import queries must stay in sync. If you change the order flow, update the bulk import path and the paired queries (OrderQuery and OrdersBulkImportQuery) together.

Contacts

Promotions

Rewards and vouchers