# Changes API v2 to v3 Source: https://developer.voyado.com/docs/api/api-changes-v3 The current version of the Customer Engagement API has been upgraded to 3 to introduce new functionality, to make it easier to expand the API in the future, and to introduce more consistency. Here is a summary of the changes going from v2 to v3: * Consolidated and renamed endpoints for better experience and consistency * Moved some endpoints to the part of the API where they make more sense * Added the use of PATCH (update resource) and PUT (replace resource) methods * Corrected some field names that reflected legacy usage * Shifted more from header parameters to query parameters * New version of /orders endpoint with expanded functionality for storing, updating and segmenting on order data All existing usage of API v2 and v1 are not affected by these changes as those versions will now enter Active LTS (Long-Term Support). The v2 and v1 endpoints continue to be supported and may be used as before until further notice. Any errors found in these versions will be corrected as needed. Client developers are encouraged to use the current version of the API, version 3, for all new development and to upgrade existing functions, when possible, to make better use of all new functionality when it is released. Generally, new endpoints are only added in new API versions, as well as breaking changes. Non-breaking changes to existing endpoints may however be released within an existing version. See the API versioning model for details on how new API versions are released, how older versions are deprecated, and exactly how breaking and non-breaking changes are defined. Below are the detailed changes moving from v2 to v3. ## Orders The new **/orders** endpoints offer functionality to trigger an automation to send order communication (email or SMS) and also expanded functionality to store and update the order data which makes it possible to segment on order data. The v2 endpoint **/orders** only supports triggering an automation and will therefore be deprecated, however it may still be used as long as the v2 status is Active LTS. The integration pattern for **/orders** is changed to asynchronous, meaning you will be able to store data to be acted on independantly from the order data. New endpoint **/orders**. Use GET to all stored orders for a contact and POST to register a new order. New endpoint \*\*/orders/\[orderId} \*\*. Use GET to retrieve a single order, PUT to update an existing order and DELETE to delete the order permanently. New endpoint **/orders/\[orderId]/action**, used to trigger an automation and send order communication by email or SMS. New endpoint **/orders/jobs/\[jobId}** used to check the status of an order registration or update. ## Contacts Changes to the **/contacts** endpoints were done for better experience and consistency, moving some to other groups or changing the terminology for clarification of its function. The names of the **/contacts/\[contactId]/bonuschecks** endpoints (four of them) changed to */reward-vouchers* and some parameters and payload values have changed. See the "Reward vouchers (bonus checks)" section below. Endpoint **/contacts/\[contactType]/bykey/\[keyValue}** removed in v3. The same functionality is available from the */contacts/\[contactType]/bykey endpoint*. Endpoint **/contacts/count** removed in v3 (but is still available in v2). Endpoint **/contacts/\[contactType]/count** removed (but is still available in v2). Endpoint **/contacts/\[contactId]/bonuspointtransactions** removed in v3. Now point transactions for contacts are fetched by first getting point account ID using */point-accounts* and then using this ID in */point-transactions* to get the actual point transactions. The header parameters when creating a contact with **/contacts** are now query parameters. The header parameters when deleting a contact with **/contacts** are now also query parameters. Method when updating contact with **/contacts/\[contactId}** changed from POST to PATCH. The endpoint **/contacts/\[contactId]/transactions** changed to its own resource */transactions* and the contactId is now sent in the query string. More article data is returned in the response in v3. ## Contact overview Changes made to the payload for **/contactoverview**. The property "bonusPoints" changes name to "points" and "checkNumber" changes to "voucherNumber". In v3 a contact's total points (bonus points) are no longer returned from **/contactoverview**. This must now be done with a separate GET request to the already existing (but slightly changed) endpoint **/point-accounts**. ## Promote to member The v2 endpoint **/contacts/\[contactId]/promoteToMember** removed in v3. This was used to: * Update contact type to "member" * Set `isApproved` to true (triggering the "New Member" automation trigger Updating contact type is handled by */contacts/\[contactId]/updateContactType* in v3. Setting `isApproved` to "true" must be done manually in v3 using: ```http theme={null} PATCH /api/v3/contacts/{contactId}) ``` ## Preferences The individual endpoints used to update preferences were consolidated to the contact update. Endpoints **/contacts/\[contactId]/preferences/acceptsEmail**, **/contacts/\[contactId]/preferences/acceptsPostal** and **/contacts/\[contactId]/preferences/acceptsSms** removed in v3. Logic now handled by the update endpoint */contacts/\[contactId}*. ## Posoffers The **/posoffers** endpoints are obsolete and will be deprecated. Promotions should now be handled by the */promotions* endpoint. Endpoint **/contacts/\[contactId]/posoffers/all** removed in v3. Endpoint **/contacts/\[contactId]/posoffers/\[id]/redeem** removed in v3. Endpoint **/contacts/\[contactId]/posoffers/available** removed in v3. Endpoint **/contacts/\[contactType]/bykey/\[keyValue]/posoffers/\[id]/redeem** removed in v3. Endpoint **/contacts/\[contactType]/bykey/\[keyValue]/posoffers/all** removed in v3. Endpoint **/contacts/\[contactType]/bykey/\[keyValue]/posoffers/available** removed in v3. Endpoint **/contacts/bykey/\[keyValue]/posoffers/\[id]/redeem** removed in v3. Endpoint **/contacts/bykey/\[keyValue]/posoffers/all** removed in v3. Endpoint **/contacts/bykey/\[keyValue]/posoffers/available** removed in v3. ## Promotions Changes were made to the **/promotions** endpoints; Endpoint **contacts/\[contactId]/promotions** removed. To fetch a contact's promotions you can now use */api/v3/promotion-assignments*. Endpoint **/contacts/\[contactId]/promotions/\[promotionId]/redeem** removed in v3. For the same functionality use */api/v3/promotions/codes/\[promotionId]/redeem*. Endpoint **/contacts/\[contactId]/promotions/\[promotionId]/assign** removed and functionality replaced by */api/v3/promotion-assignments*. ## Points Changes were made to the **/point-accounts** section to clarify usage and consolidate endpoints. Parameters sent to **/point-accounts** in order to get a contact's points have changed. Note that, because of the changes to **/contactoverview**, the **/point-accounts** endpoint is now the only way to fetch a contact's points. Endpoints **/point-accounts/definitions** and **/point-accounts/definitions** are deprecated and should no longer be used. Endpoint **/contacts/rewardpointtransaction** no longer used, replaced by */api/v3/point-accounts/transactions*. Endpoint **/point-accounts/transactions** is not to be used and is replaced by */api/v3/point-transactions*. Endpoint **/point-accounts/transactions/\[id}** is not to be used and is replaced by */api/v3/point-transactions/\[id}*. ## Reward vouchers (bonus checks) Endpoint **/promotions/reward-vouchers/\[id]/reactivate** becomes */reward-vouchers/\[rewardVoucherId]/reactivate*. Endpoint **/contacts/\[contactId]/bonuschecks/\[bonusCheckId]/redeem** becomes */reward-vouchers/\[rewardVoucherId]/redeem*. Endpoint **/contacts/\[contactId]/bonuschecks** changed to */reward-vouchers*. Endpoint **/contacts/\[contactId]/bonuschecks/available** becomes */reward-vouchers/available*. Endpoint **/contacts/\[contactId]/bonuschecks/redeemed** becomes */reward-vouchers/redeemed*. The attribute **usedBonusChecks** is replaced by **usedRewardVouchers**. The attribute **checkNumber** is replaced by **voucherNumber**. ## Tracking and inventory Changes were made to the tracking and inventory sections to supports multiple items in a single request. Endpoint **/tracking/carts/batch** removed. Endpoint **/tracking/carts** enhanced to */tracking/carts* and can accept multiple items. Endpoint **/tracking/productviews** removed. Endpoint **/tracking/productview** enhanced to */tracking/productview* and can accept multiple item. Endpoint **inventory/stock-levels/batch** removed. Batch functionality now handled by enhanced version of */inventory/stock-levels*. ## Transactions / Receipts Endpoint **/transactions** used to import receipts is deprecated. In v3 use **/receipts** instead. The attribute **usedBonusChecks** is replaced by **usedRewardVouchers**. The attribute **checkNumber** is replaced by **voucherNumber**. ## Messages Changes were made to how historical email, SMS and postal messages are fetched for a contact. The single endpoint **/api/v2/contacts/\[contactId]/messages/latest** is replaced by three endpoints: * **/api/v3/contacts/\[contactId]/emailmessages** * **/api/v3/contacts/\[contactId]/smsmessages** * **/api/v3/contacts/\[contactId]/postalmessages** ## Other changes Endpoint **/memberstatus** removed in v3. # Non-functional requirements Source: https://developer.voyado.com/docs/api/non-functional-requirements When developing a client application that communicates with Customer Engagement using your tenant-specific API key, it's essential to follow certain non-functional requirements (NFRs). The guidelines listed here ensure that your integration is secure and resilient and aligns with Voyado's best practices. ## Security best practices ### Use HTTPS with TLS 1.2 or higher for encryption in transit All API requests must be made over HTTPS to protect data, including the API key, in transit. TLS ensures that credentials and payloads cannot be intercepted or tampered with during transmission. ### Store the API key securely at rest If your client needs to persist the API key: * Store it in a **secure secrets management system** (e.g., AWS Secrets Manager, Azure Key Vault). * Avoid hardcoding the key in source code, config files, or environment variables exposed to user-facing environments. * Use **file system-level encryption** and proper access controls if stored locally. * Never store the API key in frontend or browser-accessible code — all calls to the Voyado API must be made from trusted backend systems. ### Prevent exposure in logs or error messages Do not log the API key or include it in exception traces. Redact sensitive headers before logging outbound requests. ### Use the correct header-based authentication Send the API key in the x-api-key HTTP header, not in the URL or query string, to avoid accidental exposure via browser history or intermediaries. ### Implement IP Filtering Consider restricting API access for your tenant to specific IP addresses using Voyado's IP filtering feature. This adds an extra layer of security by ensuring that only requests from authorized IPs are accepted. ## Testing and environment readiness ### Use the staging Environment Before deploying to production, thoroughly test your integration in [Voyado's staging environment](/docs/introduction/staging-environment#about-the-staging-environment). Doing this allows you to: * Validate authentication and API connectivity. * Test all intended API endpoints, including handling of success and error responses. * Simulate various scenarios, such as network latency and API timeouts. * Ensure proper logging and monitoring are in place. ## Resilience and Error Handling ### Implement appropriate retry logic Your client should be able to handle temporary issues gracefully without overwhelming the API or duplicating requests unnecessarily. Retries should be performed **only under the following conditions**: * When server timeout (504) is received: This happens when processing the request taked too long. The timeout threshold is different for each endpoint, and is generally shorter for customer facing experiences. * **In the case of other 5xx server errors:** These indicate temporary issues on the Voyado side. It is safe to retry with exponential back-off. * **Connection timeouts or network interruptions:** These are transient connectivity issues can occur between your client and Voyado's API. * **There is a 408 (Request Timeout):** The server didn’t get the full request in time, often due to network delays. it is safe to retry with back-off, especially for idempotent requests. * **In the case of 429 (Too Many Requests):** The client is being rate-limited. Retry after a short delay using exponential back-off. **Do not retry** under these conditions: * **There is a 4xx client error that is not 408 or 429:** Errors such 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), or 404 (Not Found) indicate a problem with the request itself and will not succeed on retry without changes being made. * **There are invalid payloads or schema violations:** Fix the validation issues before retrying the same request. * **There are duplicate-sensitive POST operations:** Instead of blindly retrying, confirm whether the initial request succeeded (such as by using a GET) to avoid duplicating actions. ### Recommended strategy * Use **exponential back-off** with added jitter (random delay variation). * **Cap the retries** after a reasonable limit (such as 3–5 attempts). * Always **log retry attempts**, including reasons and delays. ## Monitoring and Logging ### Comprehensive Logging Maintain detailed logs of all API interactions, including: * Request and response timestamps. * Endpoint URLs and HTTP methods. * Status codes and error messages. This facilitates troubleshooting and performance monitoring. ### Monitor Performance Metrics Regularly track metrics such as: * API response times. * Error rates. * Request volumes. Monitoring these metrics helps in identifying and addressing performance bottlenecks promptly. ## Input Validation and Data Integrity ### Validate and Sanitize Inputs Ensure that all data sent to the API is properly validated and sanitized. This prevents malformed requests and potential security vulnerabilities.  ### Secure Data Storage If your application stores data retrieved from the API, implement appropriate security measures to protect this data, especially if it includes personal or sensitive information. ## NFRs for webhooks If your system subscribes to webhooks from Voyado Customer Engagement, your webhook listener must be designed for security, reliability, and scalability. ### Validate the source of incoming webhook requests Each webhook request includes an HMAC-SHA256 signature in the x-voyado-signature header, based on a shared secret. Your listener should compute the expected signature and compare it to the one received to verify that the payload genuinely comes from Voyado. This protects against spoofed or unauthorized requests. ### Use HTTPS and restrict access to the webhook endpoint Only expose your listener over HTTPS to ensure data is encrypted in transit. Restrict access to your webhook endpoint by allowing only traffic from Svix’s external delivery IPs — the service Voyado uses to send webhooks. While not required, this is strongly recommended to reduce the risk of unauthorized access. ### Support payload encryption if required If sensitive data is to be included in some webhook payload, you can configure your webhooks to use AES-256 encryption. Your system must then decrypt the payload on receipt using your private key. Note that encryption is either enabled for all webhooks or disabled entirely. ### Respond quickly and asynchronously Webhook endpoints should return a 200 OK response as soon as possible to acknowledge receipt. Any heavy processing (e.g., database updates or downstream API calls) should be handled asynchronously. This ensures reliable delivery and prevents timeouts. ### Implement rate limiting and queuing if needed If multiple events are sent in a short time (e.g., during peak campaigns), your listener should throttle incoming traffic and queue processing to avoid failures or overload. ### Log and monitor webhook activity Log incoming webhook events and responses, including timestamps, event types, and delivery status. Monitor for spikes, failures, or validation errors to ensure system health. # Working with the API Source: https://developer.voyado.com/docs/api/the-api Most of your work integrating your systems with Voyado Customer Engagement will involve its API. Calls to the API need to be made from server-side. Calls from client-side will result in a CORS (Cross-Origin Resource Sharing) error. Here you'll find a documented overview of the API: The current version of the API is 3. That is the one recommended for all new users. ## Liveness check The `/livez` endpoint provides a minimal check to verify that the Voyado Customer Engagement service is responding. ```http theme={null} GET /api/v3/livez ``` Or: ```http theme={null} HEAD /api/v3/livez ``` A response of *200 OK* is returned when the service is reachable and able to respond. This endpoint only verifies that the service can respond to requests. It does not perform checks against dependencies such as databases or external services. Requests to `/livez` count towards the standard rate limit of 200 requests per second per tenant. ## API overview These links will take you to sections in this article or to other articles. ## Base URL Voyado's APIs are accessed through a single base URL, made up of the client's name followed by "voyado.com". Use HTTPS for all interactions with the Voyado API For your production environment the base URL form is: ```http Production URL theme={null} https://[client].voyado.com ``` For the test (staging) environment the base URL form will be: ```http Staging URL theme={null} https://[client].staging.voyado.com ``` For example, a client named ABC123 would have this base URL in the staging environment: ```http theme={null} https://abc123.staging.voyado.com ``` And this URL in production: ```http theme={null} https://abc123.voyado.com ``` ### Naming convention In this documentation, the part of the URL that comes before /api will be left out and the HTTP method will be added before it. For example, instead of an instruction to make a GET request to: ```http theme={null} https://abc123.staging.voyado.com/api/v3/contacts/{contactId} ``` You will just see this in the documentation: ```http theme={null} GET /api/v3/contacts/{contactId} ``` ## Authentication The API key provides you access to the Voyado Customer Engagement API. As with all keys, it needs to be handled carefully so it doesn't fall into the wrong hands. ### Authorization method The authorization method `apikey` is used, as part of a HTTPS header. ### User Agent In order to track where an API call comes from, and which version the customer is using, the "User-Agent" request header should be used in all calls made to the Customer Engagement API. This provides valuable data, improving service and allowing better and faster support for you if problems occur. "User-Agent" is set as a HTTPS header, stating the system and which version of the integration the customer has. For example, if the client Acme is using a POS called "Acme-POS" with version 1.5, the "User-Agent" value in the request should be something like "AcmePOS/1.5". While the "User-Agent" request header is highly recommended for all API calls, no error will occur if it is omitted. ### Content-type In all request made to the Customer Engagement API, it's mandatory to use "Content-Type: application/json". This needs to be sent in the HTTPS header in the API-call. If it's not included, or some other `Content-Type` is used, such as "text/plain", Customer Engagement will return a 500-error with the message “An error has occurred”. The following example will return a *200 OK* response assuming the connection to Customer Engagement is working. ```bash theme={null} curl -i "{your-base-url}/api/v3/livez" \ -H "User-Agent: AcmePOS/1.5" \ -H "apikey: {your-api-key}" \ -H "Content-Type: application/json" ``` The `Content-Type` header in all requests to the API must be "application/json" for your request to work. ## Rate limit In order to maintain a high quality of service throughout the system, Voyado recommends that you keep the frequency of your requests to the Voyado Customer Engagement API below 50 per second under normal operation. If you exceed the rate limit of 200 requests per second, the API will return a HTTP-status code [**429 Too Many Requests**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429). Going above the API limit recommendations for more than a few seconds can cause degradation in Voyado's services. Rate limits are measured **on the tenant level** and not per API key. ## Retry policy As a user of the Voyado Customer Engagement API, you must always ensure that, after sending a request, your solution waits for the response to arrive. If a response is delayed, you should not perform a retry before you know the reason for the delay. Always wait for a response from the Customer Engagement API before trying again. If your response is late, it is more probable that Customer Engagement is under an unusually high load and responding slowly than your message was lost along the way. Messages can be lost, of course, but it is not the most likely scenario. According to the Voyado Customer Engagement [integration principles](/docs/integration/about-integration#integration-principles), a contact should always be able to perform their entire purchase in the POS or e-com, regardless of problems such as the Customer Engagement API not responding. The internet is complex and many things can go wrong or be delayed. So if your flow is time-critical, you should be prepared to handle all likely interruptions on your end, and not be entirely dependent on ideal-case response times from Customer Engagement. ### Timeouts Customer Engagement's general stance is that delays are beneficial. The platform is designed to prioritize successful execution over speed, particularly to avoid data corruption. As such, most API endpoints are configured with long timeouts; this gives clients the best chance of receiving a response without prematurely failing the request. It is therefore not recommended that customers enforce their own timeouts. However, in critical flows, such as Create Contact, where the API interaction is tied directly to customer-facing experiences (e.g. onboarding confirmations), there is a need for quicker client feedback. In these specific cases, Customer Engagement may proactively return a timeout with a shorter interval, using the status code "504 RequestTimeOut". The exact timeout duration is not specified but might be indicated per endpoint both in this documentation and in the endpoints/developer overview. ### Retrying If a retry is unavoidable, here are a few things to keep in mind: In general, a retry should be made as late as possible based on your particular scenario. Shorter retry intervals can make the problem worse, increasing the possibility of writing incorrect data, as well as the load on the system. But it also depends on what you're doing. For example, a job started by a server or process on another system can wait increasingly long intervals between retries (10s, 30s, 60s, 300s, etc.) whereas an action invoked by a user (such as a call from POS, e-com or the web) can get away with a shorter wait before a retry is made. But, again, wait as long as possible before retrying. POST requests change the state of the system every time they are called. So retrying a POST request without knowing if the first one went through can be very dangerous. It's always wise, when retrying a POST, to: * Wait as long as your particular scenario allows * Check using a GET if your previous request went through or not * Try another POST, at increasing intervals The basic advice is to be ready for this when it happens and to follow the integration principles: ### Jitter It can be a good idea to add some jitter when performing retries. Jitter is some randomness added to your retry schedule so that every client is not calling the system at the same time and reducing its performance. For example, if your retry schedule is the following: * Retry Immediately * Then after 5 seconds * Then after 5 minutes * Then after 30 minutes Just do the following before each request: ```csharp theme={null} func sync() { // instead of immediately performing the call // we add jitter to distribute the network load val jitter = random(0.5, 1.5); wait(jitter.to_seconds); perform_network_request() } ``` ## Security For the majority of customers, interacting with the Voyado API is central to their solution, enabling them to create, read, update and delete information in multiple contact's journeys simultaneously. As a Voyado customer, you can freely access your data in real time. However, this comes with certain risks, since that information could possibly end up in the wrong hands. To mitigate these risks it's imperative that all users of the Voyado API do their best to handle their traffic in the most secure way possible. This section will list the basic measures that all API users should implement. ### API key in query string The usage of an API key in the query string of an incoming request is NOT considered safe. Such a request is not strictly a security issue in itself, provided encrypted traffic is used. The problem is that the request, on its way from point A and point B, is most likely logged by the servers handling the traffic. The whole query string will then be visible in those logs, making them a potential security issue. API keys, because of this, should **not** be used in the query string but only in the header of the request. ### Encryption of data Users of the Voyado API must adhere to common practices regarding encryption of traffic. We adhere to Mozilla's definition of Intermediate Compatibility. HTTPS should always be used when communicating with the Voyado API to guarantee basic security. ### TLS Starting **October 1, 2025**, Voyado will only support **TLS 1.2 or higher**. The older standards, TLS 1.0 and TLS 1.1, are considered insecure and will no longer be accepted. Any requests using non-supported TLS versions will be denied. ### IP filtering with API keys It is possible to restrict access to the Voyado API for a specific API key using IP-filtering. This measure, set up by Voyado, limits access to traffic coming from specific IP addresses or ranges of IP addresses, greatly reducing the risk for leakage of data. In the case of an API key becoming compromised, an attacker would then also need access to a device/service communicating through the correct IPs to gain access to the data, providing an extra layer of security. The API key on its own is not enough to provide access. ### Recommended ciphers * ECDHE-RSA-AES128-GCM-SHA256 * ECDHE-ECDSA-AES256-GCM-SHA384 * ECDHE-RSA-AES256-GCM-SHA384 * ECDHE-ECDSA-CHACHA20-POLY1305 * ECDHE-RSA-CHACHA20-POLY1305 * DHE-RSA-AES128-GCM-SHA256 * DHE-RSA-AES256-GCM-SHA384 ## Versioning strategy API versioning is the practice of managing changes to an API and ensuring these changes are made without disrupting clients. A good API versioning strategy clearly communicates the changes made and allows API consumers to upgrade to the latest version at their own pace. Voyado's API versioning model for Customer Engagement is designed to enable their developer teams to release new and changed functionality that extends the API in a controlled way. The new model gives Voyado's client\`s developers (both partner and in-house) improved and updated documentation, including well-defined release notes for all changes to the API. The model ensures that Voyado's client developers (both partner and in-house) always get updated documentation, including well-defined release notes for all changes to the API. ### The Customer Engagement API today * The current version of Customer Engagement API is version 3 * All versions of the API can be used independently * A common API versioning scheme is used for all endpoints * Major version upgrades are done when breaking changes are needed (see below) * Minor non-breaking changes are done regularly * V1 is considered deprecated but can still be used ### Definition of changes The definition of what is a breaking and a non-breaking change is vital in understanding the Customer Engagement API versioning strategy. The following are examples of **non-breaking changes**: * Adding an optional field to a request message. Existing client code will not be forced to use the new field. * Adding a required field with a default value to a request message. The server will add the default value if the client does not send it. * Adding a field to a response message. Existing client code should ignore any such new fields. Clients should not implement a mapping that raises an error for new fields. These are examples of **breaking changes**: * Renaming fields or resource paths in a way that requires client code to change. * Renaming or removing fields in a request or response message. * Removing API operations used by existing clients of the API. * Changing fields from single value to multiple value (like from a single email per member to a list of email addresses). * Changing the HTTP method or the response codes returned by an API operation. Code accessing our API should not break if an API endpoint starts to return responses containing additional fields. Such changes could be made by Voyado without warning as part of our normal development so you need to be ready for them. Breaking changes will be flagged early, giving you adequate time to prepare. ### The API versioning vision The Customer Engagement API should be **reliable**, **scalable** and **easy to use**, providing clients with real-time access to their core CXM environment. There are four key goals: Voyado's developers must be able to release new or changed endpoints to production whenever they are ready. This will work like this: 1. Non-breaking changes will be developed and released continuously to production by Voyado's teams following the normal release process. 2. Breaking changes (that trigger a new version) will also be released continuously but will be hidden behind a feature toggle. 3. Breaking changes should be collected into official releases, ensuring consistent communication both externally and internally. 4. First-mover clients may be selected to use pre-released API functionality before official releases, although with limited support. To ensure backward compatibility: 1. Voyado will not force clients to upgrade their API version, and will avoid impacting working integrations by keeping multiple supported versions in parallel. 2. Voyado will warn well in advance before moving a version to an unsupported end-of-life state, giving clients time to upgrade. 3. Voyado will deprecate versions in a controlled manner, and have a dialogue with clients still using unsupported version to help them upgrade. Voyado will ensure clear communication and up-to-date documentation. * New versions with breaking changes will require all related API documentation to be updated and clear release notes to be communicated both externally and internally. * Minor non-breaking releases will also require changes to API documentation as well as clear release notes. Voyado will be as transparent and consistent as possible with API roadmaps and release notes. * Voyado will publish and communicate the new API versioning model externally to partners and clients. * Voyado will communicate the new API version strategy and versioning model to all Voyado departments. * Voyado will train their dev teams to use the new API versioning model. The goal is for new functionality to only be released in the current API version, to minimize maintenance but also to give our clients an incentive to upgrade to the latest version, increasing the number and quality of features they have available to them. However, an older API version could sometimes be updated to use newly released code if that simplifies overall code maintenance. ### API release status These are the lifecycle phases for an API version: Graph showing API lifecycle phases for different versions **Limited:** Only accessible by selected clients (used in development). **Current:** The latest release. New releases will be introduced regularly, but there is no fixed release schedule. **Active LTS:** Long term support. There is no set time limit, it depends on the support cost and how much that version might limit development. **Deprecated:** A version not supported but still active. Clients are notified at least 6 months in advance if a deprecated version is to be moved to end-of-life. **End-of-life:** This version is not supported. It may be deleted or redirected to a newer version. It will be removed when not used by any client. ### Strategy in a nutshell These are the important points of the Customer Engagement API versioning strategy: 1. Versioning will be separated on an application level (Customer Engagement, Product Discovery Storefront, Product Discovery Admin). 2. A single version will be used for all Customer Engagement API endpoints. 3. A new API version will be released when breaking changes are introduced. 4. An old API version will be supported for at least 18 months. 5. Non-breaking changes will be automatically applied to the latest version. API versioning in Customer Engagement is not connected to the API versioning in Product Discovery. # Timeout changes API v3 Source: https://developer.voyado.com/docs/api/timeout-changes This is information about a timeout change to the Customer Engagement API, posted 2025-04-10. ## Create contact endpoint This section concerns data posted to the following endpoint: ```http theme={null} POST /api/v3/contacts ``` This endpoint will now enforce a shorter timeout, and will return a "504 RequestTimeOut" status code if a response does not come within about 5 seconds. This is designed to provide more timely feedback in customer-facing flows, such as user sign-ups and onboarding, where quick responses are critical for a smooth experience. If Customer Engagement returns a timeout status code for this endpoint when a request is made, it means that no contact has been created, and all related internal operations will be rolled back. If this happens, the integrating system should then retry the request in accordance with the Customer Engagement [retry policy](/docs/api/the-api#retry-policy). At this link you will find guidance on the recommended retry strategies. This change is only for *API version 3*. Version 2 will function as it previously did. ## Redeem promotion endpoint This concerns the following endpoint: ```http theme={null} POST /api/v3/promotion-assignments/{assignmentId}/redeem ``` A similar timeout policy has already been implemented for this endpoint. A request will time out after approximately 4.5 seconds unless the promotion has already passed the "point of no return" (which means it has been marked as redeemed). ## General policy for timeouts Customer Engagement's general approach is to prioritize successful processing over speed. Most API endpoints are therefore configured with long timeouts to reduce the risk of data corruption. In general, custom client-side timeouts are discouraged unless absolutely necessary. If you have any questions about how this change may impact your integration, or would like help adjusting your implementation, reach out to your support team. # UICulture and localization Source: https://developer.voyado.com/docs/api/uiculture-localization Culture (in the context of software) refers to how culture-dependent data such as dates, currencies and numbers are both presented and interpreted as input. An example is whether "." or "," is to be used for the decimal separator and the thousands separator. UICulture refers to which locale-specific external assets are to be loaded in. In the case, it's the language of the texts shown in Customer Engagement. By default, these texts are sent in Swedish, because Voyado's servers are configured with that locale. In normal website requests, the text language is determined according to the user's settings, which are saved in a cookie in the browser. Using cookies is also possible for backend/API calls, but it is not standard. There is, however, a way to be sure of which translations are delivered, regardless of other settings: the UICulture parameter. By adding `?UICulture=en-GB` to the query string of your API call when requesting the texts, you can be sure they will be sent to the client in English. For example: ```http theme={null} https://demo.staging.eclub.se/startdata/localization?UICulture=en-GB ``` If your call contains a `UICulture` parameter that is not valid, English texts will be returned as this is the translation engine's default. To force translations to be returned in a specific language, use the `UICulture` parameter. # E-com and communication Source: https://developer.voyado.com/docs/communication/communication Here we cover the tracking and communication possibilities in Customer Engagement. ## E-com and tracking Captures customer interactions on your e-com and connect them to a customer profile in Customer Engagement. * **Web activity tracking** records on-site behavior such as product views and cart activity * **Onsite messaging** allows targeted messages and campaigns to be displayed directly within the e-commerce experience * **Back in stock** enables customers to subscribe to stock updates for a specific product and triggers notifications when items are restocked ## Campaigns Learn about email, SMS and postal send-outs in Customer Engagement. Read about: * Email deliverability * Bounced and unsubscribed * Sending SMS messages * Using external content * Suggested products For how to create and localize emails for your audiences, see the Help Center: # Introduction to contacts Source: https://developer.voyado.com/docs/contacts/about-contacts A *contact* in Voyado Customer Engagement is the unique collection of data connected to an individual customer. You can't store transactions or other event data without connecting it to a specific contact. During a contact's journey, the amount of information connected to them increases with every step. When working with the contacts API, and all other Customer Engagement API endpoints, it is important to keep the API rate limits in mind to avoid degraded service. ## Contact types To track as much information as possible as a contact progresses from an unknown web user to a loyal customer, Customer Engagement uses contact types. This allows a unique data model for each phase. A contact type's name is a [handle][handle] that refers to a specific data structure. A contact type also has a display name which will be used in the UI. These display names must not be the same as the handle. The handle "Member" always exists when a Customer Engagement environment is created. The handle "Contact" is often added too. You can also add other user-defined contact types. * **Member:** This usually has the richest data model, where the person has given their consent and shared their information. This can sometimes be called "Customer" in the UI. * **Contact:** This is mainly used for email lists such as "Newsletter subscribers" or "Prospects". Typically this contact type contains only an email and some demographic data such as a country code. Sometimes "Member" is the only contact type, and sometimes both "Contact" and "Member" will exist. Other custom contact types can be added to suit the customer's specific needs. The relevant contact types, along with their key values and mandatory fields, are defined in the client project and can be obtained from your Voyado Customer Engagement team. The contact card in the Customer Engagement UI ## Data model The amount of data connected to a contact type - the type's *data model* - can be adjusted depending on your needs. Voyado and its partners can configure which fields are included and can also add custom fields in the formats listed here: ## Public identifier For every contact, one or more public identifiers are defined. This is unique data for every individual that can be used to identify them in a sales interaction. A mobile phone number is often used as a public identifier. In Sweden a person's unique *Public Identity Number* is sometimes used. A pure player (e-commerce) will probably use email, but email is not really the best identifier in a physical store scenario, where information needs to be given quickly and correctly. Multiple lookups on different identifiers can be done on a POS system. The lookup fields used can also vary from installation to installation. ## Integration key The integration key is used to link a customer to their interactions, most commonly through transactions. It is strongly advised to use a unique numerical value such as: * **contactId:** The contact's unique identifier in Customer Engagement * **memberNumber:** The value connecting a point transaction to a contact * **externalId:** The contact's unique identifier in an external system By doing it this way the data stored for a transaction can’t be linked to an individual in XML files or to API calls outside of the Voyado Customer Engagement context, increasing privacy [handle]: https://en.wikipedia.org/wiki/Handle_\(computing\) # Bulk contacts API Source: https://developer.voyado.com/docs/contacts/bulk-contacts-api The bulk contacts endpoints are used for batch updating of contact data directly over the Customer Engagement API. ## Bulk reading of contacts You can get an array of contact IDs of all contacts that have been changed (created, updated or deleted) within a specified time window using this endpoint: ```http theme={null} GET /api/v3/contacts/changes ``` This endpoint takes three parameters: Created, Updated, Deleted The start date and time of the search window. The end date and time of the search window. If `toDate` is left out of the query, the current date and time will be used. For example, assuming the date and time now is "2023-05-04 11:20:00.000" and you want the IDs of all contacts created over the last 15 minutes, the request would be: ```http theme={null} GET /api/v3/contacts/changes?changeType=Created&fromDate=2023-05-04%2011%3A05%3A00.000 ``` ### Response The response will look like this: ```json All contacts changed within the given window theme={null} [ "b16fd8f3-d0f5-aaaa-aa60-b00300f9aaaa", "febb6003-863d-bbbb-a4d0-b0030119bbbb" ] ``` Or, if there are no contacts matching your search, an empty array: ```json theme={null} [] ``` The number of contactIds that can be returned in the response is currently capped at 250 000. If your query creates a response that exceeds this limit, it will instead return a Bad Request status code. ## Bulk writing of contacts To create multiple contacts at once, the contacts bulk API endpoint can be used: ```http theme={null} POST /api/v3/contacts/bulk ``` You can also use this same endpoint to update multiple existing contacts at once: ```http theme={null} PATCH /api/v3/contacts/bulk ``` The maximum number of contacts allowed in one bulk request is 4000. The rate limit for this endpoint is the same as for other endpoints. If you are planning to do a large migration of contacts, and you have an XML export activated, first get in touch with your Voyado team. ### Query string parameters There are two optional parameters that can be added to the query string: This prevents your bulk contact request from triggering the automatic BI and XML contact exports. The default value for this attribute is "false" so you only need to include this if you want it to be true. You can read more about this parameter here. This allows you to specify the contact type of the contacts you are creating (or updating). If not specified, your default contact type configured in Customer Engagement will be used. The `avoidTriggeringExport` parameter only has an effect when doing a bulk update (that is, when using the PATCH operation). A request using these parameters (a bulk update in this case) would look like this: ```http theme={null} PATCH /api/v3/contacts/bulk?avoidTriggeringExport=true&contactType=Contact ``` In this example, all the contacts updated will be of `contactType` "Contact". ### Creation payload The data needed to bulk create contacts has the same requirements as when creating an individual contact using the `contacts` endpoint. This means, at a bare minimum, that the key attribute must be included in the payload (in this case, it's `email`). Here is a basic bulk creation payload: ```json Basic bulk contact creation payload theme={null} [ { "firstName": "Anna", "lastName": "Arkberg", "city": "Examplecity", "email": "anna@example.com", "registrationDate": "2022-01-01T00:00:00+0100" }, { "firstName": "Berit", "lastName": "Berlucci", "city": "Examplecity", "email": "berit@example.com", "registrationDate": "2022-01-01T00:00:00+0100" } ] ``` If you have specified a `contactType` in the query string then all these new contacts will be created with that type. If it's not specified, the new contacts will all be created using whatever default `contactType` you have configured in Customer Engagement. ### Consents and preferences There is an important difference between creating contacts individually with the `contacts` endpoint and creating them in bulk using the bulk endpoint discussed here. In the bulk API case, the contact data is sent "flat", as can be seen here for these two contacts: ```json Flat JSON structure for bulk contact creation theme={null} [ { "firstName": "Anna", "lastName": "Arkberg", "city": "Examplecity", "email": "anna@example.com", "acceptsEmail":true, "acceptsPostal":false, "acceptsSms":false, "memberConsent":true }, { "firstName": "Berit", "lastName": "Berlucci", "city": "Examplecity", "email": "berit@example.com", "acceptsEmail":true, "acceptsPostal":false, "acceptsSms":true, "memberConsent":false } ] ``` Note that here `memberConsent` is sent as an attribute and a value, not as an object. In Customer Engagement, this consent's `date` attribute will be the date of the bulk import and the `source` will be set to "Import". However in the case of the single-call endpoint `/api/v3/contacts/{contactId}` the "consents" and "preferences" values are always sent *as object arrays* as can be seen here: ```json JSON structure for single contact creation theme={null} { "firstName": "Anna", "lastName": "Arkberg", "city": "Examplecity", "email": "anna@example.com", "preferences": { "acceptsEmail": true, "acceptsPostal": true, "acceptsSms": true }, "consents": [{ "id": "memberConsent", "value": true, "date": "2022-01-01T00:00:00+0100", "source": "ECOM", "comment": "Approved member terms at checkout" }] } ``` In Customer Engagement, the consents `acceptsEmail` and `acceptsSms` should not be changed from "true" to "false" unless the contact has actively opted out of communication, for example through My Pages or by unchecking a pre-selected opt-in during checkout. ### Update payload When doing a bulk update through the API, you will need to specify the unique contact ID for each contact, along with the value or values you want to change. Sending null or empty values of fields in an update payload can have unpredictable results. Sometimes the current value will be overwritten, depending on the field type, and sometimes not. In general, do not send null or empty values unless you explicity want to change the field to that value, and even then it's best to confirm the results first. ```json Bulk contact update theme={null} [ { "contactId": "a9cc2989-6a39-4168-b48d-afda77a0139c", "lastName": "Berg", "city": "Exampelopolis" }, { "contactId": "4823f5ab-9d03-41d2-ae1f-afda99a013cb", "lastName": "Ek", "city": "Exampelholm" }, { "contactId": "8afa642d-d4e2-4040-a573-2dc20ea6b9a0", "lastName": "Sten", "city": "Exampelholm" } ] ``` All items in a bulk contact update must have the same properties, as seen in the example above. If they don't, a *HTTP 422: Properties differ between contacts* error will be thrown and you risk some contact data becoming overwritten with empty values. All contacts sent in a bulk update must have the same `contactType`. You either send this in the query string, or else not specify it and the default value will be used. This means you **cannot** mix contacts of different `contactType` in the same bulk update request. ### Full update payload example Here is a full update payload with all the contact attributes you can send. In practice, you'll rarely send all of these attributes at once in a contact update, and will just be updating some of them. The complete list shown here is just for reference. ```json Full update payload example theme={null} [ { "street": "Example Avenue 12", "lastName": "Person", "firstName": "Test", "zipCode": "string", "careOf": "careOf", "city": "Examplecity", "countryCode": "SE", "consent": "true", "acceptsEmail":true, "acceptsPostal":false, "acceptsSms":false, "email": "test.person@test.com", "mobilePhone": "+46700111222", "age": 65, "birthDay": "2004-04-11", "gender": "Female", "socialSecurityNumber": "190703316893", "discoveryKey": "87bd3b56-a1fd-4060-9962-4f08ef047251", "lang": "en", "currentStoreExternalId": "SE", "labels": "Label1,Label2" } ] ``` ### Responses If your request has been successful, you'll get a *202: Accepted* response. Your data is now ready for processing. A unique batch ID for your request will also be returned as a GUID. Save this batch ID as you'll need it to check the status of your batch job. If the request has not been successful, you'll get one of the following HTTP error codes: * *422: UnprocessableEntity* * *503: ServiceUnavailable* ### Status Your batch job can take time to be processed. You can check the current status of the job by calling this endpoint with the batch ID: ```http theme={null} GET /api/v3/contacts/bulk/status?batchId={batchId} ``` This returns an object containing a `status` attribute with one of these values: * **Received**: Your payload has been received. * **InProgress**: Processing of your data is in progress. * **Done**: Your contacts should now be created / updated. * **The request is invalid**: The batch ID you sent could not be found. * **Error**: There was an error. The progress of your batch will be recorded in the integration log. Batch process in the integration log The log is especially useful when finding out what happened if things go wrong. ### Starting the job Once you get back a status of "Received" your data is ready to be processed. This happens on a schedule, so it might not be completed right away. Keep polling the `status` endpoint to see when it's done. ### Wrong contact type If a bulk update contains a contact of the wrong `contactType`, updating of that contact will be skipped and the job will continue. ### Contact that already exists If a bulk create contains a contact that already exists in Customer Engagement, the existing contact will be not be changed and the job will continue. In the same way, if a bulk update refers to a contact that does not exist, that row will be skipped and the job will continue. # Contact field definitions Source: https://developer.voyado.com/docs/contacts/contact-field-definitions The fields (attributes) listed here are created by default for every contact type in Voyado Customer Engagement. ## Contact standard fields These contact attributes exist by default when setting up a new Customer Engagement tenant and every contact created will have them. | **Field name (UI)** | **DB name** | **Field type** | **Description / example value** | | :- | :- | :- | :- | | Contact type | contactType | String | The contact type for this contact, such as "Member" or "Contact" | | First name | firstName | String | Example: Jane | | Last name | lastName | String | Example: Doe | | Member Number | memberNumber | Sequence | Example: 12345678 | | Social Security Number | socialSecurityNumber | SocialSecurityNumber | Validation requires this to be either a Swedish personal identity number, a Finnish personal identity code or a Swedish samordningsnummer. If only 10 digits are specified it is assumed the contact is less than 100 years old. | | E-mail | email | Email | Example: [jane@doe.com](mailto:jane@doe.com). | | Mobile Phone | mobilePhone | PhoneNumber | Example: +4612345678. Country calling code is required (+46) | | Registration store | createdInStore | SingleStorePicker | Example: 1001. Once this is set it can't be updated | | Regular store | store | StorePicker | Example: 1002. If not set then this defaults to the same value as Registration store | | Street Address | street | String | Example: Janes street 12 | | Zip Code | zipCode | String | Example: 123 45 | | City | city | String | Example: Stockholm | | Care Of | careOf | String | | | Country | country | String | Example: Sweden | | Weighted RFM (1-100) | rfm | Number | Calculated and set in Customer Engagement | | Recency (1-5) | retail.keyvalue.recency | Integer | Calculated and set in Customer Engagement | | Frequency (1-5) | retail.keyvalue.frequency | Integer | Calculated and set in Customer Engagement | | Monetary (1-5) | retail.keyvalue.monetary | Integer | Calculated and set in Customer Engagement | | Age | age | Integer | Example: 28. Automatically set if socialSecurityNumber or birthDay is populated. | | Birthday | birthDay | BirthDate | Given as YYYY-MM-DD. Example: 1963-02-13 | | Gender | gender | Gender | Customer Engagement allows four options: Male, Female, Other, PreferNotToSay (as well as NotSet when no value has been chosen). | | Nps-grade | nps.grade | Integer | Set by Customer Engagement | | Nps-date | nps.datetime | DateTime | Set by Customer Engagement | | Average Nps-grade | nps.averagegrade | Number | Calculated in Customer Engagement | | External ID | externalId | String | Identification key from external systems. Used when Customer Engagement isn't the master. | | Country Code | countryCode | CountryCode | As [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) Example: SE | | Language | lang | Language | Contact's preferred language from the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) list (en, sv, de, nb). A filtered version of this list can be configured in the Customer Engagement back-end so that only the values you want to include are shown. | | Registration date | registrationDate | DateTime | Valid date format is ISO 8601 with time zone designator (e.g. 2021-01-26T15:10:17+01:00) | | Source | source | String | "ECOM", "API", "STORE" ... | When creating a contact, the recommended approach is to ONLY specify `countryCode` in the payload. The value for `country` will be automatically populated based on what you specify for `countryCode` (assuming you have sent a valid value). So do *not* include `country` in the payload. ### More about Birthday Customer Engagement automatically calculates if the current day is the contact's birthday, based on the standard contact attribute Birthday. This is the only way it is done. If birthday or age is added as an extra field, they will be ignored. The Birthday calculations that activate the trigger are made every morning at a random time between 6:00 and 8:00. `Birthday` must have the format **YYYY-MM-DD** with dashes as shown. Using an incorrect date format when creating a contact will *not* stop the contact from being created, but it will cause their Birthday to be null. ## Contact custom attributes As well as the standard contact attributes listed above, there is also the option to extend the data model by adding custom contact attributes, based on the client's needs. Custom attributes can be used to store data about contacts in order to create useful segments or to personalize content in send-outs. Both standard and custom contact attributes use the field types listed in the section below. ## Contact field types Below are the field types supported in Customer Engagement and their constraints: * **Text** - Standard string field * **Long text** - Extended text field * **Number** - Decimal field * **Checkbox** - One checkbox * **Checkbox (multiple)** - Several checkboxes * **Dropdown menu** - A dropdown menu * **Social security number** - The attribute `socialSecurityNumber` is a default attribute of this type. It is possible to add another social security number field as long as both fields don't set birthday and gender.  * **Store (current)** - The store (current store) field will be automatically added when activating the "Stores" module in "Enable/disable modules". Adding another attribute of this type is not recommended, and if you do then both fields will be mirrored, meaning that changing one will automatically change the other. * **Store (created in)** - See "Store (current)Company (only with Company-module) - *DEPRECATED* * **Birth date** This is type BirthDate and always given as YYYY/MM/DD, for example **1971/05/26** * **Date and time** - For other dates and times we use [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)  * **E-mail** - We validate email according to: **RFC 5322, adr-spec** * **Formula** - (Used in special cases) * **Gender** - The gender attribute is a default attribute of this type, and it can be calculated by either socialSecurityNumber (default), setting on the socialSecurityNumber attribute. * **Integer** - Maximum length of this field is 12 digits. * **Sequence** - Serial number, for instance **memberNumber**. When manually importing contacts the sequence attribute (memberNumber for instance) needs to be present but empty for Customer Engagement to set a sequence number. * **List of children** - It's possible to add multiple children with their age set by their birth date. * **Country** This is of type countryCode - ISO 3166, Alpha-2 (two digits). * **Consent** - The consent attribute includes title, linked text and the actual terms text (HTML). * **Age** (Integer) - Is set by socialSecurityNumber by default. When using the type Sequence as a custom attribute, it should never be used as a key since this will impact performance severely. You will never need to manually create any fields of the following types, since one of each is created by default whenever a contact is created, and duplicates are not allowed: * Store (current) * Store (created in) * Birth date * Gender * Age (integer) ## More about standard fields ### Mobile Phone (mobilePhone) Each contact can only have one phone number connected to them, and the number should always be formatted as MSISDN (Mobile Station International Subscriber Directory Number), as a "+" prefix followed by \[countrycode]\[phonenumber]. For example: +46707104603. Voyado Customer Engagement uses [libphonenumber](https://github.com/googlei18n/libphonenumber). a Google open source library, to validate phone numbers internationally. For a deep dive into the area of phone number validation and all the issues that can occur, [you can read this page](https://github.com/googlei18n/libphonenumber/blob/master/FALSEHOODS.md). The library libphonenumber works by setting a "default" country code, from which all numbers are validated. A number is considered valid if you, by calling that exact number from a phone in the "default" country, could succeed in placing a call. However, this can quickly become complex, making it important to follow the standard. Consider these cases: **Case 1**: If the default country is SE, the following numbers are valid: 0707104603, +46707104603, 0046707104603. This is because "00" is the international calling code from Sweden. **Case 2:** If the default country is US, the same number, 0046707104603, would now not be valid, since "00" is not an international calling prefix in the US. So, although it will sometimes work, you should never send phone numbers using only the country code (without prefixing "+"). There are many ambiguities where it is impossible to distinguish a country code from a regional code. For example, 468211415 will be validated as a number with regional code 046 (Lund) instead of a Swedish number (46) if called from regional code 08 (Stockholm). Thereby it will be stored in the database as 46468211415. Similar things can happen with Norwegian numbers that have country code 47, but local numbers may themselves also start with "47". A phone number should always be made of a "+" prefix followed by \[countrycode]\[phonenumber]. Using "local" phone numbers based on the default country code should only be done when manually entering phone numbers in Customer Engagement, i.e. when searching, or using the LoyaltyBar, or on registration forms. ### Email (email) All emails in Customer Engagement are validated according to *RFC 5322, addr-spec*. Read the details of [RFC 5322 here](https://www.rfc-editor.org/rfc/rfc5322). Read a [general explanation of email standards here](https://en.wikipedia.org/wiki/Email_address). ### Country code (countryCode) Country codes follow the ISO 3166, alpha-2 naming standard. Some useful examples are: * SE = Sweden * NO = Norway * GB = Great Britain (and Northern Ireland) * NL = Netherlands * IE = Ireland ### Language (lang) Some markets / countries use multiple languages, for example Belgium, where Dutch and French are both used. Here, the countryCode contact attribute isn't enough to identify the end-user's language. In cases like this, the lang (language) standard contact attribute can be used. The lang contact attribute is of type Language, and follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) standard. Examples: * en = English * sv = Swedish * nb = Norwegian (bokmål) * de = German Since the list of languages is very long and can be troublesome to navigate when manually setting a contact's language, a custom-made shorter version of the list can be configured for your tenant, showing only the languages you intend to use for your contacts. Contact your Voyado team to do this. ## Preferences and accept flags A contact also has several optional preference fields defining how they wish to be contacted, known as accept flags. | **Field name (UI)** | **DB name** | **Field type** | **Description & example value** | | :- | :- | :- | :- | | Accepts Email | acceptsEmail | Bool | True / False | | Accepts Postal | acceptsPostal | Bool | True / False | | Accepts SMS | acceptsSms | Bool | True / False | If these accept flags are used, they must be set to either true or false, and never to null. # Create contact Source: https://developer.voyado.com/docs/contacts/create-contact A vital part of any Customer Engagement integration journey is registering new contacts. This is done using the following endpoint: ```http Creating a contact theme={null} POST /api/v3/contacts ``` This endpoint can be used to create a contact with `contactType` of "Contact", "Member" or any of your custom types. You will need to provide the mandatory fields for your specific contact type along with whatever key value you have specified in your configuration. That will be one of the following: * **mobilePhone** * **email** * **externalId** * **socialSecurityNumber** (Swedish or Finnish personal identity number) An important change when moving from API v2 to v3 is that values previously sent in the header when creating a contact (`source`, `storeExternalId`, `createAsUnapproved`) are now sent in the query string. If these are sent in the header in a v3 request, they will be ignored. ## Parameters There are three optional parameters that can be sent in the query string of your request. They are: This shows how the contact was created. Example values are “POS” or “e-com”. This is not mandatory, but highly recommended to reduce problems later on. The unique ID of the store or market where the customer signed up (also know as the recruited-by store). Often this is a numerical code like “600”. This parameter is not mandatory, but highly recommended to reduce problems later on. This creates an unverified contact which then must be verified by some other process or action, such as clicking a link in an email. It can have the string values “true” or “false”. An unverified contact like this will be ignored in reports and won’t be included in segmentations until specifically asked for. The default value is “false” so if you are not using this, you don’t need to worry about it. While it is possible to send in any `source` value you want in the query string, in order to filter contacts in Customer Engagement using a specific `source` value, that value needs to be added in the back-end by your Voyado team. Contact them to do this. Before you try to create a contact, always check first if they already exists. For this, use [this endpoint](/docs/contacts/identify-contact/find-contact) where multiple identifiers can be used to see if a contact is already in the database. ## Payload Here is the payload you will send in your create-contact request. You should specify the `contactType` in this payload. This needs to be a `contactType` that already exists. If you give one that doesn't exist, you'll get an error and no contact will be created. If the `contactType` is not specified in your payload, it defaults to "Member". ```json theme={null} { "firstName": "Example", "email": "example@example.com", "countryCode": "SE", "birthDay": "1966-06-26", "lang":"en", "contactType": "ExampleContactType", "preferences": { "acceptsEmail": true, "acceptsPostal": true, "acceptsSms": true }, "consents": [{ "id": "memberConsent", "value": true, "date": "2022-01-01T00:00:00+0100", "source": "ECOM", "comment": "Approved member terms at checkout" }] } ``` For countries using multiple languages the `lang` attribute is used. The field `birthDay`, if used, must have the format YYYY-MM-DD as in the example payload above. If it is used, the contact's age is calculated automatically when they are created. The value of `countryCode` is mostly used in segmentation to decide which language to use when communicating with an end-user. It is therefore important that `countryCode` is correct. When creating a contact, the recommended approach is to ONLY specify `countryCode` in the payload. The value for `country` will be automatically populated based on what you specify for `countryCode` (assuming you have sent a valid value). So do *not* include `country` in the payload. Your POS should have the option to easily update the end-user's `countryCode` when necessary. The field `countryCode` is not mandatory when creating a contact but it is highly recommended. ## Response payload After the POST operation, a new contact is created and their complete data set is returned. ```json theme={null} { "id": "e8f17b1d-b777-4688-862b-cf38009a726e", "attributes": { "firstName": "Example", "lastName": null, "street": null, "zipCode": null, "city": null, "email": "example@example.com", "mobilePhone": null, "countryCode": "SE", "careOf": null, "country": "Sweden", "age": 57, "birthDay": "1966-06-26", "externalId": null, "socialSecurityNumber": null, "gender": null, "staff": false, "memberNumber": "999032095", ... "currentStore": { "id": "b5e553d1-e4ba-40a0-8ee4-a9f631ee950b", "name": "Megashop E-commerce SE, 444", "externalId": "444" } }, "meta": { "createdOn": "2022-10-24T11:22:19+02:00", "modifiedOn": "2022-10-24T11:22:19+02:00", "sourceType": "API", "approved": true, "contactType": "ExampleContactType" }, "preferences": { "acceptsEmail": true, "acceptsPostal": true, "acceptsSms": true }, "consents": [{ "id": "memberConsent", "value": true, "date": "2022-01-01T00:00:00+0100", "source": "POS", "comment": "Approved member terms at checkout" }] } ``` For consents, only the fields `id` and `value` are mandatory. ## Endpoint timeout This endpoint has a significantly lower enforced timeout (under 10 seconds) compared to others in the API, and may return a *504 RequestTimeOut* status. This is intentional to ensure timely feedback in customer-facing flows like onboarding. If Customer Engagement returns a timeout on this endpoint, no contact will have been created and all internal operations will be rolled back. In this case, the integrating system should retry the request in accordance with the [Retry Policy](/docs/api/the-api#retry-policy), which provides guidance on recommended retry strategies. ## Response status codes A successful creation will return *HTTP 201 Created* along with the contact's data set as the response body (as shown above). If the request has failed, you'll get one of the following HTTP error codes: * *400 : NoData* * *409: ApprovedContactWithKeyExists, ContactWithKeyIsBeingCreated* * *422: ValidationError* * *504: RequestTimeOut* Don't send null or empty values in the JSON payload when updating or creating a contact. Empty values will override existing values. Send only what you want to change and nothing else. # Delete contact Source: https://developer.voyado.com/docs/contacts/delete-contact Deleting a contact in Customer Engagement is done through the /contacts endpoint using the `contactId`: ```http theme={null} DELETE /api/v3/contacts/[contactId] ``` In the query string you can send a `source` parameter to show how the contact was deleted. While it's not mandatory, it is useful for tracking the reason why contacts were deleted. This gives extra information about why the contact was deleted. Example values are "POS" or "e-com" but you can send anything else you want. There is no payload sent with this request. This endpoint is not to be used for batch deletion. For mass deletion of contacts, use the Customer Engagement UI or bulk integrations methods ([see this section](/docs/contacts/bulk-contacts-api)). ## **Responses** A successful deletion will return *HTTP 200 Ok*. If a contact with that ID could not be found, or was already deleted, you'll get *HTTP 404 Not Found*. # Deep linking to contacts Source: https://developer.voyado.com/docs/contacts/identify-contact/deep-linking It's possible to link directly to the contact card of a specific individual in the Customer Engagement frontend. This is called "deep linking". This saves many steps for customer agents when they need to search for a specific individual, or when they want to point someone else to a specific contact's details. The URL you will use has this form: ```http The URL form theme={null} https://[client].voyado.com/app#/contacts/contact-card/single/00000000-0000-0000-0000-000000000000/[contactId] ``` * **client:** Your specific client / tenant name (acme, megashop or whatever) * **contactId:** The contact ID for the contact you're linking to (GUID) Note that the "00000000-0000-0000-0000-000000000000" part is **required** for this to work. If you are using the Customer Engagement staging environment, this URL will have *staging* at the start: ```http The URL form in staging theme={null} https://[client].staging.voyado.com/app#/contacts/contact-card/single/00000000-0000-0000-0000-000000000000/[contactId] ``` You can now copy and share the URL you've created, allowing anyone who is logged into your Customer Engagement frontend to go directly to this contact's information. # Find a contact Source: https://developer.voyado.com/docs/contacts/identify-contact/find-contact Use this endpoint to check if a certain contact exists in Customer Engagement: ```http theme={null} GET /api/v3/contacts/id ``` To this endpoint you can send one (or several) of the following parameters in the query string: * email * socialSecurityNumber * mobilePhone * customKey ## Request examples As can be seen in the examples given below, parameters sent in a query string must always be *URL encoded*. ```http By email theme={null} GET /api/v3/contacts/id?email=john.doe%40gmail.com ``` ```http By socialSecurityNumber theme={null} GET /api/v3/contacts/id?socialSecurityNumber=196903027573 ``` ```http By mobilePhone theme={null} GET /api/v3/contacts/id?mobilePhone=%2B46735654321 ``` Although it's called "socialSecurityNumber" (sometimes SSN) in Customer Engagement, what's actually used here is a Swedish personal identity number, a Finnish personal identity code or a Swedish coordination numbers (samordningsnummer). ```http By customKey theme={null} GET /api/v3/contacts/id?customKey=999888777 ``` As the "customKey" value you can send either a contact's `memberNumber` or `externalId`, depending on how your tenant is set up. Your Voyado team can help you here. ## The any parameter The "any" parameter accepts any of the above values. To find a match, they are scanned using the value in the "any" parameter in this order: * email * socialSecurityNumber * mobilePhone * customKey The scanning of attributes will halt as soon as the given value passes validation for an attribute, even if that value returns no matches. So if a `customKey` is used with values that could validate as another attribute, for example as `mobileNumber`, then the scanning will stop with a *404 ContactNotFound* response and no match will be returned. A request would look like this: ```http A request using any theme={null} GET /api/v3/contacts/id?any=john.doe%40gmail.com ``` Or this: ```http Another request using any theme={null} GET /api/v3/contacts/id?any=999888777 ``` The `contactType` value can also be sent in the query string along with the above parameters but it is not required. ## Responses If the request has been successful, meaning the contact exists, you'll get a *HTTP 200 OK* response along with the contactId which is the unique identifier for your contact and looks something like this: `2e40b702-2767-4abe-ae99-aa510108e22b`. This `contactId` is what you will then use to fetch the contact's data. If the request has not been successful, you'll get one of the following HTTP error codes: * *400: InvalidEmailAddress, InvalidPhoneNumber, InvalidSocialSecurityNumber, InvalidContactType, InvalidSearchQuery* * *404: ContactNotFound* * *409: MultipleMatches* * *500: InvalidSystemConfiguration* Response codes could change, so always check your Swagger page to see the latest ones. ## Multiple responses It can happen that a search returns more than one `contactId`. For example, if you do a search with the `email` parameter and several contacts have supplied the same email, they will all be returned in that response. That will look like this: ```json Multiple matches response theme={null} { "errorCode": "MultipleMatches", "messageDetails": { "multipleMatchesFound": [ "1cdd503c-b8cf-1111-a001-b05b00c888a4", "2a361f42-f831-2222-852d-b0610100adef" ] }, "message": "Exception of type 'Voyado.Api.ApiException' was thrown.", "requestId": "f0930fe5-3333-4700-dddd-2ab2653ddf0c" } ``` For multiple matches, contact IDs are returned sorted by `CreationTime` and in ascending order. This means in the order in which they were created, with the earliest created coming first. # Get contact data Source: https://developer.voyado.com/docs/contacts/identify-contact/get-contact-data Once you have the `contactId` you can then fetch a contact's data. The recommended way to do this is: ```http Fetching data for contact theme={null} GET /api/v3/contacts/{contactId} ``` If you want the fetch the contact's full profile with the *extended data model*, see here: # Get enriched profile Source: https://developer.voyado.com/docs/contacts/identify-contact/get-enriched-profile Getting a contact's full information is core to Customer Engagement's power. It allows *personalization*, giving access to Customer Engagement's full customer profiling in a single request. This service has many use cases: * Populating an e-com My Pages or other landing page with customer information * Allowing a POS system to display the complete customer profile * Enabling the staff member to act on an incorrect email address * Informing the customer about reward vouchers or individual offers soon to expire * Allowing the e-com to switch content panels depending on customer's target audience This is all achieved through the `/contactoverview` endpoint. The data from this endpoint might contain cached contact information. For real-time updates on contacts and their corresponding data (promotions, reward points, etc.) then go directly to each entity endpoint, such as for example `/contacts/[contactId]/promotions`. The /contactoverview endpoint is not to be used as a contact lookup endpoint nor for ID-fetching. For this, use the other methods [described here](/docs/contacts/identify-contact/find-contact). Parameters sent in a query string must always be *URL encoded*. ## Using /contactoverview with contactId If you know the `contactId`, you can fetch a contact's data directly without using any other information. This is because `contactId` is guaranteed to always be unique for a contact, whereas this is not enforced for the the other personal identifiers. ```http theme={null} GET /api/v3/contactoverview?contactId={contactId} ``` You can also send `contactType` if you want, as shown here, but it's not mandatory in this case. ```http theme={null} GET /api/v3/contactoverview?contactType={contactType}&contactId={contactId} ``` ## Using /contactoverview with email With the email parameter, the `contactType` must also be sent. ```http theme={null} GET /api/v3/contactoverview?contactType=member&email=jane.doe%40gmail.com ``` ## Using /contactoverview with mobilePhone With the mobilePhone parameter, the `contactType` must also be sent. ```http theme={null} GET /api/v3/contactoverview?contactType=member&mobilePhone=%2B46702291216 ``` ## Using /contactoverview with socialSecurityNumber This is the Swedish personal identity number, Finnish personal identity code or Swedish samordningsnummer (coordination number). ```http theme={null} GET /api/v3/contactoverview?contactType=member&socialSecurityNumber=198506059738 ``` With the socialSecurityNumber parameter, the `contactType` must also be sent. ## Using /contactoverview with customKey As the customKey value you can send either a contact's `memberNumber` or `externalId`, depending on how your tenant is set up. Your Voyado Customer Engagement team can help you here. ```http theme={null} GET /api/v3/contactoverview?contactType=member&customKey=999888777 ``` With the customKey parameter, the `contactType` must also be sent. ## Using /contactoverview with any The "any" parameter will accept any of the above values. They are scanned in the following order: * email * socialSecurityNumber * mobilePhone * customKey For example: ```http theme={null} GET /api/v3/contactoverview?contactType=ecom&any=jane.doe%40gmail.com ``` Or: ```http theme={null} GET /api/v3/contactoverview?contactType=contact&any=999888777 ``` With the any parameter, the contactType must also be sent. Whenever you call /contactoverview with the `contactId`, sending `contactType` is optional. In all other cases, sending the `contactType` is mandatory. ## Response If the request has been successful, you'll get a *HTTP 200 OK* response along with the contact's data. Otherwise you'll get one of the following HTTP error codes: * *400: InvalidEmailAddress, InvalidPhoneNumber, InvalidSocialSecurityNumber, InvalidContactType, InvalidSearchQuery* * *404: ContactNotFound* * *409: MultipleMatches* * *500: InvalidSystemConfiguration* ```json theme={null} { "id": "33ee9958-0e09-484f-b4c4-aa3e00e1e24c", "attributes": { "firstName": "Example", "lastName": "Examplesson", "street": "Examplegatan 12", "zipCode": "123 45", "city": "Examplestad", "email": "example@example.com", "mobilePhone": "+46739111111", "countryCode": "SE", "socialSecurityNumber": "198101010101", "gender": "Female", "careOf": null, "country": "Sverige", "birthDay": "1981-01-01", "externalId": null, "memberNumber": "999888777", "rfm": 46, "children": [ "2007-11-28T00:00:00+01:00" ], "age": 35, "registrationDate": "2019-02-05T13:21:02+01:00", "secrecyMarked": false, "currentStore": { "id": "0b7037b4-b7f5-4c9d-97e6-aa3b00ccc140", "name": "Rotterdam", "externalId": "SE014" } }, "meta": { "createdOn": "2019-04-29T15:42:25+02:00", "modifiedOn": "2020-05-31T04:02:27+02:00", "sourceType": "Import", "approved": true, "contactType": "Member", "recruitedByStore": { "id": "a9573a5a-c9bb-4593-abee-aa3b00ccc141", "name": "Nice", "externalId": "SE019" } }, "preferences": { "acceptsEmail": true, "acceptsPostal": true, "acceptsSms": true }, "tiers": { "expires": null, "tierName": "Bas", "remainingToUpgrade": 233, "basedOnPoints": 767, "tierId": "bas" }, "consents": [ { "id": "consentIntelligentOffers", "value": true, "date": "2019-05-06T14:00:51+02:00", "comment": "", "source": "ContactForm" }, { "id": "consentSegmentation", "value": true, "date": "2019-05-06T14:00:51+02:00", "comment": "", "source": "ContactForm" }, { "id": "consentGeneralTerms", "value": true, "date": "2019-05-06T14:00:51+02:00", "comment": "", "source": "ContactForm" } ], "audiences": [ "Best Customers rolling 24 months", "My VIP Group", "Intergamma VIP", "Superbokare" ], "warnings": [ "MissingMobileNumber" ], "rewardVouchers": [ { "expiresOn": "2020-08-31T23:59:59+02:00", "id": "880adf9c-b70b-47a8-b746-aaf900528332", "voucherNumber": "0000000000161", "name": "Generate reward vouchers 02/11/2019 08:00", "value": { "currency": "EUR", "amount": 3 }, "localValues": [ { "currency": "SEK", "amount": 30 }, { "currency": "NOK", "amount": 30 } ], "points": 100, "redeemed": false, "redeemedOn": null } ], "promotions": [ { "id": "27c03506-89d6-4023-a158-c0bb0dc09e58", "externalId": null, "type": "Multichannel", "name": "Spring Campaign", "expiresOn": null, "heading": "Spring Campaign", "description": "Dear customer, The spring is here, time for updating your wardrobe! Please enjoy the following spring time promotion. With love", "redeemed": false, "redeemedOn": null, "imageUrl": null, "link": null, "redemptionChannels": [ { "id": "75992b07-b287-4417-8cb8-adba010fdaf8", "externalId": null, "type": "Multichannel", "name": "20% off VIP offer!", "expiresOn": null, "heading": null, "description": null, "redeemed": false, "redeemedOn": null, "imageUrl": null, "link": null, "redemptionChannels": [ { "type": "POS", "valueType": "PERCENT", "value": 20, "instruction": null }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "VIP20PERCENT", "instruction": null } ] } ] } ``` ## Changes going from API v2 to v3 Changes were made to this endpoint for API v3. One important change to note is that in v3 a contact's total points (bonus points) are no longer returned from `/contactoverview`. For v3, this can only be done through a separate GET request to the already existing but slightly changed /point-accounts endpoint. See here for more: # Soft identification Source: https://developer.voyado.com/docs/contacts/identify-contact/soft-identification Soft identification, sometimes called "soft login" or "soft ID", allows identification of a contact who has arrived at a webpage via an email link. When your Customer Engagement environment is configured for soft identification, an encrypted JSON is sent in the link's query string, which has the following form: ```javascript theme={null} eclub=q6KCtl2WUiWxd5UzDMcfKNuZonZ6bVx7LZm2y8ctNJVJm4Oiyz339wWMIuVAzHIwikEdtDFYllMLwMhMZ4GTn55SBGjSn2ZQsDtCEDJ-zG7ptW6hC9n2UO2NVxaijERvURtBPFIE35EZPPSk_PAZHmQp72C0N6-h1xgfok9lb-9IV_hWorlegR8sD64p3SOy ``` In this data is contained all the information needed to identify the contact and present them with a personalized web experience. When decrypted, the JSON has the following structure: ```json theme={null} { "contactId": "1c9b35d6-28d8-4520-8900-a89555f7f0c5", "email": "example@example.com", "dateTime": "2018-07-11T11:19:54Z", "discoveryKey" : "6iub35d6-34d8-6666-8900-e89100f7f0c5" } ``` * **contactId:** The unique ID for the contact in Customer Engagement, used for subsequent API-calls to get all relevant information about a contact * **email:** The contact's email address * **id:** The contact's identification key as defined in Customer Engagement (for example, "email") * **dateTime:** The time and date the link was generated (V.5) * **discoveryKey:** The contact's identifier for Voyado Product Discovery (optional) To include the `discoveryKey` from Voyado Product Discovery, ask your Voyado team to set it up for you. Be careful about the access a user is granted via a soft login. A soft login link is a convenient method of identification but it can also be easily shared in error, for example by a user forwarding an email containing the link to a friend. ## Decoding the message Extract the value string from the parameter “eClub” Decoding the string from URL-safe Base64": * Replace all “-” with “+” * Replace all “\_” with “/” * Add padding characters, based on the length * Decode the string using normal Base64 decoding The initialization vector is stored in the first 16 bytes; the remainder is the encoded message" With the shared key and initialization vector from step 3, decrypt the message using AES-256 (CBC) ## Encoding the message The data is sent as a JSON with the structure shown in the example above. This JSON string is then encrypted using AES-256 and the shared key which both the customer and Voyado have (it was exchanged during implementation) and a unique initialization vector for each message. The initialization vector and the encrypted string are concatenated, Base64 encoded and modified for URL inclusion (URL-safe Base64). The complete encoded string is then sent as the query-string parameter “eClub”. The shared key used is 32 characters long, using no special characters, only a-z, A-Z, 0-9. An example of such a key is: `DaqfT7Ys6tGqM3zbesEKpmacTJEqBCp2`. There are online tools available for creating keys, such as [Strong Random Password Generator](https://passwordsgenerator.net/). ```csharp theme={null} public static string Decrypt(string keyString, string stringToDecrypt) { var key = Encoding.UTF8.GetBytes(keyString); stringToDecrypt = stringToDecrypt.Replace('-', '+').Replace('_', '/'); switch (stringToDecrypt.Length % 4) { case 2: stringToDecrypt += "=="; break; case 3: stringToDecrypt += "="; break; } var bytes = Convert.FromBase64String(stringToDecrypt); var iv = new byte[16]; var text = new byte[bytes.Length - 16]; Array.Copy(bytes, iv, 16); Array.Copy(bytes, 16, text, 0, text.Length); string plaintext = null; using (var aes = Aes.Create()) { aes.Padding = PaddingMode.PKCS7; aes.IV = iv; aes.Key = key; aes.Mode = CipherMode.CBC; var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); using (var msDecrypt = new MemoryStream(text)) { using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) { using (var srDecrypt = new StreamReader(csDecrypt)) { plaintext = srDecrypt.ReadToEnd(); } } } } return plaintext; } ``` ```perl theme={null} use Crypt::Mode::CBC; use MIME::Base64::URLSafe; my $key = 'THE SHARED KEY'; my $input = 'THE ENCRYPTED VALUE'; $input = urlsafe_b64decode($input); my $iv = substr($input, 0, 16); my $data = substr($input, 16); my $cbc = Crypt::Mode::CBC->new('AES'); my $decrypted = $cbc->decrypt($data, $key, $iv); ``` ```php theme={null} function decrypt($key, $encrypted) { $encrypted = base64_decode(strtr( $encrypted, '-_', '+/') . str_repeat('=', 3 - ( 3 + strlen( $encrypted )) % 4 )); $iv = substr($encrypted, 0, 16); $encrypted = substr($encrypted, 16); $decrypted = openssl_decrypt($encrypted,'aes-256-cbc',$key,OPENSSL_RAW_DATA, $iv); return $decrypted; } ``` ```javascript theme={null} const crypto = require('crypto'); const stringToDecrypt = "INSERT STRING TO DECRYPT HERE"; const keyString = "INSERT KEY HERE"; const key = Buffer.from(keyString, 'utf8'); let stringToDecryptFixed = stringToDecrypt.replace('-', '+').replace('_', '/'); switch (stringToDecryptFixed.length % 4) { case 2: stringToDecryptFixed += "=="; break; case 3: stringToDecryptFixed += "="; break; } const bytes = Buffer.from(stringToDecryptFixed, 'base64'); const iv = Buffer.alloc(16); const text = bytes.slice(16); bytes.copy(iv, 0, 0, 16); let plaintext = null; const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); decipher.setAutoPadding(false); plaintext = Buffer.concat([decipher.update(text), decipher.final()]).toString('utf-8'); console.log(plaintext); ``` # NPS Source: https://developer.voyado.com/docs/contacts/nps NPS stands for *Net Promoter Score*. This is a common metric used in customer experience programs. A customer's NPS measures a customer's loyalty by looking at how likely they are to recommend a given business. It is given as an number on a scale of 1 to 10. The NPS endpoint is only available in the Customer Engagement v3 API. ## Adding NPS for contact An NPS score can be added for a specific contact using the following v3 endpoint: ```http theme={null} POST /api/v3/contacts/{contactId}/createNpsResponse ``` Unique identifier of the contact, added in the path as shown. Sent in query string. The actual NPS value for this contact, from 1 - 10. An example request would then be: ```http Example request theme={null} POST /api/v3/contacts/2e40b702-2767-4abe-ae99-aa510108e22b/createNpsResponse?grade=5 ``` ## Responses The possible responses from this request are: * *200 OK*: The POST was successful and the NPS value was saved for the contact. * *400* InvalidGrade: The grade value was missing or not in the allowed range. * *404* ContactNotFound: No contact was found with the given `contactId`. # Security best practices Source: https://developer.voyado.com/docs/contacts/security-best-practices Using open web forms to add contacts to a database comes with inherent risks. These range from malicious attacks that exploit vulnerabilities to attempts to disrupt businesses. Here you'll see how to mitigate these kinds of risks and uphold the integrity and security of your Customer Engagement account. ## **Open sign-up forms and the Customer Engagement API** Access to the Customer Engagement API using your API key should, like any online service, be handled in a safe and secure way. For example, if you have created an open online sign-up form, this could be abused by a malicious actor to register non-existing contacts. And as long as the sign-ups from that form don't exceed the API's rate limit, Customer Engagement then has no way to tell that these are not legitimate contacts. This clearly causes other problems, such as polluting your contact pool with fake accounts, skewing your BI reports, and opening the possibility of sending out SMS to these fake contacts (if you have set up automations to do this) which can incur actual costs. Therefore this safety aspect needs to be considered and planned for. ## **Mitigating the risks** Here's how to think, prepare and act concerning these kinds of risks. ### **Identify the entry points** Start by identifying all the various entry points that malicious entities can exploit, including custom-built forms, API integrations, and third-party tools used for data collection. ### **Minimize the influx of bad entries** * Keep an eye on your **contact database growth** in the Customer Engagement dashboard or through reports. This will allow you to spot abnormal growth rates in your database. * Implement **data validation techniques** to verify the authenticity of personal information and data points. [See the contact field definitions](/docs/contacts/contact-field-definitions). * Use **unique identifiers**, such as email addresses and mobile phone numbers, to prevent duplication of contacts and enforce data integrity. Check with your Voyado team to ensure that those identifiers are actually set to be unique in your Customer Engagement environment. * **Cross-reference** new contact registration with those already existing to detect potential duplicates or suspicious entries. [See this article on identifying contacts](/docs/contacts/identify-contact/find-contact). * Implement **CAPTCHA mechanisms and double opt-in processes** ([see more here](https://help.engage.voyado.com/hc/en-gb/articles/17253837916316-Working-with-double-opt-in)) to add an additional layer of security and verification for sign-ups. ### **Reduce the costs of bad SMS send-outs** * Implement a **whitelist** on registration forms to validate phone numbers based on specified country codes. This will reduce the likelihood of sending SMS to erroneous or international numbers. Ask your Voyado team to help you configure this. * Use **segmentation** in automation processes to target specific audiences, which minimizes the risk of unnecessary SMS dispatches. * Consider buying **prepaid packages of SMS messages**, creating a ceiling on the number that can be sent out per month. This helps minimize losses in the event of a malicious incident. ## **Summary** By sticking to these best practices and being vigilant about potential security threats, you can maintain the integrity of your contact database. Proactive measures, coupled with continuous monitoring and adaptation, are key to mitigating these risks and ensuring a secure onboarding and contact creation process. # Target audiences Source: https://developer.voyado.com/docs/contacts/target-audiences One way of grouping your contacts in Customer Engagement is with **target audiences**. A target audience is a grouping of contacts that's recalculated on a fixed schedule (every night is standard). The contacts in a target audiences can therefore change from day to day. For example, some people will be having a birthday, some will have made their first purchase, some might have just moved to Limerick, and others might have just left Limerick, vowing never to return. Every contact can belong to many different target audiences. Other ways of grouping contacts are by **segmentation** (an on-the-fly grouping based on the current state of all contacts) and **labels** (a more static way of grouping since, once given a label, a contact will have it until it is removed). ## **Fetching target audiences** With target audiences you can profile your customers more strategically, using them as building blocks for your day-to-day campaigning, and building up a library of target audiences based on life cycle stages, personas, geographic location, product category affinity, or whatever is of interest. You can also expose these audiences in external applications, for example to further personalize a customer's experience in a sales situation in stores or online. To do this, you'll need to know which audiences exist in your Customer Engagement instance. To fetch all target audiences in your Customer Engagement instance, use this endpoint: ```http theme={null} GET /api/v3/target-audiences ``` There are no parameters or payload required in this request. ## **Structure of response** A successful request gives a **HTTP 200** response along with data in this format: ```json Target audiences response theme={null} [ { "id": "91a2ea56-c08b-48fe-b510-ffff006aedde", "name": "Svenska medlemmar" }, { "id": "5b195078-0f72-4206-ac1a-aaaa006ce19e", "name": "Silver" }, { "id": "1ef5e951-a0f1-4822-b69e-cccc006cf38a", "name": "Member" } ] ``` Each JSON object in this array represents a target audience: The unique ID for that target audience. Currently not useful, but a future endpoint could be used to return all the contacts in this target audience using this ID. The name of the target audience as entered when the audience was created. If you need to fetch all the target audiences that a specific contact belongs to, you can do it using the [contact overview endpoint](/docs/contacts/identify-contact/get-enriched-profile). # Update contact Source: https://developer.voyado.com/docs/contacts/update-contact Updating the details of a Voyado Customer Engagement contact is done through the /contacts endpoint using their unique `contactId` and the PATCH method: ```http theme={null} PATCH /api/v3/contacts/{contactId} ``` If you don’t have the `contactId`, [you can get it using other contact attributes.](/docs/contacts/identify-contact/find-contact) This PATCH endpoint is to be used for single contact updates. Do NOT use it for bulk updates of contacts. In that case, use the [bulk methods provided](/docs/contacts/bulk-contacts-api). When you have the `contactId` you call the endpoint like this: ```http theme={null} PATCH /api/v3/contacts/430d3329-7eef-45c0-37f9-ae9300a0daceid ``` As the payload, send ONLY the attributes that you want to change: ```json theme={null} { "lastName": "Examplesson", "street": "New Street 1337" } ``` In this example, the contact has changed their surname and moved to a new address. These attributes will be updated. When updating contacts over the API be sure to ONLY send the fields you want to update and nothing else. This applies for individual as well as bulk updates. Any empty fields added to your update payload (string.Empty, null, "", etc.) will overwrite the current values on the contact. You can't use this endpoint to update a contact's contact type. This has to be done using the `/contacts/[contactId]/updateContactType` endpoint. See the next section. ## Changing contact type To update `contactType` to "Member" or to another value, you will use this endpoint: ```http theme={null} POST /api/v3/contacts/{contactId}/updateContactType?contactTypeId=Member ``` In this example we are changing the contact's type to "Member". Be aware that the `contactId` is not changed when you change the `contactType`. This endpoint is the only way to update a contact's `contactType`. If you try to use the PATCH method shown above to change `contactType` it will not work. ## Don't send empty values You need to be careful to never send empty values in the JSON payload when updating a contact. If a field exists in the payload, Customer Engagement will use that value and overwrite the existing one. For example, if a contact's last name isn’t added in the POS, do *not* add that field to the payload. ```json DO NOT DO THIS! theme={null} { "firstName": "Example", "lastName": "", "street": "Big Street 1234" } ``` In this case, the current value of lastName will be overwritten by an empty string. You probably don't want this. Instead, do this: ```json This is how to do it theme={null} { "firstName": "Example", "street": "Big Street 1234" } ``` Note also that a field which does not exist in the contact's data will be ignored and no error will be returned. This means it is important to make sure you are sending the right name for each field. ```json This update will be ignored theme={null} { "firstNam": "Example", } ``` ## Responses A successful update will return *HTTP 200 Ok* along with the contact's new data. If the request has failed, you'll get one of the following HTTP error codes: * *400 : NoData* * *404: Not Found* # Introduction to BI Export Source: https://developer.voyado.com/docs/data-access/bi-export BI Export delivers your Customer Engagement data as *structured CSV files on your FTP server*. The data is cleaned, curated, and organized into ready-to-use exports. You don't need to build ingestion logic or transform the data yourself before analyzing it. ## What is an export? An **export** is a flat-file dataset for a single entity in Customer Engagement, delivered as CSV files. For example: * The "Receipt" export is one file with a list of all transaction headers * The "Message" export is one file with a list of all messages and their settings * The "Contact" export provides a list of all contacts in your Customer Engagement environment delivered as one file per contact type Some important points about exports: * Each export runs independently and produces its own file (or files). * Exports are performed once per day, during the night. * The exact time of the export is configurable in the Customer Engagement back-end. * A single export can include more than one row for the same item. If an item was updated several times between two export runs, each of those changes is included, so the file may contain multiple rows for that one item. Contact your implementation manager or CSM if you want to make changes to the export schedule. While Delta Share delivers data as "data products" (packaged datasets that group multiple related tables around one business domain) BI Export delivers one table per file, per entity. ## How exports relate Although each export is delivered as an independent file, the exports are *designed to be joined together*. Every export contains shared identifier fields (GUIDs) that let you reconstruct relationships across the dataset. * Most exports revolve around "Contact" as the central entity * From there, "Receipt" and "ReceiptItem" connect via the `receiptId` value * Messages and their delivery events connect via `messageId` and `deliveryId` * And stores are joined via `storeId` This matters when you plan your analytics. You will typically load multiple exports into your warehouse or BI tool and join them to generate insights. Here are some analysis examples: * Combine the "Receipt" and "ReceiptItem" exports using the `receiptId` value * Add also "Contact" using `contactId` to gain customer-level insight Using the `messageId` and `deliveryId` values, join these four exports: * "Message" * "MessageDelivery" * "MessageOpen" * "MessageClick" * Join the "Receipt" and "Store" exports using `storeId` Using the `contactId` value, join these exports: * \`Contact * "PointAccount" * "PointTransaction" ## BI exports available Exports marked `*` are only available for customers using Design Studio. ### Contact and loyalty Contact records per contact type. Exported columns are configurable Consent records and associated fields. Point balances per contact. All point transactions. Predictive scoring fields from the contact card. ## Transactions Transaction headers. One row per transaction. Joined to "ReceiptItem" export via `receiptId`. Transaction line items. Each row links back to its "Receipt" export header. ## Messages & campaigns All created messages and their settings. The root of the message-related exports. Which contacts received a message. Excludes control groups. Delivery status per sendout (Delivered, Blocked, and so on). Email-open events including user agent data. Click events with timestamps and target URLs. Contacts that were assigned to the control group for a message. ## Promotions Promotion template information. Recipients and their promotion status. ## Segmentation All labels stored in Customer Engagement. Contacts with labels and which labels are applied. All saved searches. Which contacts belong to which target audience. Contacts included in the master control group. ## Stores All store records. Joined to "Receipt" export via `storeId`. ## Design Studio With the introduction of the Design Studio, new BI exports were introduced to Customer Engagement, enabling BI teams to analyze campaign structure, market distribution, and cross-market performance. In these exports, the central concepts are sendouts and marketing groups. The Email, Sendout and MarketingGroup exports are *only* available for customers using Design Studio. Email-level data for sendouts created in Design Studio. Records of sendouts created and managed in Design Studio. The marketing Groups used to segment contacts by market. ## Accessing your exports To set up BI Export for your Customer Engagement instance, follow the setup guide on Help Center: Your exports, once processing is complete, are placed on Customer Engagement's internal FTP server. ```http theme={null} [base-ftp-url]/integration/export ``` The Customer Engagement FTP server should only be used for transfer of BI export files and NOT for storage. Files older than 180 days will be automatically removed by housekeeping jobs. ## Additional information * Exports run once per day. If you need higher-frequency access, consider using Delta Share. * The columns in the "Contact" export are configurable. To add or remove files, or to adjust exported fields, contact Support. * Exports marked `*` above require Design Studio. ## Learn more # Consent Source: https://developer.voyado.com/docs/data-access/bi-exports/consent | Field | Description | Data type | Example value | | - | - | - | - | | id | Row ID | Integer | 212 | | contactId | The unique contact ID | GUID | a72d5017-827c-40dd-8585-abc90086e0ac | | alias | Consent alias | string | myConsent | | value | Response | True/False | True | | consentDateTime | In UTC. Date and time when consent was made. | datetime | 2020-07-28T21:59:59+01:00 | | source | Where the consent was made | string | ContactForm | | comment | A comment on the consent | string | Any comment | # Contact Source: https://developer.voyado.com/docs/data-access/bi-exports/contact Presented below are the standard fields for this export. The contact export also allows clients to choose which fields to be included in the export. Note that additional custom fields can be created and exported from Customer Engagement depending on client requirements. | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | id | The unique contact ID generated by Customer Engagement | GUID | 0C1216A2-E0F2-4682-A377-A982009D737A | | isDeleted | Whether a contact is deleted or not | boolean | 0 | | deletedOn | When the deletion happened | datetime | 2020-01-01T00:00:00.000+01:00 | | deletionReason | Why the deletion happened | varchar | DECEASED, MANUALLY\_REMOVED | | firstName | First name | string | Jane | | lastName | Last name | string | Doe | | street | Street address | string | Janes street 12 | | zipCode | Postal code | string | 123 45 | | city | City | string | Stockholm | | email | Email address | email | [jane.doe@voyado.com](mailto:jane.doe@voyado.com) | | mobilePhone | Mobile phone number | mobilephone | +46123456789 | | countryCode | Country code | countrycode | SE | | socialSecurityNumber | Social security number (actually the Swedish the personal identity number or Finnish personal identity code) | socialsecuritynumber | 199101010101 | | gender | Gender | string | Female | | age | Age | int | 29 | | createdInStore | Recruitment store | string | 03591CA1-706A-4F9D-89DA-A97600D23FB7 | | store | Regular store | string | 51C5FFB2-B8E9-455B-9102-A976009CACEA | | careOf | Care of address | string | | | country | Country | string | Sweden | | birthDay | Birth date | date | 1991-01-01 | | memberNumber | Customer number (If Customer Engagement is master) | sequence | 1234567890 | | nps.grade | Latest grade set by the customers | int | 8 | | nps.datetime | Date and time of when the grade was set | datetime | 2020-01-01T00:00:00.000+01:00 | | nps.averagegrade | Average score of all grades from the specific contact | number | 9,75 | | rfm | RFM score | number | 86.00000 | | retail.keyvalue.recency | How recently the contact made a purchase | int | 8 | | retail.keyvalue.frequency | How frequent the contact makes purchases | int | 9 | | retail.keyvalue.monetary | How much money the contact has spent | int | 8 | | registrationDate | Date and time of registration date | datetime | 2020-01-01T00:00:00.000+01:00 | | lastModifiedTime | Date and time of when the contact was last modified | datetime | 2020-01-01T00:00:00.000+01:00 | | contactPreferencesAcceptsEmail | If the contact accepts email communication | boolean | true | | contactPreferencesAcceptsSms | If the contact accepts sms communication | boolean | true | | contactPreferencesAcceptsPostal | If the contact accepts postal communication | boolean | true | | isApproved | If the contact is approved and verified in Customer Engagement | boolean | true | | statusEmail | If the contact is reachable by email | string | Active / Inactive | | statusSms | If the contact is reachable by sms | string | Inactive | | statusPostal | If the contact is reachable by postal | string | Active / Inactive | | storeExternalId | Regular Store (External id) | string | 123 | | createdInStoreExternalId | Recruited by store (ExternalId) | string | 123 | | retail.keyvalue.purchaseamounttotal | Total purchase amount | decimal | 1000.00 | | retail.keyvalue.numberofarticlepurchasestotal | Total number of purchased articles | decimal | 10.00 | | retail.keyvalue.averagereceipttotal | Average receipt amount of all purchases | decimal | 100.00 | | retail.keyvalue.latestreceiptdate | When the last purchase was made | datetime | 2020-01-01T00:00:00.000+01:00 | | retail.keyvalue.purchasefrequencytotal | Total number of purchases | decimal | 10.00 | | retail.keyvalue.purchaseamountlast12months | Total purchase amount over the last 12 months | decimal | 1000.00 | | retail.keyvalue. numberofarticlepurchaseslast12months | Total number of purchased articles over the last 12 months | decimal | 10.00 | | retail.keyvalue.averagereceiptlast12months | Average receipt amount on all purchases made in the last 12 months | decimal | 100.00 | | retail.keyvalue.purchasefrequencylast12months | Total number of purchases made in the last 12 months | decimal | 10.00 | | retail.keyvalue.purchaseamountlast24months | Total purchase amount in the last 24 months | decimal | 1000.00 | | retail.keyvalue. numberofarticlepurchaseslast24months | Total number of purchased articles in the last 24 months | decimal | 10.00 | | retail.keyvalue.averagereceiptlast24months | Average receipt amount on all purchases made in the last 24 months | decimal | 100.00 | | retail.keyvalue.purchasefrequencylast24months | Total number of purchases made in the last 24 months | decimal | 10.00 | Member level fields are also available (for example, member.level) but the exact member fields and their names can vary depending on which member level model you are using. # Design Studio exports Source: https://developer.voyado.com/docs/data-access/bi-exports/design-studio These exports available contain information on Marketing Groups used in Design Studio, email-level data related to sendouts, and information about the actual sendouts. These exports are *only available* for customers using Design Studio. ## Email | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | messageId | Unique identifier for the message. Not related to export 'message'. | GUID | 7740afa3-55fe-43c5-95d1-4955de1c82c9 | | name | Name of the email. | string | My email title | | sendoutId | Unique identifier for the sendout, related to the export 'sendout'. | GUID | d4cb058b-de97-49a4-9f83-ab21693c4737 | | marketingGroupId | Unique identifier for the marketing group. | GUID | 71f54188-3b3a-4e14-be57-898369032438 | | abVariant | If AB test, the variant name. | string | Variant name | | percentage | If AB test, the variant percentage. | int | 10 | | scheduledSendTime | Scheduled send time for the message | datetime (UTC) | 2026-02-24T07:51:15Z | ## Marketing Group | Field | Description | Data type | Example value | | - | - | - | - | | id | Identifier for the marketing group | GUID | fe4b4f4d-bc55-4c0b-9012-739ebd50e972 | | name | Name of the marketing group | string | Sverige | | timeZoneId | Time zone for marketing group. Used to send messages in marketing group's local time. | string | W. Europe Standard Time | ## Sendout | Field | Description | Data type | Example value | | - | - | - | - | | sendoutId | The unique identifier of the Sendout. | GUID | d4cb058b-de97-49a4-9f83-ab21693c4737 | | name | The name of the Sendout. Set by the user. | string | Sendout name | | description | A description of the Sendout. Set by the user. | string | Sendout description | | type | Type is either 'Manual' or 'Automation'. | string | Manual | | createdOn | Timestamp when the sendout was created. | DateTimeOffset | 2026-01-13T09:19:31.5425806+01:00 | | modifiedOn | Timestamp when the sendout was modified. | DateTimeOffset | 2026-01-13T09:29:54.7288529+01:00 | # Labels Source: https://developer.voyado.com/docs/data-access/bi-exports/labels ## Labels | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | id | Internal ID for the label | GUID | 83E4499D-6666-48F9-8E0D-A6B001157BC5 | | type | Type of label | string | Contact Label | | name | Name of label | string | Label Test | | changeType | Whether the row has been created, updated or deleted. | string | CREATE/UPDATE/DELETE | ## LabelsToTaggedContact This export returns only those contacts that are tagged with labels, creating a row for every label a contact has. | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | labelId | Internal ID for the label | GUID | 11E2299D-6666-48F9-8E0D-A6B001157BC5 | | contactId | ID of the contact who the label is attached to | GUID | 99E4499D-7777-98F9-8E0D-A6B001157BD2 | | changeType | Whether the row has been created, updated or deleted. | string | CREATE/UPDATE/DELETE | # Master control group history Source: https://developer.voyado.com/docs/data-access/bi-exports/mcg-history Master control group (MCG) is an established tool for excluding parts of the contact database from all communication over a period of time. Usually the purpose of exclusion with master control group is to measure the effect of marketing activities over periods of time. | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | contactId | The unique contact ID | GUID | a72d5017-827c-40dd-8585-abc90086e0ac | | startDateTime | When the contact was put in the control group | datetime | 2020-06-28T21:59:59+01:00 | | endDateTime | When the contact was taken out of the control group | datetime | 2020-07-28T21:59:59+01:00 | | isInMasterControlGroup | If a contact is in this control group | boolean | true | | changeType | Not applicable | string | CREATE/UPDATE/DELETE | # Message & delivery Source: https://developer.voyado.com/docs/data-access/bi-exports/message-and-delivery ## Message The "Message" export is only applicable if you are using the classic (old) editor. | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | messageId | Unique ID for the message. | GUID | BA270CBB-EAEA-4237-AA4A-A53100909FA2 | | messageName | Name of the message. | string | 2020-06 Summer season | | subject | Subject line of the message. | string | "Digitalize your memories before summer" | | status | Sending status of the message. | string | **scheduled**, **inprogress** or **completed** | | scheduledDateTime | When the message started being processed. | datetime | 2012-04-14 16:10:21+01:00 | | recipientCount | Total number of recipients for the message (0 for postal). | int | 123 134 | | channel | Which communication channel the message was sent through. | string | **sms**, **email** or **postal** | | sendList | Which send-list the message was connected to. (Send lists are unique for each client). | string | **Campaign**, **Newsletter** | | messageSource | Whether the message was sent manually or through an automation. | string | Automation | | isTransactional | Tells if a message was sent transactionally (if true, it doesn't take the opt-out into account). | boolean | True | | controlGroupPercent | Shows the percentage of recipients that comprised the control group of the message sendout. | decimal | 5,0 | | senderStoreId | Presents the store ID if the message was sent for a specific store. | string | 101 | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | ## MessageDelivery | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | deliveryId\* | Unique delivery ID for each contact | GUID | 4A220CBG-EAEA-4237-A59A-B98101209FA2 | | messageId | Unique ID for the message | GUID | BA270CBB-EAEA-4237-AA4A-A53100909FA2 | | contactId | Unique contact ID | GUID | 5307c938-55fc-4fd8-9512-aab500977e67 | | deliveryDateTime | Delivery date or planned delivery date for postal sendouts. | datetime | 2020-04-14 15:12:33+01:00 | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | In this export `deliveryId` is the unique value. ## MessageDeliveryStatus | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | deliveryId | Unique delivery ID for each contact | GUID | 4A220CBG-EAEA-4237-A59A-B98101209FA2 | | deliveryStatus | The current status of the delivery. | string | **delivered**, **waiting**, **hardbounce**, **softBounce** | | optOutDateTime | Timestamp for when the customer unsubscribed from email communication. | datetime | 2020-04-15 15:19:33+01:00 | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | ## MessageOpen | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | openId | Unique open ID (Several IDs can be connected to one delivery) | GUID | 9B520C7G-YAEA-4237-A59A-B98101209FA2 | | deliveryId | Unique ID for delivery | GUID | 4A220CBG-EAEA-4237-A59A-B98101209FA2 | | messageId | Unique ID for the message | GUID | BA270CBB-EAEA-4237-AA4A-A53100909FA2 | | openDateTime | Timestamp when customer opened the email. | datetime | 2020-04-15 18:19:33+01:00 | | userAgent | In which platform the contact opened the email. | string | Mozilla/5.0 (Macintosh; Intel Mac OS X 10\_11\_6) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0.2 Safari/602.3.12 | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | ## MessageClick | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | clickId | Unique ID for logged click (Several IDs can be connected to one delivery) | GUID | 7C420CBG-RARA-3437-A59A-B98701209FA2 | | messageId | Unique ID for the message | GUID | 6A220CBG-EAEA-4237-A59A-B98101209FA2 | | deliveryId | Unique ID for the delivery | GUID | 4A220CBG-EAEA-4237-A59A-B98101209FA2 | | clickDateTime | Timestamp on when the customer clicked a link in the email. | datetime | 2020-04-15 18:19:33+01:00 | | url | The link adress which was clicked. | string | [https://integrate.voyado.com](https://integrate.voyado.com) | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | ## MessageControlGroup | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | messageControlGroupId | Unique control group ID (identity) | int | 12345 | | messageId | Unique ID for the message | GUID | BA270CBB-EAEA-4237-AA4A-A53100909FA2 | | contactId | Unique ID for the contact | GUID | 5307c938-55fc-4fd8-9512-aab500977e67 | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | # Point account Source: https://developer.voyado.com/docs/data-access/bi-exports/point-account | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | pointAccountId | ID of the point account | Integer | 1 | | contactId | Unique ID for the contact | GUID | DFD03C68-592C-463F-A46D-A86400011A28 | | pointDefinitionId | The ID for the points "type" | Integer | Default is 1 | | balance | The current points total | Decimal | 150 | | modifiedOn | The date of the last change | datetime | 2020-07-30T08:11:04 | | changeType | Not applicable. | string | Whether the row was created, updated or deleted (CREATE / UPDATE / DELETE) | # Point transaction Source: https://developer.voyado.com/docs/data-access/bi-exports/point-transaction | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | pointTransactionId | The transaction's ID | Integer | 120912726 | | pointAccountId | ID of the point account | Integer | 1 | | transactionType | What kind of transaction this is | String | Addition, Deduction, Expiry, Cancellation | | source | Where the transaction was created | String | Automation, Purchase, Manual, Return, ReceiptAdjustment, BonusPromotion, BonusPromotionReturn, RewardVoucher, Adjustment, BonusBalanceAdjustment, UpdateBalance, PointShop, PointTransactionCancellation | | amount | The transaction amount | Decimal | 150 | | description | A description of the transaction | String | Chainmail bought | | ValidFromUTC | The date from which the transaction is valid | DateTime | 2020-06-28T21:59:59Z | | ValidToUTC | The last date the transaction is valid on | DateTime | 2020-06-28T21:59:59Z | | transactionDate | When the transaction was registered | DateTime | 2020-06-28T21:59:59Z | | changeType | Not applicable. | String | Only CREATE is applicable here. | | ContactID | Unique ID for the contact | GUID | DFD03C68-592C-463F-A46D-A86400011A28 | # Predictive scoring Source: https://developer.voyado.com/docs/data-access/bi-exports/predictive-scoring | **Field** | **Type** | **Description** | | :- | :- | :- | | contactId | GUID | 1E46D85F-2B20-43E1-8E12-A53100909F8F | | averageDaysBetweenPurchases | decimal | Average days between purchase ex: 123.4 | | purchasedRecently | boolean | true/false | | yearsLeftAsActiveCustomer | decimal | Days left ex: 123.4 | | activeSegment | string | Active, Declining or Leaving. Actionable scoring buckets 0-0.5, 0.51-0.75, 0.76-1.0 | | churnDate | date | The predicted date that this customer will be lost | | churnScore | decimal | Probability scoring for losing the customer | | clv | Decimal/Currency | Currency | | changeDateTime | datetime | Datetime when row was changed | | changeType | | Insert/Update/Delete | # Promotions Source: https://developer.voyado.com/docs/data-access/bi-exports/promotions ## Promotions | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | promotionId | The unique ID for the promotion template | GUID | 33E4499D-1259-48F9-8E0D-A6B001157EC5 | | promotionName | Given name of the promotion | string | 20% discount on purchase - Welcome offer | | promotionType | Type of the promotion | string | LoyaltyBar, Code, MobileSwipe, BonusCheck | | externalId | The external promotion ID, set by API only. | string | ABC123 | | validFromDateTime | Valid from | datetime | 2020-06-30T09:00:00+01:00 | | validToDate | Valid to | datetime | 2020-07-30T00:00:00+01:00 | | externalPromotionCode | If type is **BonusCheck** this contains the bonus check (voucher) number. For multichannel promotions this will never have a value. | string | 24587495 | | createdOn | When the promotion template was created | datetime | 2020-05-30T00:00:00+01:00 | | changedOn | When the promotion was last changed | datetime | 2020-07-29T10:00:00+01:00 | | deletedOn | When the promotion was deleted | datetime | 2020-07-30T00:00:00+01:00 | ## **PromotionRecipient** | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | promotionRecipientId | The unique ID for each promotion assignment | GUID | 66E4499D-1259-48F9-8E0D-A6B001157EC5 | | promotionId | The unique ID for the promotion template | GUID | 33E4499D-1259-48F9-8E0D-A6B001157EC5 | | contactId | The unique contact ID | GUID | 99E4499D-3459-48F9-8E0D-A6B001157ED7 | | isUsed | Whether the promotion has been used or not | boolean | false | | usedInStoreId | In which store the promotion was used | string | 1001 | | UsageDateTime | When the promotion was used | datetime | 2020-07-29T12:00:00+01:00 | | validFromDateTime | When the promotion is valid from | datetime | 2020-05-30T00:00:00+01:00 | | validToDateTime | When the promotion is valid until | datetime | 2020-07-29T10:00:00+01:00 | | createdOnDateTime | When the promotion was created | datetime | 2020-07-30T00:00:00+01:00 | | receiptExternalId | The unique external ID for the receipt | string | 4646f3ef-732a-4bae-a352-ae8ae843e003 | | value | Value on promotion | decimal | 73.0000 | | valueType | Type of value | String | EXTERNALOFFER, PERCENT, MONEY | | changeType | Type of change | String | CREATE, DELETE | # Receipt & items Source: https://developer.voyado.com/docs/data-access/bi-exports/receipt-and-items Below is a description of two different exports. One is the export of the receipt header and the other export contains the line-items of each receipt. The receiptId is the main key-reference here in order to connect the two tables. ## Receipt | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | receiptId | Receipt ID (Generated by Customer Engagement) | GUID | BA270CBB-EAEA-4237-AA4A-A53100909FA2 | | contactId | Unique contact ID | GUID | 5307c938-55fc-4fd8-9512-aab500977e67 | | storeId | Internal unique store ID where the purchase was made. (Generated by Customer Engagement) | GUID | HJ270CBB-EAEA-4237-AA4A-743100909FA2 | | storeExternalId | The store ID generated by the client | string | 1001 | | receiptNumber | The invoice number | string | 3247265354287 | | totalGrossPrice | The total purchase amount | decimal | 500.00 | | totalQuantity | The total quantity of all items | int | 4 | | totalReturned | The number of returned items in one receipt | int | 2 | | totalLocalPrice | The total purchase amount in local currency | decimal | 50.00 | | exchangeRate | The currency exchange rate | decimal | 10.00 | | localCurrency | The currency code | string | EUR | | numberOfItems | The number of items in the receipt | int | 4 | | numberOfPurchases | The number of items of type PURCHASE | int | 2 | | numberOfReturns | The number of items of type RETURN | int | 2 | | createdOnDateTime | The date and time of the transaction | datetime | 2024-10-23T20:26:38Z | | externalId | The unique receipt ID | string | 123-123456789 | | changeType | Whether the row was created, updated or deleted. | string | CREATE/UPDATE/DELETE | ## ReceiptItem | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | receiptItemId | Item ID (Generated by Customer Engagement) | GUID | JU270CBB-K9EA-4237-A6TA-B90300909FA2 | | receiptId | Receipt ID (Generated by Customer Engagement) | GUID | BA270CBB-EAEA-4237-AA4A-A53100909FA2 | | contactId | Unique contact ID | GUID | 5307c938-55fc-4fd8-9512-aab500977e67 | | storeId | Unique store ID (Generated by Customer Engagement) | GUID | HJ270CBB-EAEA-4237-AA4A-743100909FA2 | | transactionDateTime | When the purchase was made | datetime | 2020-06-30T10:34:00+01:00 | | type | PURCHASE or RETURN | string | PURCHASE | | sku | Store keeping unit | string | 12314-5 | | articleNumber | The article number | string | 12314 | | quantity | Quantity | int | 2 | | grossPaidPrice | Summed price of all units | decimal | 100.00 | | grossPaidPricePerUnit | Price per unit | decimal | 50.00 | | localPrice | Price in local currency | decimal | 10.00 | | localCurrency | Currency code | string | EUR | | discounts | Total amount of discount on line item | decimal | 10.00 | | discountPercent | Discount percentage | decimal | 10.00 | | marginPercent | The margin percentage of sold item. | decimal | 50.00 | | taxPercent | VAT percentage for item | decimal | 25.00 | | articleName | Name of the article | string | Voyado cap | | articleGroup | Highest level of categorizing | string | Hats | | articleSpare1-10 | Up to 10 extra fields | string | Size medium | | articleDateSpare1-5 | Up to 5 extra datetime fields | datetime | 2020-01-02T03:04:05+01:00 | | awardsBonus | If item is valid for bonus | boolean | true | | changeType | Whether the row was created, updated or deleted | string | CREATE/UPDATE/DELETE | # Saved search & target audiences Source: https://developer.voyado.com/docs/data-access/bi-exports/saved-search-target-audience These exports present information on your segmentations and target audiences. If the value of **savedSearchIsTargetAudience** in the `SavedSearch` export is "true", you can then use the **savedSearchId** value to pull relevant data for this contact and target audience from the `TargetAudienceToContact` export (further down). ## SavedSearch | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | savedSearchId | ID for the saved search generated in Customer Engagement | GUID | a72d5017-827c-40dd-8585-abc90086e0ac | | savedSearchedName | The name of the search | string | Best customers | | createdOnDateTime | Date and time of creation | datetime | 2020-05-28T08:11:04 | | changedOnDateTime | Date and time of latest change | datetime | 2020-05-30T08:11:04 | | deletedOnDateTime | Date and time of deletion | datetime | 2020-05-30T08:11:04 | | changeType | Whether the row was created, updated or deleted | string | CREATE/UPDATE/DELETE | | savedSearchIsTargetAudience | Whether the saved search is a target audience | bool | true / false | ## TargetAudienceToContact This export tells you when contacts entered and exited each of your target audiences. | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | id | Unique ID for each contact and target audience connection | GUID | EBC34A3F-42D8-4EA1-804B-A94C00A9AB50 | | contactId | Unique ID for the contact | GUID | DFD03C68-592C-463F-A46D-A86400011A28 | | savedSearchId | Reference to SavedSearch | datetime | a72d5017-827c-40dd-8585-abc90086e0ac | | startDateTime | Date and time when contact entered the target audience segment | datetime | 2020-05-30T08:11:04 | | endDateTime | Date and time when contact left the target audience segment | datetime | 2020-07-30T08:11:04 | | changedTyped | Not applicable | | | # Store Source: https://developer.voyado.com/docs/data-access/bi-exports/store | **Field** | **Description** | **Data type** | **Example value** | | :- | :- | :- | :- | | storeId | The store ID recorded in Customer Engagement | GUID | 41500a95-b34b-4b8b-a342-ab9001393789 | | name | Name of the store | string | Voyado flagship store | | isDeleted | If the store is deleted | boolean | false | | city | Store's city location | string | Stockholm | | country | Store's country location | string | Sweden | | zipCode | Store's postal code | string | 111 43 | | street | Store's physical address | string | Kungsgatan 1 | | region | Store's region | string | Stockholm | | externalId | Store ID as recorded in client's system | string | 1001 | | type | Store type | string | ECOMM, RETAIL, OTHER | | active | If the store is active | boolean | true | | timeZone | Timezone of the store | string | Europe/Stockholm | | changeType | Whether the row was created, updated or deleted | string | CREATE/UPDATE/DELETE | # Introduction to Delta Share Source: https://developer.voyado.com/docs/data-access/delta-share Delta Sharing is an open protocol for the secure exchange of large amounts of data. Its advantage is that it allows you to download only the changes and not the whole dataset every time, saving time and bandwidth. ## Delta Share in Customer Engagement In Customer Engagement, you can get direct access to raw data using the open Delta Sharing protocol. Unlike BI Export, data is not pre-processed or aggregated. Instead, each Customer Engagement domain team publishes their own data products, and you connect to them directly from your own data infrastructure. Delta Sharing in Customer Engagement is designed for customers who want full flexibility to work with raw data in their own warehouse, update in almost real-time, and model the data to their own analytical needs. ## What is a data product? A **data product** is a packaged, domain-specific dataset published through Delta Share. Unlike a BI Export (which is a single flat file per entity), a data product groups together multiple related tables around one business concept. ## The two data products Delta Share offers two categories of data products: * **Raw data products**: Domain data with minimal processing (schema, validation, basic cleaning). Included at no additional cost. * **Analytical data products**: Pre-modelled data using a star schema (fact and dimension tables), designed for direct analytical use. Subject to a separate fee. | | **Raw data products** | **Analytical data products** | | :- | :- | :- | | **Data model** | As-is from the source module | Star schema (fact + dimension tables) | | **Transformation** | Minimal: schema, validation, basic cleaning | Pre-modelled and analysis-ready | | **Best for** | Data engineers building pipelines | Analysts querying data directly | | **Cost** | Included with Delta Share | Separate fee, contact your CSM or AM | The two kinds of data products wil now be considered separately. ### Raw data products Raw data products provide domain-specific data from each Customer Engagement module. The data is shared with minimal processing (Voyado adds schema definitions, validate the structure, and perform basic cleaning) but there is no enrichment, transformation, or cross-domain consolidation applied. Raw data products are *included at no additional cost* with Delta Share. Here is the current list of raw data products: Core engagement data across Customer Engagement. Purchase and transaction data: receipts, line items, returns, and related structures. Message delivery and interaction data: sendouts, delivery status, opens, clicks. Contact record data: core contact attributes and related fields. Customer reviews and feedback: review summaries, detailed reviews, and comments. Dun & Bradstreet household enrichment data with demographic and lifestyle attributes. The data products you have access to depends both on your Customer Engagement configuration *and* what has been enabled for sharing. ### Analytical data products Analytical data products deliver pre-modelled, analysis-ready data through Delta Share. Unlike raw data products, the data is structured using a **star schema** with clearly defined fact and dimension tables. This means you can query it directly for reporting and analytics without needing to build your own transformation layer. Analytical data products are subject to a separate fee. Contact your Voyado CSM or account manager for details on availability and pricing. #### Starfish Starfish is used in Customer Engagement's analytical data products. It is a modelled analytical data product built on a star schema, containing fact tables (such as transactions, message events) and dimension tables (such as contacts, stores, products) all designed for direct use in BI tools and analytical queries. Starfish removes the need to construct your own dimensional model from raw data. ## Accessing Delta Share This process varies depending on which kind of data products you are using. * **Raw data products:** You can set up these shares and receive credentials yourself using Config Hub. The guide here explains it in greater detail: * **Analytical data products (Starfish):** Since this option is a paid offering, credentials are provisioned by Voyado and provided by your CSM or account manager after purchase. ## How to connect Once you have access, you'll receive an activation link. Opening this link lets you download a `.share` credentials file, which looks something like this: ```json theme={null} { "shareCredentialsVersion": 1, "bearerToken": "fdij0238d209f94h2...", "endpoint": "https://northeurope-c2.azuredatabricks.net/api/2.0/...", "expirationTime": "9999-12-31T23:59:59.999Z" } ``` The `bearerToken` is sensitive information, so treat it like you would an API key or a password. Do *not* commit it to source control. Delta Share works with any client that implements the Delta Sharing protocol: | **Tool** | **How to connect** | | :- | :- | | **Power BI** | Get Data → Delta Sharing → paste server URL and bearer token | | **Tableau** | Install "Delta Sharing by Databricks" from Tableau Exchange | | **Spark (PySpark)** | Use `format("deltasharing")` with the profile path | | **pandas (Python)** | Use the `delta_sharing` library with `load_as_pandas()` | | **Other** | Any client implementing the Delta Sharing REST protocol | ```python theme={null} table_url = "/path/to/profile.share#.." df = spark.read.format("deltasharing").load(table_url) ``` ```python theme={null} cdf = (spark.read .format("deltasharing") .option("readChangeFeed", "true") .option("startingTimestamp", "2025-08-01 00:00:00") .load(table_url) ) ``` ```python theme={null} import delta_sharing as ds pdf = ds.load_as_pandas(table_url) ``` ## Updating data Delta Share allows customers to incrementally retrieve changes to their data and store those changes in their own data warehouse or similar storage solution. This is achieved by storing every batch of new or modified records with `_commit_version` tag. Consumers can then download all data associated with a specific commit version. When new or updated data is added to Delta Share, that record is given the value “insert” in the `_change_type` column. A record with `_change_type` value of "insert" simply means data which is new or updated. Delta Share uses append mode, meaning that every update adds new records instead of replacing existing ones. * The column `_commit_version` indicates which version the record belongs to. * The column `_commit_timestamp` shows when the record was added. _commit_timestamp Only download the changes made since your last sync. ## Deleting data In January 2026 the "delete" pattern was introduced. Depending on how consumers of Delta Share have implemented their integrations, this change may require adjustments on their side. Data can now have a `_change_type` value of "delete". This means the row is older than 30 days and will be removed. This value will be automatically generated for any row where the `import_date_time` is older than 30 days. A `_change_type` value of "delete" does not modify the original data. All fields remain unchanged. Instead, it serves as a technical log entry indicating that the corresponding row will be removed from Delta Share. Consumers of Delta Share may choose to ignore these "delete" records. ## Extra information Here is some extra information on Delta Share. ### Key concepts * **Share**: A logical grouping of data for a recipient. In Customer Engagement, each recipient has access to one share. * **Schema**: A grouping of tables within a share. Each Customer Engagement share contains one schema. * **Table**: A dataset made up of one or more files, in Parquet format. * **Recipient**: A principal with a bearer token. One recipient can have access to mulitple shares. Simply grant or revoke recipients access in confighub for a particular share. Data is stored in *Apache Parquet* format with a Delta metadata layer. Records are appended — not overwritten — so multiple versions of a record may exist. Use the `import_date_time` column to identify the most recent version. ### Best practices When implementing Delta Sharing here are some best practices to keep in mind: * Keep shares small and purposeful: one share per product, avoid “kitchen sink” shares * Schema contracts: version breaking changes (for example `dp\orders\v2`) * Select only columns you need; apply filters for predicate pushdown * For Power BI, mind the row limit behavior in Power Query * Incremental loads: prioritise CDF over re-reading entire tables * Security: favor OIDC federation over long-lived bearer tokens where possible (better rotation, MFA) ### Rate limit The rate limit for Delta Share is 10 requests per second. Exceeding this will returns *HTTP 429*. Stagger your requests when querying multiple tables in parallel. ### Data retention Delta table history is retained for *30 days*. After that, older versions are removed and time-travel reads are no longer possible. If you need long-term historical access, copy or snapshot the data locally. ### FAQ Here are collected some frequently asked questions on Delta Share in Customer Engagement. You get the latest full snapshot of the table — all currently active records. Historical data outside the 30-day retention window is not included. The change feed only covers the 30-day retention window. Versions older than 30 days are cleaned up automatically and cannot be retrieved afterward. Yes — they serve different purposes and can be used in parallel. See the decision guide for guidance. Using the "query" option returns **all data currently available in the Delta Share**. In other words, the **latest full snapshot** of that table. It does *not* include all historical data, only what's active right now. Some Delta setups allow “time travel” to older versions of the data, but this only works if history sharing is enabled for that table. Old versions are automatically cleaned up according to the retention policy (for Voyado, that's 30 days).\ This means you only have access to what's currently part of the share and not every piece of data stored in Customer Engagement. Yes. Delta Share tables follow a **retention policy**. In Voyado's case, **data older than 30 days** is removed automatically. So, when you use a "query" you're getting the most recent snapshot. Data outside of the retention window isn't available. The "changes" option provides **incremental updates,** meaning only the rows that were added, modified, or deleted between table versions. It works through Delta's Change Data Feed (CDF), so the feature must be enabled for that table. Each update includes metadata like: * \_change\_type * \_commit\_version * \_commit\_timestamp The versions you can access depend on the **retention window**, not the number of versions or files. Once versions are older than 30 days, they're no longer accessible. If you try to query a starting or ending version outside that 30-day window, no data will be returned. Availability is based solely on time, not on how many versions or files exist. Once data versions pass the 30-day retention threshold, they're cleaned up automatically. There's no way to retrieve them afterward. If a table shows no version history, it typically means: * Change Data Feed (CDF) or version tracking is not enabled for that share, or * All historical versions have been cleaned up by the retention policy. In such cases, you'll only have access to the **latest snapshot**, with no historical data or changes available. # Contact Data product Source: https://developer.voyado.com/docs/data-access/delta-shares/contact-data-product The Contact Data product focuses on contacts/members data. It offers a single, reliable contact list that everyone can use to personalize communication and improve customer engagement The previously nested `value_json` (VARIANT) field has now been expanded into explicit, typed columns that reflect the actual columns delivered to the tenant catalog. **Tenant column:**`tenant_id`\ **Description:** The table contains contact/member records. Each row represents a contact with its detailed attributes flattened into dedicated columns — identity and profile fields (names, email, phone, address), membership and marketing metadata, and registration/creation timestamps. This data can be used for: * building a single reliable contact list for personalization * analyzing contact management processes and engagement * tracking changes over time via the timestamp fields * integrating contact data with other systems | Column | Data Type | | :- | :- | | tenant\_id | STRING | | careOf | STRING | | city | STRING | | contactId | STRING | | contactTypeId | STRING | | countryCode | STRING | | creationTime | TIMESTAMP | | customAttributes | STRING | | dateOfBirth | STRING | | email | STRING | | externalId | STRING | | firstName | STRING | | gender | STRING | | isApproved | STRING | | language | STRING | | lastName | STRING | | marketingGroupId | STRING | | memberNumber | STRING | | mobilePhone | STRING | | postalCode | STRING | | registrationByStoreId | STRING | | registrationDate | TIMESTAMP | | registrationSourceType | STRING | | registrationSpecificSource | STRING | | street | STRING | | swedishFinnishPersonalIdentityNumber | STRING | | timestamp | TIMESTAMP | # Customer Engagement Data product Source: https://developer.voyado.com/docs/data-access/delta-shares/engage-data-product The Customer Engagement Data product provides a comprehensive view of customer engagement and communication activities. It organizes key information such as contact interactions, email preferences, message performance (like clicks and opens), product views, abandoned carts, and promotional participation. It also includes insights into audience targeting, receipts, and consent management. With Customer Engagement Data you can analyze customer behavior, refine marketing strategies, and improve overall communication effectiveness, through a centralized and structured data source. Some use cases areas: * Analyzing customer engagement to understand interactions, identify effective strategies, and discover opportunities for improvement * Optimizing marketing strategies by using data insights to tailor campaigns, target specific audiences, and maximize ROI * Managing customer communication by handling consent efficiently, ensuring compliance, and maintaining clear and consistent messaging ## AbandonedCart This table is deprecated and is no longer being updated. An iteration of this table is planned for a new data product, likely to appear before the end of 2026. **Tenant column:**`tenantId`\ **Description:** The table contains data related to abandoned shopping carts on our website. It captures information such as the contact ID of the user, the reference number of the cart, the timestamp of when the cart was abandoned, the session ID, whether it was a new session or not, the user agent used, the referral source, the URL of the page where the cart was abandoned, the SKU of the product in the cart, the quantity of the product, the URL of the cart page, the tenant ID, and the timestamp of when the data was imported. This table is valuable for analyzing user behavior and identifying potential issues in the checkout process. | Column name | Type | | :- | :- | | contactId | STRING | | cartreference | STRING | | time | TIMESTAMP | | session | STRING | | newsession | BOOLEAN | | useragent | STRING | | referred | STRING | | url | STRING | | sku | STRING | | quantity | INT | | carturl | STRING | | tenantId | STRING | | ImportDateTime | TIMESTAMP | ## ArticleLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about articles, including their unique identifiers, names, and associated stock keeping units (SKUs). It also tracks modifications, eligibility for bonus awards, and the type of changes made to each article. This data can be used for inventory management, reporting on article performance, and analyzing changes over time. Additionally, it includes timestamps for when articles were last modified and exported for Business Intelligence purposes. | Column name | Type | | :- | :- | | articleId | STRING | | sku | STRING | | articleName | STRING | | articleNumber | STRING | | articleGroupName | STRING | | modifiedDateTime | TIMESTAMP | | awardsBonus | BOOLEAN | | spare1 | STRING | | spare2 | STRING | | spare3 | STRING | | spare4 | STRING | | spare5 | STRING | | spare6 | STRING | | spare7 | STRING | | spare8 | STRING | | spare9 | STRING | | spare10 | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | itemNumber | STRING | | brand | STRING | | color | STRING | | size | STRING | | mainCategory | STRING | | productCategory | STRING | | subCategory | STRING | | ageGroup | STRING | | gender | STRING | | description | STRING | | season | STRING | | ean | STRING | | gtin | STRING | | isStub | STRING | | tenant\_id | STRING | ## ConsentLatest **Tenant column:**`tenantId`\ **Description:** The table contains records of consent given by contacts, including details such as the contact identifier, consent status, and timestamps for when consent was given and when the record was imported. This data can be used to track consent compliance, analyze consent trends over time, and manage contact preferences effectively. | Column name | Type | | :- | :- | | id | STRING | | contactId | STRING | | alias | STRING | | value | BOOLEAN | | consentDateTime | TIMESTAMP | | source | STRING | | comment | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | filename | STRING | | ImportDate | DATE | | tenant\_id | STRING | ## ContactsLatest **Tenant column:**`tenantId`\ **Description:** The table contains detailed information about contacts, including their demographics, communication preferences, and engagement metrics. It can be used to analyze contact approval status, communication acceptance, and demographic distribution. Additionally, the RFM scores and NPS grades can help in understanding contact value and satisfaction levels, which can inform marketing strategies and customer relationship management. | Column name | Type | | :- | :- | | contactId | STRING | | contactType | STRING | | isApproved | BOOLEAN | | isDeleted | BOOLEAN | | gender | STRING | | age (see note below) | INT | | zipCode | STRING | | city | STRING | | countryCode | STRING | | acceptsEmail | BOOLEAN | | acceptsSms | BOOLEAN | | acceptsPostal | BOOLEAN | | statusEmail | STRING | | statusSms | STRING | | statusPostal | STRING | | rfm | DOUBLE | | recency | INT | | frequency | INT | | monetary | INT | | latestNpsGrade | INT | | latestNpsDateTime | TIMESTAMP | | registrationDateTime | TIMESTAMP | | lastModifiedDateTime | TIMESTAMP | | deletedDateTime | TIMESTAMP | | deletedReason | STRING | | purchaseAmountTotal | DOUBLE | | numberOfArticlesTotal | DOUBLE | | averageReceiptTotal | DOUBLE | | latestReceiptDateTime | TIMESTAMP | | purchaseFrequencyTotal | DOUBLE | | purchaseAmount12months | DOUBLE | | numberOfArticles12Months | DOUBLE | | averageReceipt12Months | DOUBLE | | purchaseFrequency12months | DOUBLE | | recruitedStoreId | STRING | | currentStoreId | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | email | STRING | | mobilePhone | STRING | | firstName | STRING | | lastName | STRING | | externalId | STRING | | creationTime | TIMESTAMP | | street | STRING | | country | STRING | | socialSecurityNumber | STRING | | careOf | STRING | | secrecyMarked | STRING | | memberNumber | STRING | | source | STRING | | canReceiveEmail | BOOLEAN | | canReceiveSms | BOOLEAN | | canReceivePostal | BOOLEAN | | birthDate | STRING | | isClearedAfterDeletion | BOOLEAN | | tenant\_id | STRING | The `age` value is rounded up to nearest 5 years. So if a contact is 27 the value here will be 30. ## DeliveredMessageLatest **Tenant column:**`tenantId`\ **Description:** The table contains data related to delivery events, including details about each delivery, associated messages, and contacts. It records timestamps for delivery and import events, as well as identifiers for tenants and files. This data can be used to track delivery performance, analyze changes over time, and support business intelligence efforts. | Column name | Type | | :- | :- | | deliveryId | STRING | | messageId | STRING | | contactId | STRING | | deliveryDateTime | TIMESTAMP | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | deliveryYearMonthDate | STRING | ## EmailUnsubscribeLatest **Tenant column:**`tenant_id` **Description:** The table contains data related to message delivery events, including details about delivery attempts and bounce types. It can be used to analyze delivery performance, track issues with message bounces, and understand the timing of events. Key information includes identifiers for deliveries and messages, timestamps for event occurrences, and the type of bounce that occurred. | Column name | Type | | :- | :- | | delivery\_id | STRING | | message\_id | STRING | | EventTime | TIMESTAMP | | BounceType | STRING | | contact\_id | STRING | | tenant\_id | STRING | | LinkId | STRING | | kafka\_timestamp | TIMESTAMP | | import\_date\_time | TIMESTAMP | | event\_year\_month\_date | STRING | ## LabelLatest **Tenant column:**`tenantId`\ **Description:** The table contains records related to data imports for various tenants. It includes details such as the unique identifier for each record, the type of data, and the name associated with the record. Additionally, it tracks timestamps for when the data was imported and exported for business intelligence purposes, as well as the filename from which the data was sourced. This table can be used to monitor data import activities, analyze import trends, and manage tenant-specific data effectively. | Column name | Type | | :- | :- | | id | STRING | | type | STRING | | name | STRING | | tenantId | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | ImportDate | DATE | | filename | STRING | | tenant\_id | STRING | ## LabelsToTaggedContactLatest **Tenant column:**`tenant_id` **Description:** The table contains data related to imported files associated with contacts. It includes unique identifiers for both labels and contacts, as well as timestamps for when the data was imported. This information can be used to track the import process, manage data associated with specific contacts, and analyze the timing of data imports. | Column name | Type | | :- | :- | | label\_id | STRING | | contact\_id | STRING | | filename | STRING | | ImportDateTime | TIMESTAMP | | tenant\_id | STRING | | ImportDate | DATE | | import\_date\_time | TIMESTAMP | ## MessageClickLatest **Tenant column:**`tenantId`\ **Description:** The table contains data related to user clicks on messages. It includes details such as the unique identifiers for clicks, messages, and deliveries, as well as timestamps for when clicks and imports occurred. This data can be used to analyze user engagement with messages, track the performance of different messages, and understand user behavior based on the device used for clicks. | Column name | Type | | :- | :- | | clickId | INT | | messageId | STRING | | deliveryId | STRING | | clickDateTime | TIMESTAMP | | url | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | tenantId | STRING | | tenant\_id | STRING | | ImportDateTime | TIMESTAMP | | clickYearMonthDate | STRING | | ImportDate | DATE | | contactId | STRING | | userAgent | STRING | | messageClass | STRING | | email | STRING | ## MessageControlGroupLatest **Tenant column:**`tenantId`\ **Description:** The table contains data related to message control groups and their associated messages. It includes identifiers for messages and contacts, details about changes made to messages, and timestamps for import and export activities. This data can be used to track message processing, analyze changes over time, and support business intelligence efforts. | Column name | Type | | :- | :- | | messageControlGroupId | INT | | messageId | STRING | | contactId | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | tenant\_id | STRING | ## MessageLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about messages that are scheduled to be sent, including their status, recipient details, and associated workflows. Use cases include tracking message delivery, analyzing the effectiveness of different messaging channels, and managing transactional messages. It also provides insights into control groups and the timing of message imports and exports for business intelligence. | Column name | Type | | :- | :- | | messageId | STRING | | messageName | STRING | | subject | STRING | | status | STRING | | scheduledDateTime | TIMESTAMP | | recipientCount | INT | | channel | STRING | | sendList | STRING | | messageSource | STRING | | isTransactional | BOOLEAN | | controlGroupPercent | DOUBLE | | senderStoreId | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | workflowId | STRING | | workflowActivityId | STRING | | isWorkflowTemplate | BOOLEAN | | workflowMessageId | STRING | | tenant\_id | STRING | ## MessageOpenLatest **Tenant column:**`tenantId`\ **Description:** The table contains data related to message open events. It includes details such as the unique identifiers for messages and deliveries, timestamps for when messages were opened and imported, and information about the user's device. This data can be used to analyze user engagement with messages, track the performance of different message types, and understand trends over time. | Column name | Type | | :- | :- | | openId | INT | | messageId | STRING | | deliveryId | STRING | | openDateTime | TIMESTAMP | | userAgent | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | tenantId | STRING | | tenant\_id | STRING | | ImportDateTime | TIMESTAMP | | openYearMonthDate | STRING | | ImportDate | DATE | | contactId | STRING | | messageClass | STRING | | email | STRING | ## NPSResponseLatest **Tenant column:**`tenant_id` **Description:** The table contains records related to contacts associated with various stores. It includes information such as unique identifiers for contacts and stores, the date and time of record creation and import, and the grade or rating given by the contact. This data can be used to track contact interactions, analyze feedback trends, and monitor changes over time. | Column name | Type | | :- | :- | | contact\_id | STRING | | store\_id | STRING | | created\_date\_time | TIMESTAMP | | create\_date | DATE | | grade | STRING | | changeType | STRING | | tenant\_id | STRING | | import\_date\_time | TIMESTAMP | ## ProductView This table is deprecated and is no longer being updated. An iteration of this table is planned for a new data product, likely to appear before the end of 2026. **Tenant column:**`tenantId`\ **Description:** The table captures data related to product views by users. It includes information such as the unique identifier for the contact, product category, SKU, and timestamps for when the product was viewed and when the data was imported. This data can be used to analyze user engagement with products, track session behaviors, and understand referral sources. | Column name | Type | | :- | :- | | contactId | STRING | | category | STRING | | sku | STRING | | time | TIMESTAMP | | session | STRING | | newsession | BOOLEAN | | useragent | STRING | | referred | STRING | | url | STRING | | tenantId | STRING | | ImportDateTime | TIMESTAMP | ## PromotionLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about promotions, including their identifiers, names, types, and validity periods. It also tracks changes made to promotions, such as creation, modification, and deletion dates. This data can be used for analyzing promotion effectiveness, managing promotional campaigns, and integrating with external systems. | Column name | Type | | :- | :- | | promotionId | STRING | | promotionName | STRING | | promotionType | STRING | | validFromDateTime | TIMESTAMP | | validToDateTime | TIMESTAMP | | externalPromotionCode | STRING | | externalId | STRING | | createdOnDateTime | TIMESTAMP | | changedOnDateTime | TIMESTAMP | | deletedOnDateTime | TIMESTAMP | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | tenant\_id | STRING | ## PromotionRecipientLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about promotion recipients, including whether the promotion was used, where it was used, usage timestamps, validity period, value and value type, and receipt linkage. | Column name | Type | | :- | :- | | promotionRecipientId | STRING | | promotionId | STRING | | contactId | STRING | | isUsed | STRING | | usedInStore | STRING | | usageDateTime | TIMESTAMP | | validFromDateTime | TIMESTAMP | | validToDateTime | TIMESTAMP | | createdOnDateTime | TIMESTAMP | | receiptExternalId | STRING | | value | STRING | | valueType | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | tenant\_id | STRING | ## ReceiptItemsLatest **Tenant column:**`tenantId`\ **Description:** The table contains detailed information about receipt items from transactions. It includes identifiers for receipts, contacts, and stores, as well as transaction timestamps. Key data points include article details such as SKU, quantity purchased, pricing information, discounts, and tax percentages. This table can be used for analyzing sales performance, understanding customer purchasing behavior, and evaluating pricing strategies. | Column name | Type | | :- | :- | | receiptItemId | STRING | | receiptId | STRING | | contactId | STRING | | storeId | STRING | | transactionDateTime | TIMESTAMP | | type | STRING | | sku | STRING | | articleNumber | STRING | | quantity | INT | | grossPaidPrice | DOUBLE | | grossPaidPricePerUnit | DOUBLE | | localPrice | DOUBLE | | localCurrency | STRING | | discounts | DOUBLE | | discountPercent | DOUBLE | | marginPercent | STRING | | taxPercent | DOUBLE | | articleName | STRING | | articleGroup | STRING | | articleSpare1 | STRING | | articleSpare2 | STRING | | articleSpare3 | STRING | | articleSpare4 | STRING | | articleSpare5 | STRING | | articleSpare6 | STRING | | articleSpare7 | STRING | | articleSpare8 | STRING | | articleSpare9 | STRING | | articleSpare10 | STRING | | articleDateSpare1 | STRING | | articleDateSpare2 | STRING | | articleDateSpare3 | STRING | | articleDateSpare4 | STRING | | articleDateSpare5 | STRING | | awardsBonus | BOOLEAN | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | transactionYearMonthDate | STRING | | tenant\_id | STRING | ## ReceiptsLatest **Tenant column:**`tenantId`\ **Description:** The table contains detailed information about receipts generated from transactions. It includes unique identifiers for receipts, contacts, and stores, as well as financial details such as total gross price, local price, and quantities of items purchased and returned. This data can be used for analyzing sales performance, tracking returns, and understanding customer purchasing behavior over time. | Column name | Type | | :- | :- | | receiptId | STRING | | contactId | STRING | | storeId | STRING | | storeExternalId | STRING | | receiptNumber | STRING | | totalGrossPrice | DOUBLE | | totalQuantity | INT | | totalReturned | DOUBLE | | totalLocalPrice | DOUBLE | | exchangeRate | DOUBLE | | localCurrency | STRING | | numberOfItems | INT | | numberOfPurchases | INT | | numberOfReturns | INT | | createdOnDateTime | TIMESTAMP | | externalId | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | createdOnYearMonthDate | STRING | | tenant\_id | STRING | ## SavedSearchLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about saved searches, including their identifiers, names, timestamps for creation, modification, deletion, and flags used for targeting audiences. | Column name | Type | | :- | :- | | savedSearchId | STRING | | savedSearchName | STRING | | createdOnDateTime | TIMESTAMP | | changedOnDateTime | TIMESTAMP | | deletedOnDateTime | TIMESTAMP | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | savedSearchIsTargetAudience | BOOLEAN | | tenant\_id | STRING | ## StoreLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about stores, including their locations, status, and identifiers. It can be used to track store details such as whether they are active or deleted, analyze store distribution by city or country, and manage data imports and exports for business intelligence. This data is useful for operational reporting, location analysis, and understanding store performance. | Column name | Type | | :- | :- | | storeId | STRING | | name | STRING | | isDeleted | BOOLEAN | | city | STRING | | country | STRING | | zipCode | STRING | | street | STRING | | region | STRING | | externalId | STRING | | type | STRING | | active | BOOLEAN | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | tenant\_id | STRING | ## TargetAudienceContactLatest **Tenant column:**`tenantId`\ **Description:** The table contains data related to target audience contacts, including their identifiers, associated saved searches, and timestamps for various events such as import and export. Use cases include tracking changes to target audience contacts, analyzing the timing of contact imports and exports, and managing audience segmentation based on the saved searches. | Column name | Type | | :- | :- | | targetAudienceContactId | STRING | | savedSearchId | STRING | | contactId | STRING | | startDateTime | TIMESTAMP | | endDateTime | TIMESTAMP | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | tenant\_id | STRING | # Customer Engagement Purchases product Source: https://developer.voyado.com/docs/data-access/delta-shares/engage-purchases-product The Customer Engagement Purchases product provides a complete view of customer purchases and transactions. It brings together essential data such as store details, products, receipts, individual receipt items, and customer contact information. By using Customer Engagement Purchases, you can analyze purchasing behavior, manage transaction records efficiently, and maintain accurate customer data to support smarter business decisions. Use cases: * Analyzing customer purchasing behavior to identify trends, popular products, and peak buying periods * Managing and tracking detailed transaction records for accurate reporting and financial insights * Maintaining and using customer contact data to enable personalized and targeted communication ## ArticleLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about articles, including their unique identifiers, names, and associated stock keeping units (SKUs). It also tracks modifications, eligibility for bonus awards, and the type of changes made to each article. This data can be used for inventory management, reporting on article performance, and analyzing changes over time. Additionally, it includes timestamps for when articles were last modified and exported for Business Intelligence purposes. | Column name | Type | | :- | :- | | articleId | STRING | | sku | STRING | | articleName | STRING | | articleNumber | STRING | | articleGroupName | STRING | | modifiedDateTime | TIMESTAMP | | awardsBonus | BOOLEAN | | spare1 | STRING | | spare2 | STRING | | spare3 | STRING | | spare4 | STRING | | spare5 | STRING | | spare6 | STRING | | spare7 | STRING | | spare8 | STRING | | spare9 | STRING | | spare10 | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | itemNumber | STRING | | brand | STRING | | color | STRING | | size | STRING | | mainCategory | STRING | | productCategory | STRING | | subCategory | STRING | | ageGroup | STRING | | gender | STRING | | description | STRING | | season | STRING | | ean | STRING | | gtin | STRING | | isStub | STRING | | tenant\_id | STRING | ## ContactsLatest **Tenant column:**`tenantId`\ **Description:** The table contains detailed information about contacts, including their demographics, communication preferences, and engagement metrics. It can be used to analyze contact approval status, communication acceptance, and demographic distribution. Additionally, the RFM scores and NPS grades can help in understanding contact value and satisfaction levels, which can inform marketing strategies and customer relationship management. | Column name | Type | | :- | :- | | contactId | STRING | | contactType | STRING | | isApproved | BOOLEAN | | isDeleted | BOOLEAN | | gender | STRING | | age | INT | | zipCode | STRING | | city | STRING | | countryCode | STRING | | acceptsEmail | BOOLEAN | | acceptsSms | BOOLEAN | | acceptsPostal | BOOLEAN | | statusEmail | STRING | | statusSms | STRING | | statusPostal | STRING | | rfm | DOUBLE | | recency | INT | | frequency | INT | | monetary | INT | | latestNpsGrade | INT | | latestNpsDateTime | TIMESTAMP | | registrationDateTime | TIMESTAMP | | lastModifiedDateTime | TIMESTAMP | | deletedDateTime | TIMESTAMP | | deletedReason | STRING | | purchaseAmountTotal | DOUBLE | | numberOfArticlesTotal | DOUBLE | | averageReceiptTotal | DOUBLE | | latestReceiptDateTime | TIMESTAMP | | purchaseFrequencyTotal | DOUBLE | | purchaseAmount12months | DOUBLE | | numberOfArticles12Months | DOUBLE | | averageReceipt12Months | DOUBLE | | purchaseFrequency12months | DOUBLE | | recruitedStoreId | STRING | | currentStoreId | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | email | STRING | | mobilePhone | STRING | | firstName | STRING | | lastName | STRING | | externalId | STRING | | creationTime | TIMESTAMP | | street | STRING | | country | STRING | | bonusPoints | DOUBLE | | socialSecurityNumber | STRING | | bonusPointsModifiedOn | TIMESTAMP | | careOf | STRING | | secrecyMarked | STRING | | memberNumber | STRING | | source | STRING | | canReceiveEmail | BOOLEAN | | canReceiveSms | BOOLEAN | | canReceivePostal | BOOLEAN | | birthDate | STRING | | isClearedAfterDeletion | BOOLEAN | | tenant\_id | STRING | ## ReceiptItemsLatest **Tenant column:**`tenantId`\ **Description:** The table contains detailed information about receipt items from transactions. It includes identifiers for receipts, contacts, and stores, as well as transaction timestamps. Key data points include article details such as SKU, quantity purchased, pricing information, discounts, and tax percentages. This table can be used for analyzing sales performance, understanding customer purchasing behavior, and evaluating pricing strategies. | Column name | Type | | :- | :- | | receiptItemId | STRING | | receiptId | STRING | | contactId | STRING | | storeId | STRING | | transactionDateTime | TIMESTAMP | | type | STRING | | sku | STRING | | articleNumber | STRING | | quantity | INT | | grossPaidPrice | DOUBLE | | grossPaidPricePerUnit | DOUBLE | | localPrice | DOUBLE | | localCurrency | STRING | | discounts | DOUBLE | | discountPercent | DOUBLE | | marginPercent | STRING | | taxPercent | DOUBLE | | articleName | STRING | | articleGroup | STRING | | articleSpare1 | STRING | | articleSpare2 | STRING | | articleSpare3 | STRING | | articleSpare4 | STRING | | articleSpare5 | STRING | | articleSpare6 | STRING | | articleSpare7 | STRING | | articleSpare8 | STRING | | articleSpare9 | STRING | | articleSpare10 | STRING | | articleDateSpare1 | STRING | | articleDateSpare2 | STRING | | articleDateSpare3 | STRING | | articleDateSpare4 | STRING | | articleDateSpare5 | STRING | | awardsBonus | BOOLEAN | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | transactionYearMonthDate | STRING | | tenant\_id | STRING | ## ReceiptsLatest **Tenant column:**`tenantId`\ **Description:** The table contains detailed information about receipts generated from transactions. It includes unique identifiers for receipts, contacts, and stores, as well as financial details such as total gross price, local price, and quantities of items purchased and returned. This data can be used for analyzing sales performance, tracking returns, and understanding customer purchasing behavior over time. | Column name | Type | | :- | :- | | receiptId | STRING | | contactId | STRING | | storeId | STRING | | storeExternalId | STRING | | receiptNumber | STRING | | totalGrossPrice | DOUBLE | | totalQuantity | INT | | totalReturned | DOUBLE | | totalLocalPrice | DOUBLE | | exchangeRate | DOUBLE | | localCurrency | STRING | | numberOfItems | INT | | numberOfPurchases | INT | | numberOfReturns | INT | | createdOnDateTime | TIMESTAMP | | externalId | STRING | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | createdOnYearMonthDate | STRING | | tenant\_id | STRING | ## StoreLatest **Tenant column:**`tenantId`\ **Description:** The table contains information about stores, including their locations, status, and identifiers. It can be used to track store details such as whether they are active or deleted, analyze store distribution by city or country, and manage data imports and exports for business intelligence. This data is useful for operational reporting, location analysis, and understanding store performance. | Column name | Type | | :- | :- | | storeId | STRING | | name | STRING | | isDeleted | BOOLEAN | | city | STRING | | country | STRING | | zipCode | STRING | | street | STRING | | region | STRING | | externalId | STRING | | type | STRING | | active | BOOLEAN | | changeType | STRING | | filename | STRING | | biExportDate | TIMESTAMP | | ImportDateTime | TIMESTAMP | | tenantId | STRING | | ImportDate | DATE | | tenant\_id | STRING | # Loyalty product Source: https://developer.voyado.com/docs/data-access/delta-shares/loyalty-product Point account data for tracking loyalty program engagement and rewards activity **Use this share to understand your customers' loyalty program engagement, track promotion usage, point accumulation and redemption patterns, and analyze customer lifetime value through rewards activity. This data helps you identify your most engaged customers and optimize your loyalty program strategy.** This product only contains **point account data** at this time. Promotion data is still in the [Customer Engagement Data product](/docs/data-access/delta-shares/engage-data-product). ## point\_account Tenant column identifier: tenantId The point\_account table stores the current state of each customer's point account, including their point balance and account metadata. Use this table to understand your customers' point balances and track changes over time. You can join this table with contact data to segment customers by their point holdings or identify high-value loyal customers. | Column name | Type | | - | - | | tenant\_id | STRING | | point\_account\_id | STRING | | contact\_id | STRING | | balance | DOUBLE | | modified\_on | TIMESTAMP | # Message Services product Source: https://developer.voyado.com/docs/data-access/delta-shares/message-services-product The Message Services product focuses on email and SMS data, offering insights into message performance and user engagement. It is pivotal for optimizing communication strategies and measuring campaign effectiveness. Some use cases are: * Enhancing communication strategy with performance insights * Calculating engagement metrics for emails and SMS ## email\_click **Tenant column:**`tenant_id`\ **Description:** The table contains data related to messaging events, capturing details such as the tenant, message identifiers, and user interactions. It includes information on the type and category of events, timestamps for when events occurred, and metadata like IP addresses and user agents. This data can be used for analyzing messaging performance, tracking user engagement, and troubleshooting delivery issues. | Column name | Type | | :- | :- | | tenant\_id | STRING | | message\_id | STRING | | email | STRING | | event | STRING | | type | STRING | | category | STRING | | event\_timestamp | STRING | | contact\_id | STRING | | delivery\_id | STRING | | ip\_address | STRING | | url | STRING | | user\_agent | STRING | | link\_id | STRING | | kafka\_timestamp | TIMESTAMP | | write\_timestamp | TIMESTAMP | ## email\_bounce **Tenant column:**`tenant_id`\ **Description:** The table contains data related to email events, including information about message delivery, bounces, and associated metadata. Use cases include tracking email performance, analyzing bounce reasons, and understanding user engagement through event timestamps. The data can help in optimizing email campaigns and improving communication strategies. | Column name | Type | | :- | :- | | tenant\_id | STRING | | message\_id | STRING | | email | STRING | | event | STRING | | type | STRING | | reason | STRING | | bounce\_type | STRING | | event\_timestamp | STRING | | contact\_id | STRING | | delivery\_id | STRING | | ip\_address | STRING | | message\_class | STRING | | send\_time | STRING | | kafka\_timestamp | TIMESTAMP | | write\_timestamp | TIMESTAMP | ## message\_base **Tenant column:**`tenant_id`\ **Description:** The table contains data related to messaging activities within the system. It includes details such as the message ID, recipient count, and status of each message. This data can be used to analyze messaging performance, track automation activities, and understand the effectiveness of different marketing groups. Additionally, timestamps for various events provide insights into the timing of message sends and their scheduling. | Column name | Type | | :- | :- | | tenant\_id | STRING | | message\_id | STRING | | name | STRING | | subject | STRING | | automation\_definition\_id | STRING | | automation\_activity\_definition\_id | STRING | | content\_id | STRING | | sendout\_id | STRING | | marketing\_group\_id | STRING | | is\_transactional | BOOLEAN | | recipient\_count | LONG | | status | STRING | | created\_time | TIMESTAMP | | scheduled\_send\_time | TIMESTAMP | | last\_scheduled\_send\_time | TIMESTAMP | | payload\_timestamp | TIMESTAMP | | enqueued\_time | TIMESTAMP | | message\_type | STRING | | event\_ts | TIMESTAMP | ## email\_open **Tenant column:**`tenant_id`\ **Description:** This table contains data related to email opens, including information about the tenant, contact, and email message, as well as details about the device and time of the open event. Its likely purpose is to track and store email engagement metrics, allowing for analysis of email campaign effectiveness and user behavior. This data can be used for various purposes, such as generating reports on email open rates, identifying trends in user engagement, and informing future email marketing strategies. | Column name | Type | | :- | :- | | tenant\_id | STRING | | event | STRING | | contact\_id | STRING | | message\_id | STRING | | email | STRING | | user\_agent | STRING | | kafka\_timestamp | TIMESTAMP | | event\_time | TIMESTAMP | | event\_date | DATE | | write\_timestamp | TIMESTAMP | ## email\_delivered **Tenant column:**`tenant_id`\ **Description:** This table contains data related to email delivery events, including information about the recipient, email message, and delivery details, as well as timestamps for when the event occurred and was written to the table. Its likely purpose is to track and store email delivery metrics for analysis and reporting, allowing users to monitor email campaign effectiveness and troubleshoot delivery issues. Potential use cases for this table include generating reports on email delivery rates, analyzing user engagement based on user agent data, and identifying trends in email delivery times and dates. | Column name | Type | | :- | :- | | tenant\_id | STRING | | event | STRING | | contact\_id | STRING | | message\_id | STRING | | email | STRING | | user\_agent | STRING | | kafka\_timestamp | TIMESTAMP | | event\_time | TIMESTAMP | | event\_date | DATE | | write\_timestamp | TIMESTAMP | ## email\_unsubscribe **Tenant column:**`tenant_id`\ **Description:** This table contains data related to email unsubscribe events, including information about the tenant, contact, and email message, as well as details about the event itself, such as the user agent and timestamps. The likely purpose of this table is to track and store unsubscribe events for analytics and compliance purposes, allowing businesses to manage their email marketing campaigns effectively. Potential use cases for this table include monitoring unsubscribe rates, analyzing user behavior, and ensuring compliance with anti-spam laws and regulations. | Column name | Type | | :- | :- | | tenant\_id | STRING | | event | STRING | | contact\_id | STRING | | message\_id | STRING | | email | STRING | | user\_agent | STRING | | kafka\_timestamp | TIMESTAMP | | event\_time | TIMESTAMP | | event\_date | DATE | | write\_timestamp | TIMESTAMP | ## sms\_delivered **Tenant column:**`tenant_id`\ **Description:** This table contains data related to the delivery of SMS messages, including the tenant, contact, and message details, as well as timestamps for when the event occurred and was written to the table. Its likely purpose is to track and record the delivery status of SMS messages for auditing, analytics, and troubleshooting purposes. This data can be used to monitor message delivery rates, identify issues with message delivery, and analyze the performance of SMS campaigns. | Column name | Type | | :- | :- | | tenant\_id | STRING | | event | STRING | | contact\_id | STRING | | message\_id | STRING | | delivery\_id | STRING | | kafka\_timestamp | TIMESTAMP | | event\_time | TIMESTAMP | | event\_date | DATE | | write\_timestamp | TIMESTAMP | ## sms\_unsubscribe **Tenant column:**`tenant_id`\ **Description:** This table contains data related to SMS unsubscribe events, including the tenant, message, and contact information, as well as timestamps for when the unsubscribe event occurred, when the message was sent, and when it was enqueued. The likely purpose of this table is to track and store unsubscribe events for SMS messages, allowing for compliance with regulations and management of contact preferences. This data can be used for analytics, auditing, and ensuring that contacts are not sent messages they have opted out of receiving, helping to maintain a positive user experience and prevent spam. | Column name | Type | | :- | :- | | tenant\_id | STRING | | message\_id | STRING | | contact\_id | STRING | | unsubscribe\_time | TIMESTAMP | | payload\_timestamp | TIMESTAMP | | enqueued\_time | TIMESTAMP | | message\_type | STRING | | event\_ts | TIMESTAMP | ## sendout **Tenant column:**`tenant_id`\ **Description:** This table contains data related to exported sendouts, including the tenant identifier, message type, and a JSON object with additional values, as well as a unique sendout identifier and the timestamp of the event. The likely purpose of this table is to store a record of all sendouts that have been exported, allowing for auditing and tracking of these events. This data can be used for analytics, debugging, and logging purposes, such as monitoring sendout volumes, identifying trends, and troubleshooting issues with the export process. | Column name | Type | | :- | :- | | tenant\_id | STRING | | value\_json | VARIANT | | message\_type | STRING | | sendout\_id | STRING | | event\_time | TIMESTAMP | ## marketing\_group **Tenant column:**`tenant_id`\ **Description:** This table contains data related to marketing group exports, including tenant information, JSON-formatted values, message types, and timestamps of when events occurred. Its likely purpose is to store and track the history of marketing group exports for various tenants, allowing for auditing and analytics. Potential use cases for this table include monitoring export activity, troubleshooting issues, and analyzing marketing group data for business insights. | Column name | Type | | :- | :- | | tenant\_id | STRING | | value\_json | VARIANT | | message\_type | STRING | | event\_time | TIMESTAMP | # Reviews product Source: https://developer.voyado.com/docs/data-access/delta-shares/reviews-product The Reviews data product organizes customer review and feedback data from Customer Engagement. It includes tables for review summaries, detailed reviews, and comments associated with those reviews. Use this product for analyzing customer opinions, tracking review trends, and managing feedback processes. Common use cases for this data product are: * Analyzing customer opinions and sentiment to understand product perception * Tracking review trends over time to identify quality improvements or issues * Managing feedback processes and monitoring review moderation workflows * Understanding user engagement through upvotes, downvotes, and comment interactions ## review **Tenant column:**`tenantId`\ **Description:** This table contains detailed information about product reviews. It includes data on the review content, ratings, and metadata such as whether the reviewer is verified, timestamps for review creation and approval, and user details. The table supports tracking changes and deletion status. It can be used for analyzing customer feedback, monitoring review quality, and studying user engagement with product reviews. | **Column name** | **Type** | | :- | :- | | tenant\_id | STRING | | review\_id | STRING | | external\_id | STRING | | review\_group\_id | STRING | | product\_feed\_id | STRING | | product\_group\_id | STRING | | product\_id | STRING | | product\_variant\_id | STRING | | contact\_id | STRING | | has\_review\_text | BOOLEAN | | review\_text | STRING | | rating | DOUBLE | | upvote\_count | INT | | downvote\_count | INT | | is\_verified\_buyer | BOOLEAN | | status | STRING | | user\_type | STRING | | user\_detail | STRING | | review\_created\_on | TIMESTAMP | | review\_approved\_on | TIMESTAMP | | review\_rejected\_on | TIMESTAMP | | translations\_json | STRING | | is\_deleted | BOOLEAN | | change\_type | STRING | | event\_timestamp | TIMESTAMP | | write\_timestamp | TIMESTAMP | ## reviewcomment **Tenant column:**`tenantId`\ **Description:** The table captures details about review comments made by users. It includes information such as the comment's status, upvote and downvote counts, timestamps for creation and approval, and user details. This data can be used to analyze user engagement with reviews, track comment moderation efforts, and identify trends in user feedback. | **Column name** | **Type** | | :- | :- | | tenant\_id | STRING | | comment\_id | STRING | | external\_id | STRING | | review\_id | STRING | | parent\_comment\_id | STRING | | contact\_id | STRING | | upvote\_count | INT | | downvote\_count | INT | | status | STRING | | user\_type | STRING | | user\_detail | STRING | | comment\_created\_on | TIMESTAMP | | comment\_approved\_on | TIMESTAMP | | comment\_rejected\_on | TIMESTAMP | | translations\_json | STRING | | is\_deleted | BOOLEAN | | change\_type | STRING | | event\_timestamp | TIMESTAMP | | write\_timestamp | TIMESTAMP | ## reviewsummary **Tenant column:**`tenantId`\ **Description:** The table contains aggregated summaries of product reviews, including various metrics such as the number of ratings, average rating, and sentiment analysis. It can be used to assess product performance, identify customer satisfaction trends, and analyze the characteristics of buyers. This data helps in understanding how different ratings and sentiments are distributed across products, as well as tracking changes over time. | **Column name** | **Type** | | :- | :- | | tenant\_id | STRING | | summary\_id | STRING | | review\_group\_id | STRING | | number\_of\_ratings | INT | | number\_of\_reviews | INT | | average\_rating | DOUBLE | | median\_rating | DOUBLE | | min\_rating | INT | | max\_rating | INT | | five\_star\_ratings | INT | | four\_star\_ratings | INT | | three\_star\_ratings | INT | | two\_star\_ratings | INT | | one\_star\_ratings | INT | | rating\_standard\_deviation | DOUBLE | | verified\_buyer\_count | INT | | unverified\_buyer\_count | INT | | unknown\_buyer\_count | INT | | positive\_sentiment\_count | INT | | neutral\_sentiment\_count | INT | | negative\_sentiment\_count | INT | | average\_sentiment\_score | DOUBLE | | total\_upvotes | INT | | total\_downvotes | INT | | summary\_based\_on | STRING | | created\_on | TIMESTAMP | | modified\_on | TIMESTAMP | | deleted\_on | TIMESTAMP | | is\_deleted | BOOLEAN | | translations\_json | STRING | | change\_type | STRING | | event\_timestamp | TIMESTAMP | | write\_timestamp | TIMESTAMP | # Targeting product Source: https://developer.voyado.com/docs/data-access/delta-shares/targeting-product The Targeting data product provides customer enrichment and segmentation capabilities based on Dun & Bradstreet household data. It includes demographic and lifestyle attributes such as household identifiers, moving events, purchase power indicators, and behavioral data. Use this for customer profiling, targeting strategies, and understanding household-level characteristics. Common use cases are: * Audience segmentation based on demographic and lifestyle attributes * Personalization strategies using household-level data * Tracking contact enrichment history and managing data integrity * Analyzing customer lifecycle patterns and household behaviors ## bisnode\_enrichment **Tenant column:**`tenantId`\ **Description:** The table contains data related to customer enrichment activities. It includes information about contacts, their corresponding identifiers, and details about the enrichment process such as the value, type, and the temporal aspects of changes. This table is useful for tracking updates to customer profiles, understanding enrichment history, and managing data integrity by identifying deleted entries. | **Column name** | **Type** | | :- | :- | | tenant\_id | STRING | | contact\_id | STRING | | bci\_id | STRING | | value | STRING | | type | STRING | | precision | STRING | | enrichment\_id | STRING | | version | INT | | is\_deleted | BOOLEAN | | change\_type | STRING | | event\_timestamp | TIMESTAMP | | write\_timestamp | TIMESTAMP | # Data access overview Source: https://developer.voyado.com/docs/data-access/overview Customer Engagement offers you direct access to your own data so that you can use it for analytics, reporting, and integration into your own data infrastructure. Two access methods are available: * BI export * Delta Share Which one is most suitable for you depends on your technical setups, data needs, and update frequencies. Choose what best fits your stack, or use both in parallel. ```mermaid actions={false} theme={null} flowchart TD A["Engage Data Access
Unified data access offering"] A --> B["BI Export
FTP / CSVs
Included"] A --> C["Delta Share
Delta Sharing Protocol"] B --> D["CSV Exports
Once daily, curated"] C --> E["Raw Data Products
Domain-specific, included"] C --> F["Analytical Data Products
Star schema, separate fee"] classDef red stroke:#ef4444,color:#b91c1c,fill:#fff,stroke-width:2px; classDef purple stroke:#7c3aed,color:#6b21a8,fill:#fff,stroke-width:2px; classDef neutral stroke:#d1d5db,color:#111827,fill:#fff,stroke-width:1.5px; class A,B red; class C,E,F purple; class D neutral; ``` ## The two methods Data is organized differently in each method, and the terminology also differs. ### BI Export Here the data is delivered as an "export", which is a flat-file dataset for a single entity in Customer Engagement. For example, you can choose: * The "Contact" export * The "Receipt" export * The "Message" export Each export is a single CSV file representing one database table. The first time you perform a particular export, it will be a *full* export. All the following times will be delta exports. ### Delta share Here, data is delivered as "data products". These are packaged, domain-specific datasets that group related tables around a business concept. Data products come in two categories: * **Raw data products**: These provide you with unprocessed domain data and are included * **Analytical data products**: These deliver modelled, analysis-ready data using a star schema and are subject to a separate fee ## Which method is best for you | | **BI Export** | **Delta Share** | | :- | :- | :- | | **Delivery** | CSV files via FTP | Delta Sharing open protocol | | **Unit of data** | Export (one flat file per entity) | Data product (raw or analytical) | | **Data content** | Curated, cross-domain aggregates | Raw domain data or pre-modelled star schema | | **Update frequency** | Once per day (nightly batch) | Near real-time, incremental | | **Setup complexity** | Low — fetch files from FTP | Higher — requires Delta Sharing client | | **Transformation** | Pre-processed by Voyado | Raw as-is, or pre-modelled (analytical) | | **Best for** | Ready-made analytics, quick setup | Custom pipelines, data warehouses, direct BI | Both methods can be used in parallel. Some customers use BI Export for consolidated reporting and Delta Share for operational pipelines. Contact your CSM or Account Manager if you need guidance on which approach fits your setup. ### When to use BI Export BI export is a good option when: * You want structured, analysis-ready data with minimal setup * Your BI tool consumes CSV files or connects via FTP * You need cross-domain data pre-processed and combined * Daily refresh frequency is sufficient BI export: * Curated CSV exports via FTP * Structured, cross-domain datasets processed nightly * Ready for direct use in any BI tool, no pipeline required. ### When to use Delta Share Using Delta Share is a good option when: * You want raw or pre-modelled data in your own data warehouse or data lake * You need higher-frequency updates (2-4 batches per day) * You have technical capability for the Delta Sharing protocol * You want to control your own transformation layer or use pre-built analytical models Delta Share: * Access your Customer Engagement data through the Delta Sharing protocol * Access either as raw domain feeds or as pre-modelled analytical datasets * Close-to-real-time updates and tool-agnostic * Designed to integrate directly into your data infrastructure # About file based transfer Source: https://developer.voyado.com/docs/file-based-transfer/about-file-based-transfer File based transfer in Customer Engagement refers to transferring your data as files via the Voyado FTP. Transfers like this are done periodically, usually once per hour, but this can be adjusted per client and method according to your needs. If the Voyado FTP server is unresponsive, it's up to the sender to re-attempt the transfer until it has been successfully received. See how file based transfer is actually done here: # Article XML import Source: https://developer.voyado.com/docs/file-based-transfer/article-xml-import The article XML importer allows you to import an article directory from a file. The following fields are available in the \
node of your XML import file. | Field | Format | Max. characters | Description | | - | - | - | - | | sku\* | string | 255 | Article SKU, the most detailed designator for an article. This must be unique. Transactions will be matched using this key. | | number\* | string | 255 | Article number / group number, can be shared by color variants of the same product over multiple SKUs | | name\* | string | 255 | The article name in human readable form. | | maincategory\* | string | 255 | | | awardsbonus | boolean | 1 or 0 | If 1 (true) this article can award points, otherwise it does not | | brand | string | 255 | | | color | string | 255 | | | size | string | 100 | | | productcategory | string | 255 | | | subcategory | string | 255 | | | agegroup | string | 255 | | | gender | string | 6 | | | description | string | 255 | | | season | string | 255 | | | gtin | string | 20 | Global trade item number, same as EAN but a more international standard | | spare1 ... spare10 | string | 255 | Ten fields (named "spare1" up to "spare10") for additional data | *\* indicates a mandatory field.* You can find examples [on the example files page](/docs/file-based-transfer/example-files). If you are using Product Discovery, make sure that the variant ID matches the SKU in Customer Engagement. # Avoid triggering export Source: https://developer.voyado.com/docs/file-based-transfer/avoid-triggering-export When changes are made to your contacts (by updating, adding or deleting them) through a file import or the bulk API, some exports will be automatically triggered in Customer Engagement. If you *don't* want this to happen, you can use the attribute `avoidTriggeringExport`. Setting this to "true" will suppress both the automatic BI and contact XML exports. The default for `avoidTriggeringExport` is "false" which means that BI and contact XML exports will be triggerred by changes made to your contacts. You will only need to include it (as "true") if you **don't** want these BI and contact XML exports to be triggered. This setting was previously known as "silent export". ## For XML For an XML import, `avoidTriggeringExport` is handled as an attribute in the top-level \ node: ```xml theme={null} ``` ## For bulk API When using the contact bulk API, `avoidTriggeringExport` is passed in the query string: ```http theme={null} GET /api/v3/contacts/bulk?avoidTriggeringExport=true&contactType=Contact ``` ## Constraints For the `avoidTriggeringExport` setting to work, your file can *only* consist of a `contactType` key (such as "member") and fields that are *not* on the standard contact fields list. If `avoidTriggeringExport` is set to "true" and your import / update contains standard contact attributes that are not keys (such as FirstName or Email), your import will fail and an error will be written to the integration log. If an import is done with only keys and standard contact attributes, but `avoidTriggeringExport` is false or missing, exports will then be triggered and entries added to "changelog" and "lastmodified". The `avoidTriggeringExport` flag can't be combined with either of the ImportSettings `InsertNewContacts` and `ShouldDefaultSetIsApprovedAsTrue`. # Contact CSV import Source: https://developer.voyado.com/docs/file-based-transfer/contact-csv-import This is the structure of an example row in your CSV file: | firstName | lastName | email | mobilePhone | street | | - | - | - | - | - | | Example | Examplesson | [example@example.com](mailto:example@example.com) | +46739111111 | Examplegatan 123 | Read more about the manual import of contact data [in this guide](https://help.engage.voyado.com/hc/en-gb/articles/17245851011612-Import-contact-data-manually-to-Engage). You'll need to check your contacts data before importing it to Customer Engagement, to avoid problems later on. Read about the preparation process for a CSV import of contacts [on this page](https://help.engage.voyado.com/hc/en-gb/articles/17245864400924-Prepare-your-file-for-import). You can download examples as CSV and Excel [on the example files page](/docs/file-based-transfer/example-files). ## Splitting your contacts CSV file If a contacts CSV file has more than 50 000 records, you should split it into several files before importing them into Customer Engagement. One way to do this is using a python script like this one: ```python expandable theme={null} import csv import os def create_batches(input_file, output_folder, batch_size): # Create output folder if it doesn't exist if not os.path.exists(output_folder): os.makedirs(output_folder) with open(input_file, 'r', newline='') as csvfile: reader = csv.reader(csvfile) header = next(reader) batch_number = 1 batch_count = 0 output_csv = None for i, row in enumerate(reader): if i % batch_size == 0: if output_csv: output_csv.close() output_file = os.path.join(output_folder, f'batch_{batch_number}.csv') output_csv = open(output_file, 'w', newline='') writer = csv.writer(output_csv) writer.writerow(header) batch_number += 1 batch_count = 0 writer.writerow(row) batch_count += 1 if output_csv: output_csv.close() if __name__ == "__main__": input_file = 'sample_data.csv' # Here enter your CSV's file name output_folder = 'output_batches' # Here write the output folder batch_size = 50000 # Here enter the number of rows per batch create_batches(input_file, output_folder, batch_size) ``` To use this script, do the following: 1. Make sure that Python 3 is installed on your computer. 2. Create a folder containing an empty text file called "script.py". 3. Copy the code above and paste it into that file. 4. Place the CSV you want to split into the same folder. 5. Open Command Prompt on PC (terminal in Mac) and navigate to this folder. 6. Run the script using the command "python script.py". 7. Now a folder should be created containing your files. 8. Take these files and import them into Customer Engagement. ## Importing your files In the Customer Engagement frontend go to **Contacts** then **Import/Export** and then **Import**. opt-out-import-02.png You'll now see this page. opt-out-import-04.png) Hit "Browse" and select your CSV file. You can also drag-and-drop your file here. Choose your contact type, separator, delimeter and encoding: opt-out-import-05.png) Make sure the attributes (the columns in your file) are matched to the attributes correctly. You will be now presented with three import options. opt-out-import-06.png) * **Create and update:** If you're importing new contacts or a mix or old and new. * **Only create new contacts:** If you're sure you're just creating new contacts. * **Only Update:** If you're sure you're only updating existing contacts. Choose your import and hit "Next": opt-out-import-07.png) Now select "Import" to start your CSV import. Obviously you'll need to repeat this for every individual contacts CSV file you have. # Contact XML export Source: https://developer.voyado.com/docs/file-based-transfer/contact-xml-export The export of contacts is primarily done when Voyado Customer Engagement is not the master or when the integrating system needs to have a fallback contact registry. These are the important points to remember about exporting contacts: * Only contacts of type Member can be exported * Each file can contain information for either one or many contacts * If there is no new information, no file will be exported * If an export contains more than 10 000 contacts it will be divided into several files * The schedule of the export can be configured as required * Contacts are exported incrementally as either new, updated and deleted If a contact is created and then modified within the same export interval then Customer Engagement will combine these steps and export it just once as a new contact but with the updated data. Please read about [file based integration](/docs/file-based-transfer/about-file-based-transfer) before going any further! ## Triggered exports Any import that changes a contact's fields will automatically trigger an export. This can be suppressed by adding an attribute to your import XML. See the section about [importing contacts by XML](/docs/file-based-transfer/historical-data/importing-historical-contacts) for more information. ## Naming the export file Exported files follow this naming convention: **\[timestamp]\_\[filenumber]\_\[filename].xml** * timestamp is automatically formatted as: YYYYMMDDHHmmss * filenumber begins at 0. When that file contains 10 000 contacts, the next file is opened and filenumber increases by 1 * filename is a string that can be changed per tenant, in the Customer Engagement back-end Examples of exported files: * 20190723144514\_0\_customers.xml * 20190723144525\_1\_customers.xml * 20230507181112\_16\_members.xml ## FTP location for file exports Contact exports are placed on the Voyado Customer Engagement FTP, which can be found here: ```http theme={null} [base-ftp-url]/integration/customerExport ``` ## File example The file topology follows the structure below but it is possible to do client-specific XSLT transformations. The specific fields used will be decided based on the needs of the project. ```xml theme={null} Example Examplesson Examplegatan 12345 Examplestan example@example.com 46739111111 SE 198101010101 Sverige 1981-01-01 1000 0f8e4b81-d07f-4740-91fd-214a497b1b13 2021-05-17T11:55:45.0000000+02:00 0 true true true 00000000-0000-0000-0000-000000000000 affär 1234 00000000-0000-0000-0000-000000000000 affär 1234 ``` ```xml theme={null} Example Examplesson Examplegatan 12345 Examplestan example@example.com 46739111111 SE 198101010101 .......... ``` ```xml theme={null} ............ 1001 2016-09-28T00:00:00 2019-07-23T15:01:26.5371417+02:00 00000000-0000-0000-0000-000000000000 affär 1234 ``` For the standard fields used, see the page [Contact field definitions](/docs/contacts/contact-field-definitions). # Contact XML import Source: https://developer.voyado.com/docs/file-based-transfer/contact-xml-import The preferred way of creating and updating contacts in Voyado Customer Engagement is through the API. But sometimes file-based imports are more practical, for example when another system is the master or when it's not crucial that updates happen right away. There are some things to keep in mind about file-based updates: * Only new information generated after the last import should be transferred * Each file can contain information for one or more contacts * If there is no new information, then no file should be sent Please read about [file based integration](/docs/file-based-transfer/about-file-based-transfer) before going any further! Contact XML imports are not processed continuously and updates done in this way can take up to 24 hours to be completed. The XML import interface therefore does not reflect real-time changes in contact data. If you need that, use the contacts bulk API. See [here](/docs/contacts/bulk-contacts-api) and [here](/docs/contacts/bulk-contacts-api). ## Preparing the files Files sent to the FTP server should follow this naming convention: `[Timestamp]_[sequence number OR store id] _contacts.xml.` * \[Timestamp] should be formatted as: `YYYYMMDDHHmmss` (Y = year, M = month, D = day...) * The sequence number / store id is only needed if there is a risk that several applications might send XML files simultaneously * The number of contacts should not exceed the maximum specified on the [file based integration](/docs/file-based-transfer/about-file-based-transfer) page ## FTP location Again, see the [file based integration page](/docs/file-based-transfer/about-file-based-transfer). ## Validation Contacts are validated individually and the contact's fields are considered one by one. If validation fails for any of the contact's fields, then that field will not be created or updated. But other fields, as long as they are valid, will still be updated. An update to a contact will be skipped entirely if the validation error is grave, such as in the case of a missing mandatory key, or an age that is below the specified minimum age. (Note that age can either be specified in the import or derived from either the social security number / personal identity number or the birth date field.) If there is a major error during the import the entire file will be skipped. ## Using Contact Id as key It is possible to use contact id as identifier for a contact: `65D152A2-1C12-4E6B-93DC-1D2122554D30` When ` is included for at least one contact in the XML: * It will be used as key (ignoring other keys) * It will require all contacts in the XML to have \ specified (otherwise XSD validation will fail) * Only updates and deletes are allowed * Neither key fields nor mandatory fields are required ## File example Your XML should look like the example below, with variations depending on the number of fields used in your project. A contact inside a \ tag is interpreted by Customer Engagement as either new or modified, depending on if it already exists in the database or not. ```xml theme={null} 1526235 198010101010 Example Examplesson example@example.se +46739111111 Somebody Examplegatan 12345 Examplestan true true false true 2013-02-10T15:02:00+01:00 SE 2343 {/* You can also define other contact types */} example@example.se 1526235 ``` For the standard fields used, see the page [Contact field definitions](/docs/contacts/contact-field-definitions). ## Removing contacts To delete a contact, an XML block corresponding to the contact type (in this case, member) must be placed in the “deleted” section in the file, along with whatever unique identifier has been decided for the project. We recommend you also add the attribute deletionReason. | Field name | Format | Description | | - | - | - | | deletionReason (ATTRIBUTE) | varchar | Reason for deletion. Accepted values: MANUALLY\_REMOVED, DECEASED | Deleted contacts will remain in the database for seven days at which point complete anonymization of their data will occur. ## Importing without triggering export This is also known as a "silent export". After changes are made to your contacts through an XML file import (through updating, adding or deleting) normally an export is automatically triggered. If you don't want or need this to happen, you can use the attribute `avoidTriggeringExport`. Setting this to "true" will suppress both the BI and XML exports. You can read more about `avoidTriggeringExport` [on this page](/docs/file-based-transfer/avoid-triggering-export). Here is an example: ```xml theme={null} ``` For this to work the file can only consist of a ContactType key (such as "member") and true Contact Attributes (that don't exist on the contact table). If `avoidTriggeringExport` is set to true and the import file contains Contact Attributes that are not keys and also exist on the contact table (ex FirstName, Email), the import will fail and an error will be written to the integration log. If an import is done with only keys and true Contact Attributes but `avoidTriggeringExport` is false or missing, exports will be triggered and entries added to changelog and lastmodified. The attribute avoidTriggeringExport is false by default, so you only need to include it in your XML import if you want it to be true. # Example files Source: https://developer.voyado.com/docs/file-based-transfer/example-files Here are some useful example files you can download. ### Reward vouchers ### Points history ### Historical transactions ### Contacts ### Stores ### Transactions as XML ### Product feed as XML ### Articles as XML # File transfer by FTP Source: https://developer.voyado.com/docs/file-based-transfer/file-transfer-by-ftp This is how file transfer by FTP is done for your Customer Engagement environment. To transfer your files by FTP, you will need to install an FTP client on your end. For example, Filezilla, but there are many FTP clients to choose from. Importing data to Customer Engagement Exporting data from Customer Engagement When the import batch job is triggered, Customer Engagement begins processing files and importing the transactions. Once a file is processed you will be able to see the status of the import on the Integration log (see image below). The processed file will then be deleted from the FTP folder. The file integration log ## FTP access For both **staging** and **production**, you access your FTP like this: ```http theme={null} Domain: ftp.voyado.com ``` ```http theme={null} Port: 22 ``` ```http theme={null} Protocol: SFTP ``` Your FTP user configuration determines whether you have access to staging or production. Your username and password are assigned by your Voyado team. ## Preparing your file Here are some things to check before doing a CSV or XML file import. ### Maximum number of contacts Don't exceed 20 000 contacts per file. This is not a hard limit but a strong recommendation, since large files can make the server slow and risk causing a timeout. Keep the number of contacts per file under 20 000. ### Encoding and syntax The encoding of your CSV and XML files must be UTF-8. For XML files, follow the scheme specified per import type: It is also good practice (although not mandatory) to follow the XML-syntax provided for each import. When these field names are followed, no client-specific XSLT transformation needs to be done on the Customer Engagement side. ### Time and dates The required date format is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) with a time zone designator, for example: **2021-01-26T15:10:17+01:00**. Date of birth (for example on a Contacts migration) is allowed to also use the simpler: **YYYY-MM-DD**. ### Other CSV information Your CSV should use semicolon (;) as a delimiter to separate the fields. This also means that you can't use ; anywhere in a value string. The order of the fields in a CSV does not matter. Some characters can break a file import and might need to be *escaped* before import. For example, the double quote character ("). A double quote can be a part of a product name or description, since " is the symbol used for "inch": ```text String written normally theme={null} Samsung 42" TV ``` Fix this by adding a backslash before every " in your CSV (this is called "escaping" the character). A search-and-replace is probably the best way to do this. The example above will then be: ```text Escaped string theme={null} Samsung 42\" TV ``` # Importing historical contacts Source: https://developer.voyado.com/docs/file-based-transfer/historical-data/importing-historical-contacts You will likely need to bulk-import contacts into your Customer Engagement enviroment, when starting up your Customer Engagement account, or migrating, or when updating certain data points. There are a number of ways this can be done. Each has its own dedicated article. ## Manual import as CSV files When you have a small customer database of less than 500 000 records, manual import by CSV is the recommended way to import (or update) those contacts. ## Automated import as XML files For customer databases of 500 000 records or more, it is recommended to use the XML-importer. This is a more automated process where you export your contacts from your own database as XML files that are then placed on the Customer Engagement FTP server. From here they are imported by a scheduled job. This method has better high volume performance and superior error management. For this to work, the "ContactXmlImporter" module setting needs to be activated by your Customer Engagement team. ## Contacts bulk API Contacts can also be created and updated in bulk through a dedicated API endpoint. # Importing historical points Source: https://developer.voyado.com/docs/file-based-transfer/historical-data/importing-historical-points This article covers the importing of historical points *before* point calculations (including calculations via API, automations, and purchase/return activity) have been started for your Customer Engagement environment. If point calculations are *already active* when you want to do a points import, [follow this guide](/docs/file-based-transfer/importing-points#case-2-when-tenant-is-already-active) instead. To get your points data into Customer Engagement, you will need to migrate it. This migration is done by importing the points history, row by row, from a **CSV** or **XML** file. If you want to keep track of the entire points history for each of your contacts, this import allows you to do that. You can also choose to just import the current balance for a contact as a single row, without the history. Both negative and positive points values can be migrated in this way. For a successful import: * Do not exceed 20,000 rows in your CSV file (or `pointtransaction` nodes in your XML) - split into several files if needed * Be sure to include all the mandatory fields listed on the table below * If using a CSV make sure it's UTF-8 encoded and with semicolon separated values * As the `transactionType` value, use only "purchase", "return", "adjustment" or "rewardvoucher" * As the `keyType` value, use only "memberNumber", "externalId" or "contactId" * Only set a value in one of the fields “expireAfterMonthsInactive“ and “validTo“ * If the point transaction is already expired, set the date value of the expiration in the field `validTo`, but DO NOT set `expireAfterMonthsInactive` * If the settings for expiration are correctly set up AND the value of `validTo` or `expireAfterMonthsInactive` matches that expiration setting, these fields can both be set to blank and values will be added based on what is set up in these expiration settings Do not mix different keyType values in the same file as the importer takes the first one it finds and then uses that keyType for all the subsequent rows in the import file. For a points import to work, contacts of type "Member" must have one of following keys: memberNumber, externalId or contactId. If the contact does not have any of these keys, the import will fail and give an error. When importing a CSV, single and double quote -- the characters " and ' -- are so-called reserved characters and will be interpreted as the start and end of a string field. They can't be used for anything else, or the import will fail. For example " is sometimes used as the symbol for "inch" in product names. Having this in just one field in your CSV could break the import. ## If a contact doesn't exist When importing points transactions from a file, it can happen that the contact connected to the transaction might not exist in the database. If this happens, and `keyType` in your import has one of the values "memberNumber" or "externalId", then a new contact will be created. However, if this happens when `keyType` has the value "contactId", then a new contact will *not* be created. Customer Engagement is not able to create a new contact from a specified `contactId`. So, in this case, the whole file will be skipped and not imported. Any contact created in this way will be approved and their `firstname` and `lastname` will be “PointTransactionImport”. ## Structure of the import file This is what each row in your CSV or each `pointtransaction` node in your XML should contain. | Name | Description | | - | - | | \*key | The contact identification string (this can’t be empty). | | \*keyType | What `key` represents, which can be either "memberNumber", "externalId", or "contactId". | | \*transactionType | This must be either "purchase", "return", "adjustment" or "rewardvoucher". | | \*transactionDate | The timestamp of the points transaction. Replaces the older "createdOn". Example: 2019-06-13T12:00:25+1:00 | | \*pointDefinitionId | A positive value in the PointDefinition database table. The default (and currently only) value is 1. A value that doesn't exist in the PointDefinition table will cause a validation error. | | \*amount | The number of points added (positive) or removed (negative). | | description | Optional string | | validTo | Optional string | | createdBy | Optional string | | source | We recommend leaving the source blank. | | expireAfterMonthsInactive | Optional integer | Mandatory fields are marked with \*. The other fields are optional but recommended. The names in the column "Name" can start with a lower-case or upper-case letter. The fields `validto` and `expireAfterMonthsInactive` cannot be added in the same row. ## Example of values | **key** | **keyType** | **pointDefinitionId** | **transactionType** | **amount** | **transactionDate** | **description** | | :- | :- | :- | :- | :- | :- | :- | | 1567 | memberNumber | 1 | purchase | 100 | 2021-08-21T08:16:59+02:00 | Trousers | | 1839 | memberNumber | 1 | purchase | 100 | 2022-10-17T20:36:26+02:00 | Hat | | 1544 | memberNumber | 1 | return | -100 | 2021-12-23T14:05:25+01:00 | Gloves | | 1356 | memberNumber | 1 | purchase | 100 | 2022-03-02T00:47:20+01:00 | Pauldrons | | 1644 | memberNumber | 1 | rewardvoucher | -100 | 2021-05-21T11:17:54+02:00 | Converted to voucher | | 1567 | memberNumber | 1 | rewardvoucher | -100 | 2021-11-24T01:04:38+01:00 | Converted to voucher | | 2135 | memberNumber | 1 | purchase | 4210 | 2021-10-24T01:04:38+01:00 | Accumulated points | For the example contact with `key` of 2135, all their points are imported as one entry, without history. Currently, Customer Engagement supports only one point definition, with a value of 1. Support for using several point definitions will be added in the future. You can download an example CSV and XML file on the example files page: Keep your CSV to 20,000 rows or fewer, and your XML to 20,000 `pointtransaction` nodes or fewer. ## Importing the file Your CSV or XML file / files must now be placed on your FTP inside a folder named **Integration/PointTransactionImport** The actual processing of your files must be manually triggered by your Voyado Customer Engagement team. The result of the integration, including the validation check, will be shown in the integration log as type **Data migration**. ## File validation Each file is validated before it is processed. If any error is found during the validation of a file, it will be logged in the integration log and the whole file will be skipped. This is a safety feature since unique IDs are not used for the rows inside the files. Validation will then continue with the next file. The following validations are applied to all mandatory fields in your files: * All returns must have a negative value * All purchases must have a positive value * The `amount` value must be a number, either integer or decimal * The `PointDefinitionId` value has to be a positive integer value and must exist in the "PointDefinitionId" database table * The `transactionDate` value must be in the format`2019-06-13T12:00:25+1:00` * The `transactionType` value must be either "purchase", "return", "adjustment" or "rewardvoucher" * The `expireAfterMonthsInactive` must be a positive number higher than 0 * The fields `validto` and `expireAfterMonthsInactive` cannot be added in the same row ## Performing the import After your files pass validation, the actual import is done like this: 1. They will be imported to blob storage and remain there until all files in the batch are imported 2. The system then reads the files from blob storage and start importing all rows 3. The system then updates `PointAmountLeft` for all contacts so it matches the imported data # Importing historical transactions Source: https://developer.voyado.com/docs/file-based-transfer/historical-data/importing-historical-transactions When getting started with Voyado Customer Engagement, historical transactions are valuable as a way to gain insights and allow you to try out more advanced segmentation. However, the Customer Engagement API should never be used to perform operations on a large number of transactions simultaneously (a so-called bulk update). For doing this, our recommended approach is to migrate your transactions via a file, either as XML or CSV. The key questions you need to answer before a file-based migration are: * How far back should I go? (Voyado usually recommends 24 months) * Do I plan to add additional product data to the transactions? * Does my data include transactions that aren't relevant? For example, those of deleted contacts or with irrelevant products. Any such migration needs to be planned with your Voyado team. Migrations are handled separately from automated imports. In Customer Engagement, a transaction is defined as when a customer visits a physical or online store and purchases (or returns) one or more items. The transaction contains metadata -- such as the transaction's unique ID, the store's ID, the timestamp -- plus the data for each individual item purchased or returned (line items). The transaction's data is then sent to the endpoint /receipts to be saved so you might see the term "receipt" turning up in the API. An import of historical transactions by file will NOT trigger the "Product purchase" automation trigger. ## Prerequisites Here are some important things to confirm before you import your historical transactions. * All contacts referred to in your file must already exist in Customer Engagement before the import of transactions is started. If a contact does not exist, the transaction data connected to them will be ignored. * Likewise, all stores referred to in the file must already exist in Customer Engagement. * If an imported transaction already exists in Customer Engagement then the imported transaction data will be ignored. No updates will be done to the existing transaction. * Don't exceed 20 000 receipts per file. This is not a hard limit but a strong recommendation. Large files can make the server very slow and risk causing a timeout. Note that the number refers to transaction *lines* and not transactions. * If the import is done in CSV format the columns should be separated by semicolons and *not* by commas. * For dates, Customer Engagement requires the data formatted as ISO8601 with Time zone designators. It is important to only use the `matchKeyType` that is configured for your Customer Engagement environment when importing transactions, otherwise your import will fail. Allowed values for `matchKeyType` are: * email * mobilePhone * externalId * memberNumber * socialSecurityNumber ## Validation of your files Validation of your CSV or XML files is an important step before importing. It ensures your data has the correct format and will greatly reduce the possibility of errors and lost data. Use the Customer Engagement validation service to make sure your files are in the correct format. You can read more about the service below. When using the data validator for transactions, be aware that it will NOT detect missing stores or contacts, because the validator is not directly connected to a specific Customer Engagement environment. The client should ensure that the stores referenced exist in Customer Engagement, as do the contacts. However, when importing that file into Customer Engagement, any "bad" lines will just be skipped and logged as errors. ## File import location When your files have been prepared and approved on your side, they'll need to be placed in a designated folder on the Customer Engagement FTP for the actual import to happen. Place your transaction files in the folder named transactionMigration. If this folder has not been created, you can create it manually in the FTP client if you have sufficient access rights. [Read more about the import steps here](/docs/file-based-transfer/about-file-based-transfer). If you need help, contact your Voyado team. ## CSV file import You can download CSV and Excel examples [on the example files page](/docs/file-based-transfer/example-files). There are two kinds of rows in a CSV import, header rows and item rows. * Header rows hold information about the transaction * Item rows are the items involved in that transaction Whether a row is a header or an item is determined by the the field called **header** which is 1 for header rows and 0 for item rows. ### Header fields *Fields marked with \* are mandatory.* | **Field** | **Example** | **Type** | **Description** | | :- | :- | :- | :- | | \*matchKey | [doe@voyado.com](mailto:doe@voyado.com) | string | A value to match against the specified customerKeyType and contactType | | \*matchKeyType | email | string | Valid options: memberNumber, socialSecurityNumber, mobilePhone, externalId, email | | \*contactType | member | string | Which contact type to match the transaction with | | \*uniqueReceiptId | 2287565954-25 | string | Needs to be a unique number for each transaction | | \*receiptNumber | 2287565954 | string | The transaction number you segment on in Customer Engagement. Does not have to be unique | | \*createdDate | 2020-09-06T15:55:54+01:00 | datetime | Date and time when the transaction was created formatted according to ISO 8601 with Time Zone designator | | \*storeExternalId | 930 | string | The unique ID for the store where the purchase was made | | \*currency | EUR | currency | The currency code, according to ISO 4217, for the currency in which the customer paid (local currency) | | \*exchangeRateToGroupCurrency | 10.00 | decimal | The exchange rate to the group currency set in Customer Engagement. Default value is 1.00 | | \*totalGrossPrice | 200.00 | decimal | The total price paid by the customer including VAT. A decimal point (.) is always used as decimal separator. Default is 0 | | \*header | 1 | bit | Should always have the value 1 if it is a transaction header | | paymentTypeType | creditcard | string | Pre-defined values can be set: “card”, “cash”, “creditcard”, “bonuscheck”, “swish”, “klarna”. If your value does not match one of the above, it will be categorized as custom: “custom: \[value]”(max 245 characters) | | paymentTypeValue | 200.00 | decimal | Amount in decimal format, cannot be empty | | paymentTypeDescription | Mastercard | string | A descriptive text (max 255 characters) | Only the `matchKeyType` that is actively configured for your Customer Engagement tenant will work when importing transactions. To avoid failed imports, verify which `matchKeyType` is configured in your environment before use. ### Item row fields *Fields marked with \* are mandatory.* | **Field** | **Example** | **Type** | **Description** | | :- | :- | :- | :- | | \*type | PURCHASE | string | A line item can either be of type PURCHASE or RETURN | | \*sku | 12345-1 | string | Store keeping unit of the article | | \*articleName | Denim white | string | Article name | | articleGroup | Jeans | string | The highest level of the article group categorization to segment on in Voyado | | \*quantity | 1 | integer | The number of units sold in the specific line item. If it is a RETURN then it must be a negative value. | | \*grossPaidPrice | 200.00 | decimal | The total price of units in the specific line item including VAT and the discount withdrawal | | articleNumber | 12345 | string | Article number | | awardsBonus | true | boolean | If the line item included for reward calculation or not. Default value is **true** if the field is not defined | | taxAmount | 40.00 | decimal | The total VAT of the units in the line item. Default is 0 | | taxPercent | 25.00 | decimal | The VAT percentage of the unit. Default is 0 | | discount | -200.00 | decimal | Discount value in local currency including VAT / MOMS. Should always be negative. Segmentation on discounts is possible in Voyado. Default is 0 | | spare\[1-10] | | string | Additional data for the line item | | datespare\[1-5] | 2020-09-06T15:55:54+01:00 | datetime | Additional datetime data for the line item | | \*header | 0 | bit | Must have value 0 if it is a line item row | Unusual characters might give an error during a CSV validation or import. One of these is the character " (ASCII code Alt 0148). If your import fails for unknown reasons, check for this and other unusual characters. ## XML file import If your import data is in XML format, it must have the following header: ```xml theme={null} ``` A complete XML file for importing transactions will look something like this: ```xml theme={null} 120200000044 memberNumber Member 7a5f0140-c50f-11eb-8984-5b9396574b87 7a5f0140-c50f-11eb-8984-5b9396574b87 2020-08-06T08:29:40+02:00 8202 SEK 1 200 bonuscheck 50kr bonuscheck 50.00 card card 150.00 PURCHASE A123 1 250 ArticleNumber123 ArticleName 1 Group 1 0 false Spare 1 Spare 2 RETURN A123 -1 50 ArticleNumber456 ArticleName 2 Group 2 0 false Spare 3 ``` Due to technical limitations, only one discount amount can be processed per file. If multiple discounts need to be used, summarize the values into a single discount amount. It's important to note that when sending **paymentMethod** in the XML you need to include all three elements, type, value and description, as seen in the example above. The mandatory fields in an XML import are the same as in the CSV import. See the tables above. ## Validation service There is an online validation service you can use to make sure your transactions (receipts) and points files are correctly formatted before you try to import them into Customer Engagement. You'll find the online validation here: [https://datavalidator.voyado.com](https://datavalidator.voyado.com/). Follow the [guidelines laid out in this section](/docs/file-based-transfer/historical-data/importing-historical-transactions). When using the data validator for transactions, be aware that it will NOT detect missing stores or contacts, because the validator is not directly connected to a specific Customer Engagement environment. The client should ensure that the stores referenced exist in Customer Engagement, as do the contacts. However, when importing that file into Customer Engagement, any "bad" lines will just be skipped and logged as errors. ## Validation service - local The local validation service is no longer supported. Validation is now done through the [web-based validation service](/docs/file-based-transfer/historical-data/importing-historical-transactions#validation-service). # Importing reward vouchers Source: https://developer.voyado.com/docs/file-based-transfer/historical-data/importing-reward-vouchers Reward vouchers can be imported by file (CSV, UTF-8). Each import file should *not* contain more than 20,000 rows. ### The file | **Column name** | **Description** | | :- | :- | | \*MemberKey | Key (hard-coded to attribute memberNumber) | | \*PromotionName | Name of the campaign that the voucher might be part of | | \*CreatedOn | Creation date | | \*ExpiryDate | Expiration date | | \*ValidFrom | Valid-from date, can differ from creation date | | \*RewardVoucherValues | Defines how a check amount is converted into the currencies that the customer currently uses | | \*BonusPoints | Amount of reward points converted | | \*Value | Value of the reward voucher | | \*UniqueCode | Voucher's unique code, preferably using another series than what Customer Engagement is using, to prevent duplicates | | \*Used | If the voucher has already been used (1 or 0) | | UsedBy | Field to say who used the voucher (optional) | | UsedStoreId | Field to show which store received the voucher (optional) | | UsedByInvoiceExternalId | Field to specify which invoice the voucher was used on (optional) | Mandatory fields are marked with \*. ### Example | MemberKey | PromotionName | CreatedOn | ExpiryDate | ValidFrom | RewardVoucherValues | BonusPoints | Value | UniqueCode | Used | UsedBy | UsedStoreId | UsedByInvoiceExternalId | | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | :- | | 201 | Q1 bonus | 2018-01-19 10:36:57.000 | 2018-07-19 10:36:57.000 | 2018-01-19 10:36:57.000 | SEE NOTE | 1000 | 100 | A0001 | 1 | | 679 | INVOICE-1 | | 16 | Q4 bonus | 2017-11-19 10:36:57.000 | 2018-05-19 10:36:57.000 | 2017-11-19 10:36:57.000 | SEE NOTE | 1000 | 100 | A0002 | 0 | | 567 | INVOICE-2 | The values for `RewardVoucherValues` will be something like: ```json theme={null} [{"Currency":"EUR","Amount":5.00}, {"Currency":"NOK","Amount":50.00}, {"Currency":"SEK","Amount":50.00}] ``` The file should then be placed on your client FTP inside a folder named "BonusCheckImport". **File example** You can download examples as CSV and Excel [on the example files page](/docs/file-based-transfer/example-files). **Starting the import** There is no scheduled job to process the file. It must instead be triggered manually by your Voyado team. The result, including the validation check, will be shown in the [integration log](/docs/file-based-transfer/integration-log) under **Data migration**. # Importing stores Source: https://developer.voyado.com/docs/file-based-transfer/historical-data/importing-stores Stores can either be added manually one by one, or via file import (CSV, UTF-8 encoded). If you are importing stores, we recommended you do it before anything else is imported. This is because contacts (which are a separate import) might contain store references in their data, such as favorite store or recruited-by store, requiring the stores data to already be in place. A transactions import will also require stores to be set up first. Make sure stores are imported / set up before any other kind of data is imported. When using a file import, your CSV should use semicolon (;) as a delimiter to separate the fields. The order of the fields doesn't matter. ### Import stores by API You can create a store in Voyado Customer Engagement through this API endpoint: ```http theme={null} POST /api/v3/stores ``` See your Swagger page for an example of the payload. ### Import stores by CSV There are three mandatory fields (see table below). Apart from these you only need to include the fields you want to set. | **Field** | **Example** | **Type** | **Description** | | :- | :- | :- | :- | | \*externalId | 222 | string | The store's unique id. | | \*name | The Store | string | The name of the store. | | \*timeZone | Europe/Stockholm | string | The time zone. | | type | ECOMM | string | The store's type, either ECOMM (eCommerce) or RETAIL (physical). | Download the file example to see all the optional fields. In **externalId** there is a small letter L followed by a large letter I that might be hard to tell apart in certain fonts. **File example** You can download examples as CSV and Excel [on the example files page](/docs/file-based-transfer/example-files). **Starting the import** Processing of the file must be manually triggered by your Voyado Customer Engagement team. # Importing points Source: https://developer.voyado.com/docs/file-based-transfer/importing-points Importing points by file in the new points system works differently depending on if the Customer Engagement tenant is being configured *for the first time*, or if the Customer Engagement tenant is *already up and running*. The folders where the files are placed have similar names, and these are easily confused, so take care to put them in right place depending on which of the two cases apply to you. Each import is done by running a different scheduled job. There is also a difference in how contacts are identified in the two imports. The initial setup import will allow you to choose from a range of key types and keys (for example “memberNumber”), while in the ongoing points import you'll need to use the internal contact ID. ## Case 1 - When setting up tenant Your CSV or XML file, formatted in the correct way, should be placed in this FTP folder: ```shell theme={null} integration\pointTransactionImport ``` The job to run is `ImportHistoricalPointsFromFileJob` which will validate and import historical point files and store them in the database. ```csv Example with ValidTo theme={null} Key;KeyType;PointDefinitionId;Amount;TransactionType;TransactionDate;CreatedOn;ValidTo;CreatedBy;Description;Source;ExpireAfterMonthsInactive ABC123;MemberNumber;1;100.5;Purchase;2023-01-01T08:14:00.829Z;2023-01-01T08:14:00.829Z;2024-01-01T08:14:00.829Z;test@voyado.com;Test description;; ABC123;MemberNumber;1;100;Purchase;2023-02-01T08:14:00.829Z;2023-02-01T08:14:00.829Z;2024-02-01T08:14:00.829Z;test@voyado.com;Test description1;; ABC123;MemberNumber;1;-100;Return;2023-03-01T08:14:00.829Z;2023-03-01T08:14:00.829Z;2024-03-01T08:14:00.829Z;test@voyado.com;Test description2;; ``` ```csv Example with ExpireAfterMonthsInactive theme={null} Key;KeyType;PointDefinitionId;Amount;TransactionType;TransactionDate;CreatedOn;ValidTo;CreatedBy;Description;Source;ExpireAfterMonthsInactive ABC123;MemberNumber;1;100.5;Purchase;2023-01-01T08:14:00.829Z;2023-01-01T08:14:00.829Z;;test@voyado.com;Test description;;12 ABC123;MemberNumber;1;100;Purchase;2023-02-01T08:14:00.829Z;2023-02-01T08:14:00.829Z;;test@voyado.com;Test description1;;12 ABC123;MemberNumber;1;-100;Return;2023-03-01T08:14:00.829Z;2023-03-01T08:14:00.829Z;;test@voyado.com;Test description2;;6 ``` The CSV file must always include both the `ValidTo` and `ExpireAfterMonthsInactive` columns. However, for each row, only *one* of these fields can contain a value. The unused field must be left empty, and not removed from the file. For the setting-up case, it's important that no other point calculation are going on at the same time, such as points being added by API or automations. This import must be the only point job that is running. For the second job (see Case 2 below) it's not a problem to have several points calculations going at once. Always use . (a dot) for non-integer amounts in files. Using , (a comma) or anything else will cause an error. For example "10.50" is correct while "10,50" is not correct. ## Case 2 - When tenant is already active When the tenant is already active, another job is used. This is a bit slower but more safe and ensures that no data is modified incorrectly. Your CSV or XML file, formatted in the correct way, should be placed in this FTP folder: ```shell theme={null} integration\pointTransaction ``` CSV files in these imports should have a maximum of 2000 rows. Multiple files can be imported in the same run. The job to run is `ImportPointsFromFileJob` and this validates, reads and imports the points. ### Procedure Create a file, and add it to the folder specified. Then run the scheduled job mentioned in the information. When the job runs it will handle all files added to the folder. The import will add points in the same way as points are usually added in the system. To have the correct balance for point accounts after the import, the Update Balance job needs to run. ### Properties used Must be an existing, not deleted, approved contact of type Member. Cannot be 0. Definition must exist in Customer Engagement. Can be NULL. Defaults to UTC Now Offset. Should not be empty. If points-based member levels are used, and you want to exclude some points, only use these predefined values: * Purchase * Adjustment * BonusBalanceAdjustment * Return * RewardVoucher * UpdateBalance * BonusPromotion * BonusPromotionReturn * Automation Should not be empty. Can be excluded from the file. Defaults to UTC Now Offset. Can be excluded from the file. Defaults to the expiry rules set up for the tenant (for example, 12 months rolling expiry). Cannot be combined with a value in `ExpireAfterMonthsInactive`. One or the other may be used, or neither — never both in the same row. Can be excluded from the file. Defaults to the expiry rules set up for the tenant (for example, “6” months). Must be higher than 0. Cannot be combined with a value in `ValidTo`. One or the other may be used or neither, but never both in the same row. Only to be used on active transactions. Transactions that have already expired must have an expiry date in `ValidTo` and no value in this field. Only applies if using the expiry method “Contact inactive”. These settings currently only work with predefined legacy fields that can translate to the new predefined ones. If the tenant setting for expiration is correctly set up AND the value of `validTo` or `expireAfterMonthsInactive` matches that expiration setting, these fields can both be set to blank and Customer Engagement will add the values based on what is set up in these expiration settings. ```csv Example with ValidTo theme={null} ContactId;DefinitionId;Amount;ValidFrom;ValidTo;Description;Source;ExpireAfterMonthsInactive;TimeStamp a20fe4f3-5cca-491f-ba2a-e6f04ff0d4d8;1;100.54;2023-01-01T08:14:00.829Z;2024-01-01T08:14:00.829Z;Test description;Purchase;;2023-01-01T08:14:00.829Z db9b702c-9087-4c22-82a3-f133771a6c5f;1;100;2023-02-01T08:14:00.829Z;2024-02-01T08:14:00.829Z;Test description1;Adjustment;;2023-02-01T08:14:00.829Z 1dc891cb-ae93-492e-9d4d-aa985f3d781d;1;-100;2023-03-01T08:14:00.829Z;2024-03-01T08:14:00.829Z;Test description2;Return;;2023-03-01T08:14:00.829Z ``` ```csv Example with ExpireAfterMonthsInactive theme={null} ContactId;DefinitionId;Amount;ValidFrom;ValidTo;Description;Source;ExpireAfterMonthsInactive;TimeStamp a20fe4f3-5cca-491f-ba2a-e6f04ff0d4d8;1;100.54;2023-01-01T08:14:00.829Z;;Test description;Purchase;12;2023-01-01T08:14:00.829Z db9b702c-9087-4c22-82a3-f133771a6c5f;1;100;2023-02-01T08:14:00.829Z;;Test description1;Adjustment;12;2023-02-01T08:14:00.829Z 1dc891cb-ae93-492e-9d4d-aa985f3d781d;1;-100;2023-03-01T08:14:00.829Z;;Test description2;Return;6;2023-03-01T08:14:00.829Z ``` The CSV file must always include both the `ValidTo` and `ExpireAfterMonthsInactive` columns. However, for each row, only *one* of these fields can contain a value. The unused field must be left empty, and not removed from the file. ## Validation errors Here are the validations errors you might get and what they mean. *PointTransaction file import missing mandatory columns, will skip file: \[FileName]*\ Here, the import file does not define all fields used. The file will be skipped and the error logged. *Missing contactId: \[contactId] In data that was not valid: \[\[notAccepted]]*\ The contact ID used does not represent an existing, not-deleted and approved contact of type “Member”. The file line will be skipped and the error logged. *Missing PointDefinitionId: \[\[definitionIds]] In data that was not valid: \[\[notAccepted]]*\ Definition ID does not have a corresponding point definition in the database. File line will be skipped and the error logged. *Other error message: \[\[error]] In data that was not valid: \[\[notAccepted]]*\ Source does not have a value. File line will be skipped and the error logged. *Other error message: \[\[error]] In data that was not valid: \[\[notAccepted]]*\ Description does not have a value. File line will be skipped and the error logged. *No error message*\ Points value given is zero. No points added, no logs or errors created. *No error message*\ Point account with the specified definition does not exist, or exists several times. Creation of new account is not working as intended. An exception is created. *Other error message: \[\[error]] In data that was not valid: \[\[notAccepted]]*\ The value `ExpireAfterMonthsInactive` is negative. File line will be skipped and the error logged. *Other error message: \[\[error]] In data that was not valid: \[\[notAccepted]]*\ The value `ExpireAfterMonthsInactive` is 0. File line will be skipped and the error logged. *Other error message: \[\[error]]*\ The value `ExpireAfterMonthsInactive` has a valid integer value and `ValidTo` also has a date value. File line will be skipped and the error logged. # Integration log Source: https://developer.voyado.com/docs/file-based-transfer/integration-log ## Integration log All file-based integrations create an entry in the integration log. ### Finding the integration log You’ll find the integration log in the Administration menu. integrationlog-01 Here you can see a list of all integration jobs, failed as well as succeeded. integrationlog-02 In the **Status** dropdown, you can filter the logs by status (Warning, Failures, Completed). In the **Log types** dropdown, you can filter for type of entry (BI export, Merge contacts, and so on). In the **From date** and **To date** selectors, you can filter log entries by date. Select "Update the list" after changing your filters to refresh the results. ### Email error notification It is possible to configure Customer Engagement to send out an email whenever an error is logged in the integration log. This allows problems to be flagged as soon as they happen and issues with file integrations to be quickly identified. As a Customer Engagement user, you can specify one or several email addresses to receive these error messages. An email will be sent to this address for every integration log entry made with status Failures. Ask you Customer Engagement team to set this up for you. Any filtering of these emails (for example, forwarding specific mails to specific users based on keywords) will need to be done in your own mail system. Using a group email might therefore be a good idea, to avoid having important error information going to a single person who might be out of office or who has ended their employment. Ask your Voyado team to set up email notifications for integration log errors. # Promotions XML create & assign Source: https://developer.voyado.com/docs/file-based-transfer/promotions-xml-create-assign Multichannel promotions can be created and assigned to specific contacts: * In the Customer Engagement UI * Via the API * By XML file import This article goes through creating and assigning multichannel promotions by XML file import. When assigning a promotion by XML file, the promotion needs to exist in Customer Engagement prior to the assignment, or at least be added in the same file as the assignment. ## Create multichannel promotion Your XML file can either contain the minimum of information or have a number of extra fields. This is the minimal amount of information you need in your XML file to create a promotion: ```xml Minimal information theme={null} ``` The promotion's name as shown in the POS, for example. External ID of the promotion. Must be unique for a new promotion to be created. Max 450 characters. Here is the complete XML model for creating a promotion. ```xml theme={null} ``` The promotion's name as shown in the POS, for example. The external ID of the promotion. It must be unique for a new promotion to be created. Max 450 characters. Will default to "true" if not specified. If not specified and this is a new promotion, the current date will be used. The last day of validity. Will default to `promotionName` if not specified Will default to `promotionName` if not specified If none is specified, then one channel of each type ECOM and POS will be created with value type `EXTERNALOFFER`. **Important:** Redemption channels can only be configured when the promotion is first created via XML import. Subsequent imports updating the same promotion (matched by `externalPromotionId`) will NOT modify redemption channels. Must be `ECOM` or `POS`. Valid values: `EXTERNALOFFER`, `PERCENT`, `MONEY`, `MANUAL`. If the `redemptionChannels` element is omitted entirely, all channels default to `EXTERNALOFFER`. Will default to `promotionName` if not specified. Max 512 characters. Defaults to null if not specified. A promotion that is active and has a `validFromDate` in the future can be found under **Scheduled Promotions** in the Customer Engagement UI. ## Assign promotion to contact Adding a promotion code to a promotion will set the "published status" of the promotion to "true". ```xml theme={null} ``` The external id of the promotion. Max 450 characters. The external ID of the promotion code. Must be unique across all promotions (not just unique in the current promotion). Max 450 characters. Customer Engagement contact ID (GUID) for the contact. Max length 255. **Deprecated.** Will be removed in a future version. Do not use. Overrides the promotion's `validFromDate` for this customer and coupon. Must be same or later than the promotion's `validFromDate`. Example: 2022-02-21. Overrides the promotion's `validToDate` for this customer and coupon. Must be same or later than the promotion's `validToDate`. Example: 2022-02-22. The code exposed to the customer. Adding a promotion code to a promotion will set the published status of the promotion to "true". ## Create and assign to specific contacts In this example, the promotion is created and assigned in the same file. Only one promotion per file is supported. To import multiple promotions, place multiple XML files in the import folder. The job processes all .xml files sequentially. There is a maximum 10,000 promotion codes per promotion per file. ```xml theme={null} ``` See the previous section for the field descriptions. ## File handling after import After the import job processes a file, you will get one of these results: * **Successful import:** The file is moved to an "Archive" subfolder within the import directory. * **Validation or import failure:** The file is moved to an "Error" subfolder, along with a ".log" file containing error details and line numbers. * **File sharing violation or SQL timeout:** The file remains in the import folder and will be retried on the next job run. # Promotions XML export Source: https://developer.voyado.com/docs/file-based-transfer/promotions-xml-export Customer Engagement can export assigned, redeemed and deleted promotions as XML files. Customer Engagement listens for changes to promotions assigned to contacts and exports these changes on a configurable schedule to the Customer Engagement internal FTP-server. The XML files created can be fetched from there and consumed by third-party systems. Be aware that new and redeemed promotions are exported in *separate files* even though they are exported in the same run. Main promotions are *not* included in these XML exports, only the *assigned* promotions for individual contacts that were based on them. If a main promotion is terminated early, it will not be included in the XML export and its assigned promotions will not be automatically cancelled. ## Field descriptions Below are Customer Engagement's default export fields. Note that these field names can be changed with a XSLT-transformation created by a product specialist. | **Field** | **Example value** | **Description** | | :- | :- | :- | | contactId | 7b89b95e-6347-4d66-8b02-a10a9c830119 | Customer Engagement's internal contact GUID | | contactKey | 4398423 | The value of the set key for clients environment | | contactType | Member | The contact type of the contact | | couponId | 32e0f814-eb7b-4857-8c05-a8e900c43e44 | The unique id for the contact and promotion combined. Used for redeeming. | | description | Spring chainmail promotion | A description of the promotion | | promotionId | 44a0f722-1f28-4965-a648-a8e8010c86d5 | The promotion template id | | externalPromotionId | 12345 | The promotion's ID in another system. | | validFromDate | 2018-05-26T00:00:00+01:00 | When the promotion becomes valid | | expireDateTime | 2019-05-26T00:00:00+01:00 | The expiry date of the promotion | | redemptionChannels | | Tells you in which channels the promotion can be used. | | type | POS or e-com | Value under redemptionChannels. Tells you if that redemption channel is POS or e-com | | valuetype | MONEY, PERCENT or EXTERNALOFFER | Value under redemptionChannels. Shows if the promotion is monetary, a percentage or an external promotion | | value | 20.00 | Value under redemptionChannels. The amount to be applied to the receipt as a discount. Can be a percentage (20.00), an amount (100.00) or an external code defined for type EXTERNALOFFER (10OFFSHOES). | | instruction | 10 % discount | Value under redemptionChannels. A message you can write in Customer Engagement which will be presented in the POS for the staff to read. | | currency | SEK | The currency is of the monetary discount | | storeId | 1005 | In what store/e-com the promotion was used | | storeType | RETAIL/ECOM | If the store was a physical store or an e-com | | redeemedDateTime | 2020-09-29T08:14:50.0168149+01:00 | What date and time the promotion was redeemed | | deletedDateTime | 2020-09-29T08:14:50.0168149+01:00 | What date and time the promotion was deleted in Customer Engagement. | ## Default XML-format Below is the default format of an XML-export (broken into three parts). The format, if desired, can be transformed and changed into another format more suited for the third party system. ```xml theme={null} c9618bab-ad41-4eae-978e-4836305a9aa8 2eca7904-5b05-4796-bf68-a322e3e4a589 dda2e506-41da-44ae-9950-c12d3e661657 789 Member 2020-09-29T09:27:22.345926+02:00 2020-09-29T09:27:22.345926+02:00 2020-10-29T09:27:22.345926+01:00 123456 Description from presentation POS EXTERNALOFFER Instruction POSDISCOUNT20 SEK ECOM EXTERNALOFFER ECOMDISCOUNT20 ... ``` ```xml theme={null} ... d1f4b5c8-4e0b-4f90-9e0c-7c1268ca7119 ab3ac06e-f528-4aef-92d8-f425b26d17d3 113297af-58b8-493b-ada3-f737d6f9d7a5 789 Member 2020-09-29T09:27:22.345926+02:00 123 RETAIL ... ``` ```xml theme={null} ... 96e5da31-ec8d-4fb8-a0e4-099dfa8c0e60 d68c6f69-cfcf-433e-8303-3dde2f8db152 df29a1da-821d-4ec6-a1e0-dd2abcc47502 789 Member 2020-09-29T09:27:22.345926+02:00 ``` # Transactions XML import Source: https://developer.voyado.com/docs/file-based-transfer/transactions-xml-import The API is the preferred way of creating and updating contacts in Customer Engagement. However, sometimes file-based imports are more suitable, for example when another system is the master and immediate updates are not crucial. Customer Engagement connects all purchases to the contact who made them and this can later be used for segmentation and analysis. The transaction object has some flexibility in order to fulfill a client's specific needs. In addition to the standard fields, you can add up to 10 extra string fields and up to 5 extra date fields which can be used for segmentation. Only include fields that are necessary since every additional field can affect the overall performance. Read more about transactions [here](/docs/transactions/transaction-field-definitions). Please read about [file based integration](/docs/file-based-transfer/about-file-based-transfer) before going any further! ## Naming the files Files sent to the FTP server should follow this naming convention: \[Timestamp]\_\[sequence number OR store id] \_transactions.xml \[Timestamp] should be formatted as: "YYYYMMDDHHmmss". The sequence or store id is only needed if there's a risk that several applications might send XML files at the same time, although it is never wrong to include it. A correct filename has this form: *20220808134729\_471\_transactions.xml* ## FTP location The XML files should be placed here: ```http theme={null} [base-ftp-url]/integration/transactionImport ``` ## Scheme and field definitions You can download the basic XSD scheme used for the XML import [on the example files page](/docs/file-based-transfer/example-files). Find the complete list of Field definitions [here](/docs/transactions/transaction-field-definitions). Spare fields are handled differently in API and file-based XML imports. In file-based XML imports, item spare fields must be sent as explicit XML elements such as `spare1`, `spare2`, and `datespare1`. For example: `true` Do *not* send item spare fields using `extraData` item names in XML, even if the same fields are sent that way in the API. ## Register a purchase Read more about registering a purchase [here](/docs/transactions/register-a-purchase). ```xml theme={null} 90154424 memberNumber Member 14534304434293 87684684684 2019-07-23T09:02:01+02:00 12313 SEK 1.1 200.00 bonuscheck 25kr bonuscheck 25.00 creditcard Mastecard 175.00 489456* ICA Banken 45343 625bafb8-00ee-44c1-aacc-aa9400a77cbb PURCHASE 2 200.00 40.00 25.00 23424123456 123456 Stickad tröja V-neck Tröjor 25.00 false true         ABC123 2026-07-23T09:02:01+02:00 325 50% rabatt nedsatt vara -200.00 ``` ### Register a return Read about registering a return [here](/docs/transactions/register-a-return). # About integration Source: https://developer.voyado.com/docs/integration/about-integration Below is an overview of how to integrate with the Customer Engagement platform. It covers: * Available integration patterns * The authentication model * Key considerations for data synchronization and error handling Use this section to consider the right integration approach before diving into the implementation details. ## Integration patterns The Customer Engagement platform supports multiple integration patterns depending on use case, your data volume, and your latency requirements. * **API**: Synchronous, request-based access to platform data and operations. Suitable for real-time and transactional workflows where your system actively reads or writes data. * **Webhooks**: Event-driven notifications sent from Customer Engagement to your system. Used for reacting immediately to changes without the need for polling. * **Technology partners**: Using a pre-made integration with one of Customer Engagement's many technology partners. * **File-based imports**: Scheduled or on-demand FTP batch imports using files. * **File-based exports**: Batch exports delivered as files via FTP, suitable for downstream systems that consume data from files. * **Delta sharing**: Incremental data access designed for analytics platforms and data lakes. Requires a dedicated setup and is updated once per day. * **BI export**: Curated analytical datasets, updated once per day. Used for datasets not yet available via delta sharing. Not intended for operational integrations. ## Authentication Authentication in Customer Engagement works like this: * All integrations authenticate use *API keys*. * The API key is included in the request header for each API call. * Multiple keys can be created, but all keys currently have the same level of access. * Token-based authentication and scoped permissions are not supported. * Different integration patterns provide different guarantees concerning data freshness and consistency. * Customer Engagement does not support request-level idempotency. Duplicate prevention is handled through **configurable duplicate control**, where you define which customer attributes must be unique. Integrations should be designed with this in mind, particularly for retries or reprocessing data. ## Error handling Error handling depends on the integration method used. * APIs require synchronous error handling per request. * Webhooks require validation and acknowledgement of incoming events. * Imports and file-based exports require monitoring of job execution and result files. * Delta sharing and BI export require validation of data completeness per refresh cycle. More detailed guidance is provided in the respective sections. ## Integration principles Your connection to Voyado must never be critical to the purchase process in checkout, nor to any other business critical contact transaction. A contact should always be able to perform their entire purchase in the POS and e-com, regardless of problems like the client's broadband being down, or firewalls not working, or the Customer Engagement API not responding. Your systems should continue to operate even if communication to Voyado is down. A customer should therefore prepare for performance issues, or even the case where communication to Voyado, for whatever reason, is not available. There are several levels of preparation a customer can consider, according to their needs, budget, and ambitions. This can be seen as a kind of insurance. The more a customer invests, in terms of time, planning and resources, the fewer problems and data loss they risk in case of failure. Cases 2, 3 and 4 below require purchases to be imported to Customer Engagement either via XML or API with buffer or temp-DB for potential downtime. With an intelligent buffer for purchases WITH contact data, it is possible for the integration part to decide whether the contact registered should be pushed as a new contact or only tag the transaction to an existing contact when the connection is back up. This is the case where no fail-safe for contact-handling exists. However, the purchase process must always proceed. Here are a few suggestions for when a connection fail happens. **Notifying customers that the loyalty program is down using messages/alerts:** ”There were problems handling your member/customer data. You can proceed with your purchase but will not be able to use your individual member/customer offers. This purchase will also not generate reward points due to this issue. We regret the inconvenience. Contact Customer service for more info.” **Apply loyalty offers to all** As an apologetic gesture you could open up general customer/member offers to all. No new contacts can be registered during this downtime. In order to be able to tag transactions with a contact ID even though there is no communication with Customer Engagement, a secondary integration key should be implemented. Normally we recommend that transactions are tagged with a contact ID: not personal information, such as a mobile phone number, but rather a Customer Engagement contact ID or a generic member number. Then the POS or e-com must always take that public identifier (such as a mobile phone number or a social security number in Sweden) and check using the API which integration key should be used. Our recommendation is to use one or more of the public identifiers, such as a mobile phone number or email address as a secondary integration key, that is, to tag the mobilePhone to the transaction instead of the primary integration key. This is then matched as mobilePhone data in Customer Engagement. **Example using primary integration key, in this case “memberNumber”:** ```json theme={null} { "contact": { "matchKey": "28972987289", "matchKeyType": "memberNumber", "contactType": "member" }, "uniqueReceiptId": "2018468464468", "receiptNumber": "468464468" } ``` **Example using secondary integration key, in this case “mobilePhone”:** ```json theme={null} { "contact": { "matchKey": "46707234567", "matchKeyType": "mobilePhone", "contactType": "member" }, "uniqueReceiptId": "2018468464468", "receiptNumber": "468464468" } ``` Reward points will be accounted for and reward vouchers can be used when coded into the transaction. It is critical, as a GDPR compliance, that the integrator deletes all local logging and temporary storing of this integration data when it contains personal information. Customer Engagement flushes integration logs and files after a certain time and the integrator should too. No new contacts can be registered during this downtime. In this case, a reasonably fresh copy of Customer Engagement's contact database resides locally in the e-com platform and/or POS. This is obtained via a primary data download from Customer Engagement and incremental delta import via Customer Engagement's contact export XML. This export can be as complex as it needs to be, either: * A small database that just allows the integration key to be tagged to a transaction * A database containing fully-fledged contact objects No new contacts can be registered during this downtime. Use Engages [bulk read changes API](/docs/contacts/bulk-contacts-api) to fetch all contact changes from Customer Engagement in a single API call. In this case, a reasonably fresh copy of Customer Engagement's contact-DB resides locally in the e-com platform and/or POS. This is obtained via a primary data download from Customer Engagement and incremental delta import via Customer Engagement's contact export XML. In addition to this it is possible to create and change contacts locally and sync them incrementally to Customer Engagement. The data is obtained via delta export from Customer Engagement via XML import. In this case the integration key can't be the Customer Engagement internal contact ID. Instead it will be a number sequence created by the integrator's environment when it creates new contacts or some secondary integration key used specifically for this situation. To write all local contact changes to Customer Engagement in a single API call, use the [bulk write contacts API ](/docs/contacts/bulk-contacts-api)endpoint. # Customer Engagement Source: https://developer.voyado.com/docs/introduction/about Developer documentation for Voyado Customer Engagement, the customer data and loyalty platform for personalized, omnichannel retail communication. User guides for the Customer Engagement application Explore the entire agentic CX suite for retail Our AI docs assistant is always ready to help. Hit the "Ask Assistant" button above or write your question in the input area further down where it says "Ask a question". ## API version These docs cover the latest version of the Voyado Customer Engagement API, version 3. The docs for the legacy API v2 can be found here: If you are moving from v2 to v3 it is recommended to read the changes page: ## Initial setup This walks you through how to get started. A lot of your basic configuration can be done in the Config Hub. See here: Customer (contact) data and consents are central to working with Customer Engagement: Read these sections to learn about customer and store data: Here you can learn about working with your product data and feeds: When integrating your systems with Customer Engagement you'll be working with its API and/or FTP. Learn about those here: Making sure your emails get delivered is important before going live with Customer Engagement: For a more detailed guide on integrating your systems with Customer Engagement, see the [Integrations Best Practice guide](/docs/setup/best-practice-guide). ## Config hub As mentioned, your Customer Engagement environment has a self-service area, called *Config Hub*, where several parts of the initial setup can be done. All guides for Config Hub can be found in the Help Center: Only users belonging to the "Configurator" permission group can access the Config Hub. ## Other links User guides and instructional articles can be found at the Help Center: On the status page you can see if there are any current issues. You can also subscribe to be informed of important service updates which is recommended. Partner integration articles can be found at the dedicated portal: Voyado takes no responsibility for code created and shared by unauthorized third parties. Any code you download and use from public repositories such as GitHub is done entirely at your own risk, even if it has "Voyado" in the name. # Reference material Source: https://developer.voyado.com/docs/introduction/reference-material Here are some resources that might be useful when you are working with Customer Engagement. Download example CSV, XML and other assets useful in file migration. See the front-end and back-end packages used in Customer Engagement. # Staging environment Source: https://developer.voyado.com/docs/introduction/staging-environment As a Voyado customer, you might want to test functions and features before going live. You can then request access to your Staging environment. ## **About the staging environment** Staging is where new features in Customer Engagement are tested, bugs are ironed out and flows are perfected. It's primarily a testing environment for Voyado's development teams, but if you as a customer want to use it, then you can. However, keep a few things in mind: * **Stability**: Since Staging is where we test our next release and deploy our new code, it can go down without warning. So don’t count on it being up and running at any one time. * **Performance**: Staging, being a test environment, is not optimized for performance and could be a lot slower than your live environment. It is primarily a place for testing functionality and patterns, not volume and speed. Do not import more than 100,000 contacts. * **Data**: You can import your data into Staging. But beware that you cannot then transfer this data from Staging into your live environment. It will need to be imported again. * **Privacy**: When using Staging we recommend that you use dummy contacts and not import real contact data. Any real contact data in Staging could create GDPR issues, so when using Staging be sure to remove all such sensitive data when you are done. * **Features**: Features in Staging might not always match those in your live environment. * **Allowlist**: For security reasons, messages can only be sent to customers through the Staging environment if the email address (or phone number) beng used has been added to your allowlist. Your project team at Voyado must do this for you. See more about the allow list below. ## **Email and SMS allow lists** The staging environment has restrictions concerning how emails and SMS are sent out. To allow an email address or mobile phone number to receive communication from staging (whether service / order communication or otherwise) it must be added to the allowlist. The allowlist is only relevant for the staging (test) environment. ### **Finding the allowlist** The allowlist can be found on the side menu under **Administration**. Administration menu Allowlist menu From here you can open the **Email allowlist** or the **SMS allowlist**. ### **The email allowlist** Opening the email allowlist shows the list of email addresses that have been added. The email allowlist Rolling over an email brings up the **Delete** icon you can use to remove that address from the list. Select the **Add** button to add new email addresses. Configure email allowlist Now add the email addresses according to the instructions. ### **The SMS allowlist** Opening the SMS allowlist shows the list of mobile phone numbers that have been added. Show SMS allowlist Rolling over a mobile number brings up the **Delete** icon you can use to remove that number from the list. Select the **Add** button to add new mobile numbers. Configure SMS allowlist Now add the numbers according to the instructions. # Introduction to loyalty Source: https://developer.voyado.com/docs/loyalty/about-loyalty A loyal customer is a valuable customer. In the following sections you can learn how to work with Customer Engagement's various loyalty features such as: * [Points](/docs/loyalty/points) * [Point shop](/docs/loyalty/point-shop) * [Vouchers](/docs/loyalty/vouchers) * [Promotions](/docs/loyalty/promotions) * [Challenges](/docs/loyalty/challenges) * [Interactions](/docs/loyalty/interactions) All loyalty features are add-ons. Talk to your Voyado AM to learn more. # Challenges Source: https://developer.voyado.com/docs/loyalty/challenges A challenge defines a path towards a goal and motivates the customer to reach it. The path consists of a number of checkpoints and awards can either be given when the end goal has been reached and the challenge is completed, or at certain checkpoints along on the way. Challenges allows the retailer to drive engagement, rewarding desired actions and behaviors and making customers feel more involved. ## Use cases for challenges The more basic concept of a stamp card is included in challenges, but challenges can be used for so much more. The use cases are basically endless. A customer can also have more than one challenge running at the same time, increasing engagement. Here’s a few examples of what you can do with challenges: * Ask customers to submit five product reviews, each review counting as one checkpoint * Help customers gear up for their hike, setting checkpoints for different outdoor products * Encourage customers to shop in your physical store, then make a purchase online, then "like" your Instagram page, and finally subscribe to your newsletter, each action being a checkpoint. * Activate a sustainability-themed challenge focused on refreshing, repairing, and recycling products, or reward customers for bringing their own bag for in-store purchases. The following section will detail the API endpoints used in Challenges. For more help and guides, you can visit [this page ](https://help.engage.voyado.com/hc/en-gb/articles/17234962807580-Challenges-in-Customer-Engagement)in the Help Center. ## IDs used in challenges Three different IDs are used when working with the challenges API: * **Contact ID**: The usual Customer Engagement contact ID, used to uniquely identify a contact. * **Definition ID**: The *definition* of a challenge, its basic structure or template, has a unique definition ID. A challenge definition will have the same definition ID from when it's created to when it's destroyed. * **Challenge ID**: Assigning a challenge definition to a contact creates a specific "instance" of that definition, which then belongs to that contact alone. This instance is called a challenge, and is given a unique challenge ID. Each challenge ID is therefore *uniquely connected to one contact ID and definition ID*. ## The challenges API endpoints The challenges API consists of 6 endpoints. ```http Get all challenges assigned to a contact theme={null} GET /api/v3/challenges ``` ```http Get specific challenge assigned to a contact theme={null} GET /api/v3/challenges/{challengeId} ``` ```http Get all challenge definitions theme={null} GET /api/v3/challenges/definitions ``` ```http Get specific challenge definition theme={null} GET /api/v3/challenges/definitions/{definitionId} ``` ```http Award checkpoints in challenge (can be batched) theme={null} POST /api/v3/challenges/checkpoints ``` ```http Assign and activate challenge for contact theme={null} POST /api/v3/challenges/definitions/{definitionId}/assign ``` These endpoints will now be considered in more detail. ## Get all challenges for contact This endpoint returns all of a contact's challenges, even the completed ones. ```http theme={null} GET /api/v3/challenges ``` All of these parameters are sent in the query string. The contact's unique ID Restrict the results to only that challenge definition Can be used to paginate the results Can be used to paginate the results Narrow the results to "All", "Active", "Completed" or "NotCompleted" ## Get specific challenge for contact Use this endpoint to get the full details of an assigned challenge, including the contact's progress, using the challenge ID. A challenge is only ever connected to *one* contact, so the contact ID is not required in the request. ```http theme={null} GET /api/v3/challenges/{challengeId} ``` The response will look something like this: ```json theme={null} { "challengeId": "081225de-8a42-45f8-a3b7-03d9c97ee19c", "challengeStart": "2023-09-21T10:52:38.2535557+02:00", "completedCheckpoints": 2, "contactId": "2285d25b-622c-45ce-bc13-fccdd1147d4d", "id": "3a4a81c3-ea2c-4f4c-866b-6dcadf257759", "isActive": false, "isChallengeCompleted": false, "latestCheckpointAddedOn": "2023-09-26T10:52:38.2535557+02:00", "requiredCheckpoints": 5, "status": 3, "links": [ { "id": "3a4a81c3-ea2c-4f4c-866b-6dcadf257759", "href": "https://yoururl/api/v3/challenges/3a4a81c3-ea2c-4f4c-866b-6dcadf257759", "method": "GET", "rel": "self" }, { "id": "081225de-8a42-45f8-a3b7-03d9c97ee19c", "href": "https://yoururl/api/v3/challenges/definitions/081225de-8a42-45f8-a3b7-03d9c97ee19c", "method": "GET", "rel": "definition" } ] } ``` ## Get all challenge definitions This endpoint returns a list of all challenge definitions in your Customer Engagement environment. ```http theme={null} GET /api/v3/challenges/definitions ``` All of these parameters are sent in the query string. Can be used to paginate the results Can be used to paginate the results "All" (the default) as well as "Active", "Draft", "Scheduled" or "Ended" A successful request will return something like this: ```json theme={null} { "links": [ { "href": "", "method": "GET", "rel": "previous" }, { "href": "", "method": "GET", "rel": "next" } ], "count": 1, "offset": 0, "items": [ { "checkpointAssignOn": 2, "checkpointAssignOnAmount": 1, "createdBy": "Joe Doe", "createdOn": "2023-07-08T09:21:39.9997785+02:00", "description": "Buy 5 get one for free", "expirationMonths": 5, "id": "74bb66f1-6993-442e-bb93-e41e1ecc3184", "isContactConsentRequired": false, "isScheduled": false, "modifiedBy": "Jane Doe", "modifiedOn": "2023-07-18T09:21:39.9997785+02:00", "name": "Dog food", "requiredNumberOfCheckpoints": 5, "status": 1, "links": [] } ], "totalCount": 1 } ``` ## Get specific challenge definition This returns a specific challenge definition based on its unique definition ID. ```http theme={null} GET /api/v3/challenges/definitions/{definitionId} ``` The response is essentially a single object from the "items" array of the previous endpoint. ## Award checkpoints for contact Use this endpoint to progress a contact along a specific challenge. The `definitionId` of the challenge and the `contactId` must be sent in the payload, along with the number of checkpoints to be added. Updates to several contacts can be batched in the same request. ```http theme={null} POST /api/v3/challenges/checkpoints ``` The payload is sent in the body of the query as follows: ```json theme={null} [ { "definitionId": "bbf8d0a3-7057-4b21-9fd1-a9ba21116d5c", "contactId": "96465573-53e7-453f-a2d9-fa65a68de111", "checkpointAmount": 1 }, { "definitionId": "bbf8d0a3-7057-4b21-9fd1-a9ba21116d5c", "contactId": "63706fea-10d9-459e-a78a-1011486fb46f", "checkpointAmount": 2 }, { "definitionId": "2b84e222-2f84-4553-a775-7755ea912e52", "contactId": "be541846-34e2-4bb2-b693-b4c5a31c27b9", "checkpointAmount": 1 } ] ``` Incorrect data will still result in an accepted response code, but that block in the payload will be skipped. Customer Engagement will continue to process the next block. In the case of incorrect data, the *HTTP 200 OK* response will contain details of the error like this: ```json theme={null} { "missingContactIds": [ "a311f9f9-9bdc-4b85-9f6c-b269ab204038" ], "missingDefinitionIds": [ "4524db0c-4d43-417e-b437-d75ca2cda3a6" ], "notAccepted": [ { "definitionId": "4524db0c-4d43-417e-b437-d75ca2cda3a6", "contactId": "aace6d91-3f0f-472e-9583-b9e5c4acbaa1", "checkpointAmount": 2 }, { "definitionId": "0c469396-d94d-4f86-934d-244d6080c353", "contactId": "a311f9f9-9bdc-4b85-9f6c-b269ab204038", "checkpointAmount": 1 } ] } ``` ## Assign challenge to contact This creates an assignment of a specific challenge for a specific contact and activates it with an initial checkpoint count of zero. ```http theme={null} POST /api/v3/challenges/definitions/{definitionId}/assign ``` The contact's unique ID sent in the query string The challenge's defiition ID sent in the request path A *HTTP 200 OK* response with the value of "true" will be returned if the assignment was successfully created (or if it already exists). A challenge is configured so that the contact always has to give their approval to be assigned the challenge. This approval is handled by the retailer and it's up to their internal process to decide when a challenge can and should be activated for a contact *before* this endpoint is called. Calling the endpoint will always activate the challenge. # Interactions Source: https://developer.voyado.com/docs/loyalty/interactions Interactions functionality is available for Customer Engagement customers who are using the paid add-on component Incentives. An interaction in Customer Engagement is some happening or event that you want to save to a specific contact in Customer Engagement. For example, an interaction could be used whenever a contact: * Writes a review * Posts on Instagram * Refers a friend * Reuses or repairs something * Marks an item as a favourite Interactions provide an additional way to collect data that can be used to understand and engage the consumer. Interactions are intended for events that can't be collected in other ways. Things like purchases, returns, NPS and so on that already have their own pathways into Customer Engagement should not use Interactions. ## Interactions data structure A Customer Engagement interaction is a dynamic object, meaning that the data in an interaction does not need to be set by Voyado, but instead can be customized through a schema provided by the customer. An interaction is different from an *activity* in Customer Engagement. Activities have only one data type, and are imported through XML, whereas interactions can be fully customized and use the API. You can use the data collected through interactions in a number of ways: **Segmentation:** Create segmentations based on the number of times or the time when an interaction was received in Customer Engagement. **Contact card:** In the tab "Interactions" you can get an overview of which interactions a contact has received. **Automations:** Set up an automation flow using the triggers "New Interaction" (any interaction being received for the first time) or "New specific interaction" (an interaction of a specific schema, with a specific value, being received for the first time). Interactions are primarily meant for tracking of events connected to contacts and their interaction with the retailer. There should be a marketing or loyalty purpose with using interactions, not just data transfer and/or storage. While there are no exact limits on the amount or frequency of data, Voyado reserves the right to restrict data volume in cases of excessive use. ## Interactions API Interactions use two dedicated endpoints. * /interactionschemas * /interactions ### The interactionschemas endpoint This is used to define the interaction schemas your interactions can use: ```http Get all interaction schemas defined theme={null} GET api/v3/interactionschemas ``` ```http Add new interaction schema theme={null} POST api/v3/interactionschemas ``` ```http Delete a specific interaction schema theme={null} DELETE api/v3/interactionschemas/{interactionSchemaId} ``` ```http Get a specific interaction schema theme={null} GET api/v3/interactionschemas/{interactionSchemaId} ``` An UPDATE does not exist for this endpoint, meaning that once it's created, an interaction schema can't be altered. However, you can delete an interaction schema and then submit a new one with the same ID. The new schema must be backward compatible with the deleted one. Deletion of an interaction schema will lead to deletion of *all* interactions based on that schema for all contacts, even if you recreate it after using the same identifier. Any automation triggers set up for a schema that has been deleted will result in errors/deadletters and automation flows that do not progress. But as soon as a schema with the same ID is put back in, the automation will work as before, provided the changes made are backward compatible (meaning that no properties present in the schema are removed in the updated version). ### The interactions endpoint This endpoint concerns the individual interactions (interaction events) sent to Customer Engagement. Such an interaction is always linked to a specific contact and a specific interaction schema. ```http Get interactions for specific schema and contact theme={null} GET api/v3/interactions ``` ```http Get single interaction by ID theme={null} GET api/v3/interactions/{interactionId} ``` ```http Create interaction for specific contact and schema theme={null} POST api/v3/interactions ``` ```http Deletes specific interaction theme={null} DELETE api/v3/interactions/{interactionId} ``` ## Creating a schema The interaction, its data and identifier, are defined in a *JSON schema*. Read more about the formatting standards [at this page](https://json-schema.org/understanding-json-schema). Once the schema is created, it must then be posted to the API, after which interactions that follow that schema can be used. Here is an example of a schema: ```json theme={null} { "id": "Reuse-Spring-2023", "displayName": "Reuse Spring Sale", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "price": { "type": "integer", "displayName": "Price", "showInContactCard": "true", "sortOrder": "1" }, "purchase-timestamp": { "type": "string", "format": "date-time", "displayName": "Purchase timestamp", "showInContactCard": "true", "sortOrder": "2" }, "name": { "type": "string", "displayName": "Name", "showInContactCard": "true", "sortOrder": "0" } }, "required": [ "name" ] } } ``` The identifier for this schema, **must** be unique. Can only contain the characters a-z, A-Z, 0-9, underscore (\_) and dash (-). For example: "Autumn-campaign\_2023" is allowed, but "Höst-kampanj\_2023" is not. The name that will be displayed in the Customer Engagement UI for this kind of interaction. The body of the actual schema. This must have the value shown. This must be "object" as shown. Containing the properies as objects. Each property object defined has the following fields: This is either "string", "number", "integer" or "boolean". This is optional and defines which format the attribute must have. For example, the "purchase-timestamp" attribute is of type "string" with a format of "date-time", so only strings in the format "2018-11-13T20:20:39+00:00" will be allowed. [Read more here](https://json-schema.org/understanding-json-schema/reference/string.html#format) about the various formats. What is shown in the Customer Engagement UI for this property. This is "true" if this property is to be displayed on the contact card. This is "true" if this property can be used in segmentation in Customer Engagement (see the section on segmentation below). An integer which determines in which column on the contact card this property will be displayed. The leftmost column has sortOrder value of 0, the next to the right has 1, and so on. Deeply nested JSON structures are not currently supported. Don't use more levels than shown in the example. The schema JSON you build is then sent to this endpoint as the body of the request: ```http theme={null} POST api/v3/interactionschemas ``` If successful, this new kind of interaction as defined by this schema is ready to use. ## Sending an interaction Once the schema has been accepted by Customer Engagement, you can start sending that kind of interaction. Every interaction must be linked to a specific Customer Engagement contact. This interaction uses the schema defined above: ```json theme={null} { "contactId":"cf6d21fa-6e2b-481c-ab38-afa1012233ff", "schemaId": "Reuse-Spring-2023", "createdDate": "2023-05-23711:13:42.074Z", "payload":{ "price": 100, "type": "Jeans", "name": "Reused Item" } } ``` The `createdDate` field, if not specified in the payload, will be automatically set to the current timestamp. This is sent as the body of a request to the following endpoint: ```http theme={null} POST api/v3/interactionschemas ``` If a property in the payload does not match the schema, that property will not be sent to Customer Engagement, but will still be saved in blob storage. If the interaction does not follow the rules of the schema, it will be ignored. Upon acceptance, a unique ID for this interaction will be returned. This can then be used to retrieve that specific interaction again, if that is ever needed, through the endpoint: ```http theme={null} GET api/v3/interactions/{interactionId} ``` ## The productSku property Many customer interactions are events where a product might be involved, for example, a product review or repair. Product metadata - such as category, brand, dimensions - might be useful in these cases for personalisation in email communication or other purposes. Therefore you should always add all this data to the schema. It is good practice to also indicate which product is the subject of the interaction with the reserved property name `productSku` since Customer Engagement might later on develop functionality around this, automatically enriching the interaction product metadata. The `productSku` property is a string value. ## Use cases of interactions Here are some use case examples of interactions, with example schemas. If your customers are identified online, either via [Voyado Soft Identification](/docs/contacts/identify-contact/soft-identification) or a login, you can track their behaviour. This is especially useful if they are showing an interest in a product on your e-com site. The schema used will be something like this: ```json theme={null} { "id": "wishlist", "displayName":"Wishlist", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "date": { "type": "string", "format":"date-time", "displayName": "Date", "showInContactCard": "true" }, "productSku": { "type": "string", "displayName": "SKU", "showInContactCard": "true" } //Product name, product category, brand, price, eco label } } } ``` If your business deals with wholesale purchases via resellers you can include post-purchase registration of that product to see what your customers have bought regardless of channel. A typical scenario could be a QR code that leads to landing page for this registration and identification for the customer, with the purchase/product data tracked to Customer Engagement as an interaction like this: ```json theme={null} { "id": "wholeSalePurchase", "displayName":"Whole sale purchases", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "date": { "type": "string", "format":"date-time", "displayName": "Date of purchase", "showInContactCard": "true" }, "productSku": { "type": "string", "displayName": "SKU", "showInContactCard": "true" }, //Product name, product category, brand, price, eco label, size "pricePaid": { "type": "number", "displayName": "Price paid", "showInContactCard": "true" }, "source": { "type": "string", "displayName": "Source of purchase", "showInContactCard": "true" } } } } ``` Interactions could be used to reward customers for making sustainable choices, such as repairing items. If you are tracking repairs of garments or other products in some system, you might want to give the customer some benefits from their actions. For this, you'll need to send those events to Customer Engagement. An interaction to use might look like this: ```json theme={null} { "id": "repair", "displayName":"Repair", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "registrationDate": { "type": "string", "format":"date-time", "displayName": "Registration date", "showInContactCard": "true" }, "productModel": { "type": "string", "displayName": "Product model", "showInContactCard": "true" }, "brand": { "type": "string", "displayName": "Brand", "showInContactCard": "true" }, "productId": { "type": "string", "displayName": "Product ID", "showInContactCard": "true" }, "storeName": { "type": "string", "displayName": "Store name", "showInContactCard": "true" }, "storeId": { "type": "string", "displayName": "Store ID", "showInContactCard": "true" }, "isFreeRepair": { "type": "boolean", "displayName": "Free repair", "showInContactCard": "true" } } } } ``` If your business involves second hand products, you could use a schema like this to track them: ```json theme={null} { "id": "preownedProduct", "displayName":"Pre-owned", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "date": { "type": "string", "format":"date-time", "displayName": "Date", "showInContactCard": "true" }, "productSku": { "type": "string", "displayName": "Product SKU", "showInContactCard": "true" }, //Product name, product category, brand, price, eco label "size": { "type": "string", "displayName": "Size", "showInContactCard": "true" }, "condition": { "type": "string", "displayName": "Condition", "showInContactCard": "true" }, "storeDropOff": { "type": "boolean", "displayName": "Store drop off", "showInContactCard": "true" }, "reimbursement": { "type": "number", "displayName": "Reimbursement", "showInContactCard": "true" }, "storeName": { "type": "string", "displayName": "Store name", "showInContactCard": "true" }, "storeId": { "type": "string", "displayName": "Store ID", "showInContactCard": "true" } } } } ``` If you have a system in place to recycle products, you could track the customer's recycling activity with this schema: ```json theme={null} { "id": "recycled", "displayName":"Recycled", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "date": { "type": "string", "format":"date-time", "displayName": "Date", "showInContactCard": "true" }, "dropOffPoint": { "type": "string", "displayName": "Drop off point", "showInContactCard": "true" }, "quantity": { "type": "number", "displayName": "Quantity", "showInContactCard": "true" }, "type": { "type": "number", "displayName": "Type of recycling", "showInContactCard": "true" }//Batteries, electronic waste, aerosol etc } } } ``` You could use interactions to reward behaviors such as: * **Reviews or surveys**: Track the number of product reviews your customers have made. * **Referrals**: Track if you customers are making your database grow through referral recruitment. ## As a placeholder for storing objects The interactions entity can be seen as a log of events connected to a contact. Interactions stored for a contact can't be changed once saved, although they can be deleted. The Customer Engagement interactions schema also allows a retailer to collect data on anything else that could be encoded as an interaction, for example the cars owned by a specific contact. ### Example: Subscriptions A retailer's business might involve subscription-based services. The events involved when such a subscription starts, changes or ends can easily be expressed as interactions. It's possible to save the active subscription to a contact as a specific interaction type and then delete it when it is no longer valid. This allows the Customer Engagement user to segment customer with a specific number of subscriptions active. All the detailed information could be put into the interaction schema as properties, either for display in the Customer Engagement contact card or for use in automations. For targeting, however, this is somewhat limited in that Customer Engagement can’t tell how many active subscriptions a contact has at any one time. ### Example: Custom attributes It's possible to encode objects and their properties as multiple custom attributes. This allows an easier way to deal with having different properties for each instance of an object. The contact model is given a set of fields, some of which might be empty. For example: 1. Car1-type, Car1-brand, Car1-year 2. Car2-type, Car2-brand, Car2-year 3. Car3-type, Car3-brand, Car3-year 4. Car3-type, Car3-brand, Car3-year A limitation here is that it becomes cumbersome to segment based on how many of an object a contact has. ## Personalizing messages You can use individual data points from an incoming interaction to personalize your send-outs. To do this you'll need the ID of the specific interaction schema. ID of interaction schema In this case, this is the string "Reuse-Spring-2023". You'll also need the name of the property in the interaction that you'd like to insert into your send-out: Name of the interaction property In this example, we want to insert the "price" value. Once you know the interaction schema ID and the property name, combine them like this in the email editor (this shows the classic email editor): Usage in the classic email editor You'll need to lower-case the interaction schema ID. This was "Reuse-Spring-2023" in the interaction, but in the editor you'll write "reuse-spring-2023" as shown above. ## Interaction segmentation The interaction Segmentation functionality described here is not yet on general release. Contact Voyado if you are interested in using it. The property `addToSegmentation` can be used in interaction schemas. Adding this property to one of your schema's properties with a value of "true” makes the property show up in the filtering tool. You'll then be able to segment for contacts with a certain value of that property. If a property is not to be segmentable, you can either include `addToSegmentation` with a value of "false" or just leave it out entirely for that property. The types allowed for segmentable properties are: * Boolean * String (date is stored as a string) * Integer * Number These are also the only types allowed in the schema definitions. This is an example of using `addToSegmentation` in a schema: Example of using addToSegmentation ### Limitations Some limitations are imposed on the use of interaction schemas with segmentable properties: 1. A Customer Engagement environment can have a maximum of 15 interaction schemas in total 2. A schema can contain a maximum of 6 segmentable propties. Validation exists to enforce this. For example, you can't have an interaction with 6 segmentable properties of type "Number", then 6 more of type "String". In the example image above, every property has `addToSegmentation` but it is only "true" for 6 of them so only these will be segmentable. ### Upgrading There is no direct way to update an existing interaction schema to use the new `addToSegmentation` functionality while still keeping the same schema name / ID. And when an interaction schema is deleted, all interactions associated with it are also removed, as well as all data displayed in the contact card or available for segmentation. This happens automatically. So if you want to leverage `addToSegmentation` and retain old interactions for a certain interaction schema, there are two ways: You'll create a new schema that is identical to the existing one, the only difference being that some properties will now have the `addToSegmentation` property with a value of "true". It will need a different name, and it's good if this name is connected to the original schema's name. For example, if the original schema is called "Repairs", create a new scheme with a name such as "Repairs-segmentable" with exactly the same structure and properties, the only difference being that some of the properties will now have the property `addToSegmentationset` to "true". Now segmentation will be possible for the new schema ("Repairs-segmentable"). The segmentation UI in Customer Engagement will display all the properties you've marked as filtering options / fields for that schema. Segmentation for the old schema ("Repairs") will just allow you to access the number of interactions received for a specified date range (which is the original behavior). If the source system retains historical interactions OR if you are able to fetch all the interactions from Customer Engagement prior to deletion of that schema using the relevant API endpoints, you have the option to delete all interactions and re-import them into an upgraded schema. Follow these steps: Confirmed that the interactions have been saved somewhere. Delete the old schema which will delete all of those interactions from Customer Engagement. Recreate the schema you deleted using `addToSegmentation` (as "true") added to the properties you want to be able to segment on. Save that schema with the same name as the one you deleted in step 2 Import the old interactions into the new schema, giving you a single schema that "owns" all the historical interactions as well as the new, all of which can be segmented on. You will need to pause all automations you might have that are triggered by new interactions, so that customer who have already passed through an interaction automation won't re-enter it. # Point shop Source: https://developer.voyado.com/docs/loyalty/point-shop The Customer Engagement Point Shop allows you to embed point shop offers directly into your web shop, native apps, or POS systems. You can fetch available point shop offers for a member, allow them to purchase an offer using their points, and track purchases. The API ensures your customer experience stays in sync with the Customer Engagement backend, handling redemptions and point deductions seamlessly. ## How it works When you create a point shop offer in Customer Engagement, two entities are generated: This represents the shop-facing item, and includes: * Point cost * Availability window * Presentation data This is the actual reward the member receives after purchase (discount, ticket, pre-sale access). This includes: * Presentation data Presentation data (including title, description, image, link) is stored both for point shop item and for multichannel promotions. From an integration perspective you will: 1. Fetch and display available point shop items 2. Let the member purchase a point shop offer 3. Retrieve and redeem the reward via Multichannel APIs The relevant API endpoints are presented here. ## 1. Get point shop items This endpoint fetches all point shop items that a specific member can purchase. ```http theme={null} GET /api/v3/point-shop-items?contactId=[contactId] ``` Unique identifier of the contact. ```json theme={null} [ { "id": "GUID", "name": "string", "cost": 0, "pointDefinitionId": 0, "promotionId": "GUID", "validFrom": "2026-03-20T13:53:08.003Z", "validTo": "2026-03-20T13:53:08.003Z", "presentation": { "title": "string", "description": "string", "imageUrl": "string", "link": "string" } } ] ``` The points required for this item. The ID of the reward. Items are filtered based on: * Member eligibility * Point cost * Active time window (`validFrom`, `validTo`) * *202: Accepted* if there are no errors. * *400 Bad Request* is received if `contactId` is empty or not a valid GUID. ## 2. Purchase point shop item Use this endpoint to purchase a point shop item for a specific contact and assign the promotion purchased to them. ```http theme={null} POST /api/v3/point-shop-items/purchase ``` ```json Purchase item payload structure theme={null} { "pointShopItemPurchaseId": "00000000-0000-0000-0000-000000000000", "pointShopItemId": "00000000-0000-0000-0000-000000000000", "contactId": "00000000-0000-0000-0000-000000000000" } ``` Standard identifier for the contact. This, the unique ID for this purchase, is a value that you have to generate in your system for each purchase attempt. This endpoint is therefore *idempotent*, and this is enforced based on the combination of `pointShopItemPurchaseId` and `contactId`. See below for more. This is the same value as the `id` returned in the *Get point shop items* endpoint above. ### Idempotency Idempotency here is based on the combination of `pointShopItemPurchaseId` and `contactId`. In practice, if a request is retried using the same values of these two attributes, the API will just return the existing purchase instead of creating a new one. This ensures that: * A purchase ID cannot be reused across different contacts * No duplicate point deductions will occur * Retries are safe in case of network issues ### Handling the request When a POST has been made to this endpoint, the systems first validates that `pointShopItemPurchaseId` and `contactId` belong together, preventing duplicate purchases and cross-user reuse of purchase IDs. If validated, then: * The purchase operation identified by `pointShopItemPurchaseId` is created or, if it already exists, reused * Points are deducted from the contact * The linked promotion is assigned to the contact In the case of a request to the endpoint being repeated: | Scenario | Result | | :- | :- | | Same `purchaseId` + same `contactId` | ✅ Returns existing purchase (idempotent) | | Same `purchaseId` + new `contactId` | ❌ Rejected (invalid request) | | New `purchaseId` + same `contactId` | ✅ New purchase created | ```json theme={null} { "id": "00000000-0000-0000-0000-000000000000", "pointShopItemId": "00000000-0000-0000-0000-000000000000", "pointAccountId": "00000000-0000-0000-0000-000000000000", "pointTransactionId": "00000000-0000-0000-0000-000000000000", "promotionAssignmentId": "00000000-0000-0000-0000-000000000000", "pointShopItemCost": 0, "status": "Processing", "purchasedOn": "2026-04-14T13:23:28.458Z" } ``` See response errors: The generic PromotionAssignmentError (HTTP 400) is split into specific error codes: * PromotionAssignmentContactNotFound (400) — the contact doesn't exist * PromotionAssignmentPromotionNotFound (400) — the linked promotion was removed * PromotionAssignmentPromotionExpired (400) — the linked promotion has ended * PromotionAssignmentError (400 → 500) — internal server error, safe to retry This can have one of the following error codes: * Item does not exist This can have one of the following error codes: * PointShopItemPurchaseProcessingTimedOut * PointShopItemPurchaseFailed ```json theme={null} { "message": "string", "validationErrors": [ { "validationErrorCode": "string", "field": "string", "errorDescription": "string" } ] } ``` * Unknown error ### Integration guidance Clients should: 1. Generate a new `pointShopItemPurchaseId` for each new purchase 2. Reuse the same ID only when retrying the same request 3. Never reuse a purchase ID across different users (different `contactId` values) ## 3. Get point shop item purchases Use this endpoint to fetch all purchases made by a specific contact. This data from this endpoint is useful for: * Seeing the full purchase history * Order tracking * UI confirmation flows * Understanding the purchase state with `status` (for example "Processing") ```http theme={null} GET /api/v3/point-shop-item-purchases?contactId=[contactId] ``` Standard identifier for the contact. ```json theme={null} [ { "pointShopItemPurchaseId": "GUID", "pointShopItemId": "GUID", "promotionAssignmentId": "GUID", "pointShopItemCost": 0, "status": "Processing", "purchasedOn": "2026-03-20T13:41:01.840Z" } ] ``` * *400 Bad Request* - InvalidContact * *404 Not Found* - No purchases found or invalid reference ## Purchase flow A point shop item purchase flow looks like this: 1. Call `GET /point-shop-items` 2. Display available offers 3. User selects an item 4. Call `POST /point-shop-items/purchase` (with `pointShopItemPurchaseId`) 5. Retry safely if needed using the same purchase ID 6. Optionally track via `GET /point-shop-item-purchases` 7. Fetch reward via the multichannel promotion API ## Multichannel promotion While the point shop API handle this: * Listing items * Purchasing items The actual reward purchased is handled by the multichannel promotion endpoints. You must integrate with those endpoints in order to: * Display reward details (title, description, barcode, expiry) * Allow redemption (in-store or online) See the multichannel promotion API documentation for how to retrieve, present, and validate assigned offers. # Points Source: https://developer.voyado.com/docs/loyalty/points In Customer Engagement, contacts can earn points when they make purchases, or be granted them for events such as when it's their birthday or when they sign up. Those points can then be converted to vouchers. * A **points model** must be configured in your Customer Engagement installation, in the **Rewards** module, for points to be available. Contact your Voyado AM about this. * On the **contact card** under the tab "Reward points" in the Customer Engagement UI, you can see all points gained by a contact. * All **points changes** done over the API use the `v3/point-accounts` endpoints, which are discussed below. Several changes were made in the various points endpoints when moving to the v3 API. [See the full list here](/docs/api/api-changes-v3). An important change is that in v3 a contact's total points are no longer returned from **/contactoverview**. This can now only be done through a separate GET request to the already existing but slightly changed **/point-accounts** endpoint. See details below. ## Terms and parameters Here are some important parameters and terms when working with points: **Point definitions:** The "type" of points. Currently one point definition exists but the system allows for different kinds of points to be tracked in parallel, each with their own point definition value. The parameter used to tell point definitions apart is `definitionId`. **Point accounts:** Points of different types are separated by their point definition value. This allows contacts to have many independent point totals and histories (accounts) each uniquely defined by a specific contact ID and a specific definition ID. The parameter for a point account is `accountId`. **Point transactions**: A transaction or an update to a transaction that specifies the changes to a contact's points. **Active points:** The points that a contact currently owns which can be used to create vouchers. **Pending points:** Points with a `validFrom` date in the future, which means they will not become active points until then. **Pagination in responses:** Done with the optional `offset` and `count` parameters. **Filter active or pending:** The `filter` parameter which can be "All", "Active", "Pending" (or blank) returns active points, pending points, or both. ## Get all point accounts To get all of a customer's point accounts, use this endpoint: ```http theme={null} GET api/v3/point-accounts ``` Send the customer's `contactId` as a query string parameter. This returns all of their point accounts in this form: ```json theme={null} { "links": [], "count": 2, "offset": 0, "items": [ { "balance": 110, "balanceExpires": "2023-08-14T13:03:52.0510384+02:00", "contactId": "efaaefb8-e07a-46d5-89fa-1423e87b0798123111", "definitionId": 1, "id": 9, "pendingPoints": 0, "pointsWillExpireDueToInactivity": "2024-06-02T10:53:04.5084821+02:00", "links": [ { "id": 1, "href": "https://yoururl/api/v3/point-accounts/definitions/1", "method": "GET", "rel": "definition" }, { "href": "https://yoururl/api/v3/point-accounts/transactions?accountId=9", "method": "GET", "rel": "transactions" } ] }, { "balance": 110, "balanceExpires": "2023-08-14T13:03:52.0510384+02:00", "contactId": "efaaefb8-e07a-46d5-89fa-1423e87b0798123111", "definitionId": 2, "id": 9, "pendingPoints": 0, "pointsWillExpireDueToInactivity": "2024-06-02T10:53:04.5084821+02:00", "links": [ { "id": 1, "href": "https://yoururl/api/v3/point-accounts/definitions/1", "method": "GET", "rel": "definition" }, { "href": "https://yoururl/api/v3/point-accounts/transactions?accountId=9", "method": "GET", "rel": "transactions" } ] } ], "totalCount": 2 } ``` The `pointsWillExpireDueToInactivity` value indicates the point in time when transactions will expire due to the expiry method "Contact inactive", and is only applicable for this expiry method. ## Get specific point account If you know the unique ID of the point account, you can get the data for that account directly by using its ID in the path as shown here: ```http theme={null} GET api/v3/point-accounts/fa1de1de-427d-4a63-8f43-513fc8b5e97b ``` This returns the following structure: ```json theme={null} { "balance": 110, "balanceExpires": "2023-08-14T13:03:51.9719335+02:00", "contactId": "af687249-b52a-4926-8591-ccf881b59e9e", "definitionId": 1, "id": 1234567899, "pendingPoints": 0, "pointsWillExpireDueToInactivity": "2024-06-02T10:53:04.5084821+02:00", "links": [ { "id": 1, "href": "https://yoururl/api/v3/point-accounts/definitions/1", "method": "GET", "rel": "definition" }, { "href": "https://yoururl/api/v3/point-accounts/transactions?accountId=9", "method": "GET", "rel": "transactions" } ] } ``` The `links` array contains optional information with some useful API calls you can use. The `balanceExpires` field indicates a point in time when the balance may change due to activation or expiration of points. However, because it depends on multiple factors, its behavior can be unpredictable. Therefore, we don't recommend relying on this field. ## Get transactions for point account A point account ID is unique for every point account in the database. You get it using the contact ID and the `/api/v3/point-accounts` endpoint. Once you have the point account ID, you can then get all the detailed transactions for that particular point account, showing every time that particular contact's points have been generated, removed or adjusted: ```http theme={null} GET /api/v3/point-transactions ``` This request returns a response of this form: ```json theme={null} { "Items": [ { "id": "861e728a-7037-4dc9-9907-5b38691a9c8e", "accountId": "33aac83b-a50c-1234-b57f-5b94b9f1a1a2", "description": "Test", "source": "Automation", "amount": 100, "type": "Addition", "transactionDate": "2024-09-20T11:51:29.408622+00:00", "createdOn": "2024-09-20T11:51:29.408622+00:00", "modifiedOn": "2024-09-20T11:51:29.408622+00:00", "validFrom": "2024-09-15T11:51:29.408622+00:00", "validTo": "2024-09-25T11:51:29.408622+00:00", "retailTransactionLineItemId": "5a3f4a1f-5f30-42c6-a333-dcaa68b3f09f" } ], "totalCount": 1, "offset": 0, "count": 1, "links": [ ... ] } ``` Every transaction (points change) for this account is an entry in the `items` array. The value of `type` can be "Addition", "Deduction", "Expiry" or "Cancellation". There are some optional parameters you can use to filter which results you get back: * The parameters `offset` and `count` can be used to paginate the results * The parameter `filter` can be used to fetch active points, pending points or both * The parameter `sortBy` lets you sort the results on transaction date or created-on date * With `sortOrder` you can order results in ascending or descending order For example. this request returns all **pending points** transactions for this point account ID: ```http theme={null} GET api/v3/point-transactions?id=55579bfd-718a-496e-bd80-50e5ec5b0d2b145&filter=pending ``` ### Point transactions descriptions In the payload returned from /point-transactions you'll see a "description" field. This functionality has changed somewhat in API v3. ```json theme={null} { "items": [ { "id": "be5abf92-98ac-4192-b0c3-af7da42fbb5a", "accountId": "1096a160-af33-4f4f-b78a-5ddc284451af", "description": "@@(Bonus.Points) - Slim Fit T-shirt", "source": "Purchase", "amount": 719.7, "type": "Addition", ... }, { "id": "12548f57-fbdf-1234-8b14-de9e30ce5e3a", "accountId": "2222a160-af60-4f4f-b78a-5ddc284451af", "description": "@@(ConvertedToBonusCheck)", "source": "RewardVoucher", "amount": -15000, "type": "Deduction", ... }, { "id": "f5d27525-918c-4fb2-a71e-c90f5efbff09", "accountId": "7777a160-af60-4f4f-b78a-5ddc284451af", "description": "Plus member - extra points for purchase (45 points) - Regular Fit Sweatshirt", "source": "BonusPromotion", "amount": 449.85, "type": "Addition", ... } ], "totalCount": 2, "offset": 0, "count": 2, "links": [] } ``` In the above example, you'll see that some `description` values contain a code, such as @@(ConvertedToBonusCheck). These codes are *placeholders for a certain type of description* and for each one you can configure a custom text to explain the transaction. for example, on a contact's My Pages. Here are explanations along with suggested descriptions: | Code | Usage | Example description | | - | - | - | | @@(Bonus.Points) | When points are awarded for a purchase. | "Points awarded from purchase." | | @@(RemovedDueToTimeLimit) | When points have passed their expiry date and can no longer be used. | "Deduction for expired points." | | @@(Bonus.DeductionForDiscount) | When points are removed because of the price being discounted. | "Deduction for discount." | | @@(Bonus.DeductionForReturn) | When points are removed because of a return being made. | "Deduction for returned item." | | @@(ConvertedToBonusCheck) | When points are removed because they have been converted to a voucher. | "Points converted to voucher." | You will need to handle this manually in your frontend, using either the texts suggested in the table, or your own customized version of them, whichever suit your needs. ## Add single point transaction This is the recommended way to add a single points transaction to a points account. ```http theme={null} POST /api/v3/point-transactions ``` To add multiple points transactions at once, to one or many point accounts, can use the bulk endpoint `/api/v3/point-accounts/transactions` which is covered in the following section. ```json theme={null} { "accountId": "00000000-0000-0000-0000-000000000000", "transactionId": "00000000-0000-0000-0000-000000000000", "transactionType": "Addition", "amount": 0, "description": "string", "source": "Automation", "transactionDate": "2024-11-13T08:16:27.949Z", "validFrom": "2024-11-13T08:16:27.949Z", "validTo": "2024-11-13T08:16:27.949Z" } ``` The account ID for the account you'll be adding the transaction to. If this transaction is coming from, for example, your e-com, then there is already a unique ID that you can use. Or you can create one and store it your own database as a reference. * *400: Bad Request* * *504: GatewayTimeout* If the settings for expiration are correctly set up AND the value of `validTo` or `expireAfterMonthsInactive` matches that expiration setting, these fields can both be set to blank and values will be added based on the expiration settings. ## Add several point transactions To batch-add point transactions to Customer Engagement, or adjust existing points, use this endpoint. ```http theme={null} POST api/v3/point-accounts/transactions ``` The transaction data is sent in the request body: ```json Adding several transactions at once theme={null} [ { "contactId": "111111fa6-103b-459b-8d01-b6c000e8e26a", "amount": 12, "definitionId": 1, "timeStamp": "2023-06-01T08:40:51.658Z", "source": "Purchase", "description": "Gilded gauntlets", "validFrom": "2023-06-16T08:40:51.658Z", "validTo": "2024-06-16T08:40:51.658Z" }, ... ] ``` Many transactions can be sent in this array, up to a maximum of 1000. The points amount (12 in the example above) will be added to the point account defined by this contact ID and definition ID. If no point account exists for that contact ID and definition ID, then one will be created and used. Common values for `source` are "Purchase", "Adjustment" and "Return". Other values are also available. Since the points in this example have a `validFrom` date that's in the future from the purchase date, these points will be pending until then. When that date arrives, they will change to active. * 202 Accepted response * 400 Too many items, New point system not active ## Get a specific transaction You can fetch the information for a specific point transaction if your know its unique transaction ID: ```http theme={null} GET api/v3/point-transactions/{transactionId} ``` This returns the transaction with the given transaction ID (assuming it exists) in the following format: ```json Transaction data example theme={null} { "accountId": 1, "amount": 800, "createdOn": "2022-11-16T13:17:54+00:00", "description": "Some text", "id": 1, "modifiedOn": "2022-12-16T00:05:21+00:00", "source": "Adjustment", "transactionDate": "2022-11-16T14:17:54+01:00", "type": "Adjustment", "validFrom": "2016-11-16T14:17:54+01:00", "validTo": "2017-11-16T23:59:59+01:00", "retailTransactionLineItemId": "d5ad8be8-6785-4922-95fe-b12500d3ff91" "links": [ ... ] } ``` The value of `retailTransactionLineItemId` allows you to link this particular point transaction back to the exact line item that caused it. ## Pending points Pending points are a Customer Engagement feature powered by the latest version of points. They can be seen as points that are granted but that only become "real" after a configurable delay. * **Active:** Points with no `validFrom` date or one that is in the past (regular points, basically) * **Pending:** Points with a `validFrom` date in the future, after which they become active Pending points can fetched via the points API. How this is done depends on whether you want the total number of points (the balance) or the detailed transactions showing each change for that point account. ### Fetch pending point balance To get the point account balances for a customer, you use the following endpoint, sending in the query string the `contactId` of the contact you are interested in. ```http theme={null} GET api/v3/point-accounts ``` In the response you'll get the point account information and balance, with `pendingPoints` as one of the values. ### Fetch pending point transactions To get the pending point transactions for a customer, make a request with the point account ID (as `id`) in the query string to this endpoint. ```http theme={null} GET api/v3/point-transactions ``` To get only pending points transactions, use the optional parameter `filter` in the query string with the value of "Pending". ### Create pending points Points can also be created as pending for one or more contacts when added through this endpoint: ```http theme={null} POST /api/v3/point-accounts/transactions ``` This is done by passing the `validFrom` parameter along with the other information required in the body of the request. Set `validFrom` it to the date in the future from which these points will change from pending to active, and now you have created pending points. Example: ```json Adding pending points example theme={null} [ { "contactId": "00000000-0000-0000-0000-000000000000", "amount": 0, "definitionId": 0, "timeStamp": "2024-09-26T08:01:19.710Z", "source": "string", "description": "string", "validFrom": "2025-09-26T08:01:19.710Z", "validTo": "2025-11-26T08:01:19.710Z" } ] ``` ### Add points manually The `validFrom` parameter is not used for points which are added manually or via automations. Points created in one of these ways will always be active immediately. When you activate pending points, a contact's already existing points will not be affected. Only points created *after* the activation will be able to use the pending points function. ## Webhooks Customer Engagement provides webhooks for working with points and vouchers. ## Point follower Sometimes a customer wants to be the *point master* (that is, to own and calculate their end-user's points in their own system). But they might still want to use Customer Engagement's functionality for visualizing and adding points based on non-purchase events like engagements, birthday, reviews or whatever. In this case, Customer Engagement can be configured to be the *point follower*. Customer Engagement and the point master system must then sync points with each other, with the point master being the single source of truth. Point follower, on a functional level, means that Customer Engagement stops generating points from purchases. This means also means that point based member levels, extra point on purchase and generation of reward vouchers doesn't work. Customer Engagement receives the current point balances for a contact from the point master system and sets the total in Customer Engagement to this value. Not that there will be no information sent about why the balance changed. The current solution for Customer Engagement as point-follower uses a combination of API endpoints and webhooks. There are three parts to the solution: * The point master sends balance-only updates via the Customer Engagement API * Customer Engagement imports the points log from an external API provided by the point master * Customer Engagement exports point adjustments to the point master via a webhook Expand the sections below to see each stage in detail. The point master needs to regularly send points updates to Customer Engagement so that both systems remain in sync. This is done using this Customer Engagement API endpoint: ```http theme={null} POST /api/v3/point-accounts/balances ``` The payload has the following form: ```json Payload to sync points accounts theme={null} [ { "contactId": "00000000-0000-0000-0000-000000000001", "amount": 122, "definitionId": 1, "timeStamp": "2023-05-16T12:20:03.807Z" }, { "contactId": "00000000-0000-0000-0000-000000000002", "amount": 63, "definitionId": 1, "timeStamp": "2023-05-16T12:20:03.807Z" } ] ``` When the point master calls this endpoint, Customer Engagement receives the request and puts it into a queue. A unique transaction ID is also returned. When the job "BalanceAccountToAmountJob" is run, the queue is processed and the received point balance is then set in Customer Engagement for each of the contacts, putting Customer Engagement in sync with the point master ### Track processing of request If the point master needs to track how the requests are being processed in Customer Engagement, it can use the following endpoint along with the transaction ID it received when it posted to /api/v3/point-accounts/balances: ```hjttp theme={null} GET /api/v3/point-accounts/balances/status/{id} ``` Be aware that this endpoint is due to be deprecated. Voyado recommend not building any new workflows using this endpoint. The response from this call will be in this form: ```json theme={null} { "messages": [ { "message": "string", "timeStamp": "2023-05-17T11:03:16.947Z" } ], "id": "string", "status": "string", "timeStamp": "2023-05-17T11:03:16.947Z" } ``` When Customer Engagement is acting as point follower, it does not hold the points log (points history) for a contact. The point master owns all that information. Customer Engagement, however, can fetch the points log using an external API supplied by the point master, and then display it on the contact card. The API call will look something like this: ```http theme={null} GET https://someUrl.com/fetchPointHistory?id=contactId ``` The value "contactId" here is the ID of the contact in question. This URL is hosted by the point master on their end. This URL needs to be entered into the Customer Engagement back-end. Your Voyado team will do this. The point master needs to ensure the response from the endpoint has this form: ```json theme={null} [ { "transactionDate": "2023-05-08T12:06:58Z", "amount": 1900, "description": "Purchase of tshirt" }, { "transactionDate": "2023-05-08T12:02:55Z", "amount": 1500, "description": "Purchase of jeans" } ] ``` Or, if the contact in question has no points yet, the response should be an empty array: ```json theme={null} [] ``` If a points adjustment is made in Customer Engagement manually in the UI or by an automation, and Customer Engagement is acting as point follower, then that adjustment is not made right away. Instead it is sent as a request to the point master system. This is done using a webhook, provided by the point master and configured in Customer Engagement. Once the point master has received the update, the points change will only be visible in Customer Engagement when the point master performs its next sync (see step 1 for how that works). Points adjustments made in Customer Engagement when it is acting as point follower might not have an immediate effect. You might have to wait for the next sync from the point master. The data sent over the webhook has the following form: ```json theme={null} { "eventType": "loyalty.addPoints", "id": "70b84f00-d212-47c0-a56d-feaab6294333", "payload": "FtaW9DkGDs8yZGxkHU5Urh5B3fYpdjJylVwlCMUj1YlqcJ+9VVxO5dIhawwF2mbDJIkP1B9IVTM9+he86ZFBcIzGLr3K9nhH1hXuc0527Pkruan5TGsanjj4Kf2+7ZueY2FG6Vu5+nEuecxsDBgREyHOynieMBKRlBn3b1AVq7vw9D3dMdE6PpDGKFVAqrxrfP8jmmnPCn7orP4qAIxq6X67D8Jya4bs6ixJvZ66HPUwkmg0cEKiAhtgUlZko9XXMMPJUvHssQuIe6zhfx2r/bH9zwfbKPCNuTQ9q74iSc2CoA9akwCgk/f+PqnYspSznEnBvbwBUYlNoq39MOjjAtoMOoZD8+Jb3SGV9UxL8rHroSlOEttp1c7mX61UFIFCdo/OluCc54ycrjIx9Jgmww==", "tenant": "pointfollower" } ``` The payload part is encrypted. The encryption key used must be entered into the Customer Engagement back-end, and also communicated separately to the point master system so they can perform the decryption with the same key. Once decrypted, the form of the payload will depend on whether the points adjustment was done via an automation, or manually in the Customer Engagement UI. ### Points adjustment via automation In this case, the decrypted payload will look like this: ```json theme={null} { "contactId": "1dfe06cd-a1be-4423-8ed4-afd900d998aa", "amount": 40, "description": "Points awarded", "source": "Automation", "pointDefinitionId": 1, "reason": "Voyado", "processId": "d6342090-7410-4c00-aa4b-dbd394dac7e6", "workflowId": "44931e65-e188-46c4-92bd-50e4818fece2", "workflowName": "" } ``` The attributes ProcessId, WorkflowId and WorkflowName refer to the automation. ### Points adjustment manually In this case, the decrypted payload will look like this: ```json theme={null} { "contactId": "1dfe06cd-a1be-4423-8ed4-afd900d998aa", "amount": 40, "description": "Give points from Engage", "source": "Manual", "pointDefinitionId": 1, "userId": "c23f28cb-d62d-42cf-9d11-b919198409ea", "userName": "Jimmy User", } ``` The `userId` and `userName` here refer to the user who made the change in Customer Engagement. The point master system can now read the points adjustment and apply it. ## View points in Customer Engagement As mentioned, when Customer Engagement is point follower, adjustments are not applied directly, but instead sent to the point master using the webhook, and from there back to Customer Engagement via the API when the next sync happens. It's important that the webhook from the point master is configured in Customer Engagement before the customer starts using it, or else points updates from automations and so on will be missed. In the Customer Engagement UI, the user can view the points added and sent out: Viewing points adjustments The Customer Engagement UI acts as an iFrame for showing point transactions after collecting them though the point master's API. ## Return matching When a return is made, certain adjustments need to be made in Customer Engagement to the item and, if relevant, to the points. # Promotions Source: https://developer.voyado.com/docs/loyalty/promotions Rewarding your customers always pays off. For this, Customer Engagement has a Promotions module. There are several kinds of promotions available in Customer Engagement but the kind you can interact with via the Customer Engagement API are *multichannel promotions* so these are the main focus here. ## Multichannel promotions A multichannel promotion is one that can be set up to be used in-store or online or both ways. These promotions can be assigned to a contact based on their previous purchases or on any other type of contact data or interaction. You can assign multichannel promotions to segments as well as to single contacts in Customer Engagement. For this reason they're also known as *personal offers*. Multichannel promotions can be used: * **In-store** - The customer is identified at the POS and their active multichannel promotions are fetched from Customer Engagement, ready to use in the checkout * **Online** - The customer will need to identify themselves either by logging in, or when they fill in their details at the checkout. Then all available multichannel promotions can be fetched from Customer Engagement and shown. How exactly this is done depends on how the e-com has been set up ## Working with multichannel promotions There are several ways to create and assign multichannel promotions when working with Customer Engagement. In this article the focus will be mostly *on the Customer Engagement API* but there are also these methods: Setting up a multichannel promotion in Customer Engagement Select the *type* of the promotion from the dropdown. You can use different values for "Can be used in store" and "Can be used online" in the same promotion. The options are: * Monetary discount * Discount in percent * Manual instruction (only for in-store POS) * External promotion code In the value type "External promotion code" the actual price rule is not stored in the promotion itself but in the retailer's system (ERP or e-com platform). When this kind of promotion is displayed, the POS looks up the relevant price rule using the external promotion code and then displays those details. Staff or customers will not notice this happening. Whenever a new promotion is created via the Customer Engagement UI, it is a draft state amd must be manually activated before it can be assigned to contacts. Assigning promotions by XML file import are covered in this article: The rest of this article will focus on working with promotions using the Customer Engagement API. ## Get promotions for contact The API endpoint used to fetch all available promotions assigned to a contact is: ```http theme={null} GET /api/v3/promotion-assignments ``` These query string parameters are used: * **contactId** - The contact's unique ID. If you have some other contact identifier, use [this endpoint](/docs/contacts/identify-contact/find-contact) to get the `contactId` you will need for this request. * **redemptionChannelType** - "ECOM", "POS" or "OTHER". If left out, all promotions are returned. ```http theme={null} GET /api/v3/promotion-assignments?contactId=67ea5555-0e09-484f-b4c4-aa3e00e1e24c&redemptionChannelType=ECOM ``` ```json theme={null} [ { "id": "00000000-0000-0000-0000-000000000000", "promotionId": "00000000-0000-0000-0000-000000000000", "externalId": "string", "type": "string", "name": "string", "expiresOn": "2026-04-24T10:44:50.626Z", "validFrom": "2026-04-24T10:44:50.626Z", "heading": "string", "description": "string", "redeemed": true, "redeemedOn": "2026-04-24T10:44:50.626Z", "assignedOn": "2026-04-24T10:44:50.626Z", "source": "Automation", "imageUrl": "string", "link": "string", "redemptionChannels": [ { "type": "POS", "valueType": "PERCENT", "value": 10, "localValues": [ { "amount": 123.45, "currency": "EUR" } ], "instruction": "string" } ] } ] ``` This identifies a specific "instance" of the promotion that was created from the "template" that was identified by `promotionId`. The `id` value is unique to that contact and is the one used when redeeming this promotion. This is the ID of the main promotion (the "template" this particular promotion was created from). Many customers can have the same value for this, since it just refers to the main promotion. This CAN'T be used to redeem a promotion. This value exists when the promotion has been created over the API. If the promotion was created manually in the Customer Engagement UI, this is null. It is not important when redeeming the promotion. The value returned as `id` is what you must use to redeem the promotion. A request to this endpoint will only return those promotions where the assigned promotion AND the main promotion are active, and with a start date that is *not* in the future. This query will include already redeemed promotions as long as they otherwise fulfill the criteria for being active. ### Redemption channels When getting a contact's promotion data, you'll get back something like this: ```json theme={null} [ { "id": "bb247ef7-80bb-440c-beb7-ab4172e56666", "promotionId": "1a50b7aa-d697-4dc6-3ce7-ab2c00b3a920", "externalId": "SO-14034", "type": "Multichannel", ... "redemptionChannels": [ { "type": "POS", "valueType": "EXTERNALOFFER", "value": "10056", "instruction": null }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "10056", "instruction": null } ] } ] ``` The `redemptionChannels` object in the response specifies if a promotion is a percentage, a monetary amount to be withdrawn from the total amount of the transaction, or an external discount code that refers to a price rule in your platform. ```json theme={null} [ { ... "redemptionChannels": [ { "type": "POS", "valueType": "MONEY", "value": { "currency": "EUR", "amount": 10 }, "instruction": null } ] } ... ] ``` These are the fields in the `redemptionChannels` object: * **type** - Which channel the promotion applies to. * **valueType** - Which type of discount we want to assign (percentage, monetary or external code). * **value** - The percentage, monetary amount or external code referring to the external price rule. With a monetary discount we also expose the *group currency* set in Customer Engagement for that client. * **instruction** - An instruction that can be used, for example, to give some information to store staff regarding the promotion. These can be displayed in all currencies declared in your Customer Engagement environment. Some `redemptionChannels` examples: ```json theme={null} "redemptionChannels": [ { "type": "POS", "valueType": "PERCENT", "value": 15, "instruction": "POS instructions." } ] ``` ```json theme={null} "redemptionChannels": [ { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "Mega_discount_March25", "instruction": "POS instructions." } ] ``` ```json theme={null} "redemptionChannels": [ { "type": "POS", "valueType": "MONEY", "value": { "currency": "EUR", "amount": 10 } } ] ``` ## Create a promotion Create a promotion using the following endpoint: ```http theme={null} POST /api/v3/promotions/multichannels ``` When creating a promotion you can give it either: * A start date and a validity time * A start date and an end date When sending in the `startDate` or `endDate` send only the year, month and day as in the examples shown below. Specific times in a day cannot be used. ```json theme={null} { "externalId": "externalIdentifier", "name": "Example promotion name", "validity": { "startDate": "2020-03-18", "assignedValidity": { "unit": "Months", "amount": 3 } }, "presentation": { "heading": "Example heading", "description": "Example description", "link": "http://www.example.com" }, "redemptionChannels": [ { "type": "POS", "valueType": "EXTERNALOFFER", "value": "POS-CAMPAIGN-123", "instruction": "Example POS instruction" }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "ECOM-CAMPAIGN-1337" } ] } ``` Here is a successful response, containing the promotion's ID: ```json theme={null} { "id": "9125f018-1838-4f65-929d-87f0b4353661", "status": "Draft", "externalId": "externalIdentifier", "name": "Example promotion ame", "validity": { "startDate": "2020-03-18T00:00:00+02:00", "assignedValidity": { "unit": "Months", "amount": 3 } }, "presentation": { "heading": "Example heading", "description": "Example description", "link": "http://www.example.com" }, "redemptionChannels": [ { "type": "POS", "valueType": "EXTERNALOFFER", "value": "POS-CAMPAIGN-123", "instruction": "Example POS instruction" }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "ECOM-CAMPAIGN-1337" } ] } ``` The value id here is the ID for the promotion definition. This is used to assign this promotion to individual contacts. Each assignment gets its own ID, connecting this promotion and contact. Here is what such a request looks like: ```json theme={null} { "externalId": "externalIdentifier", "name": "Example promotion name", "validity": { "startDate": "2020-03-18", "endDate": "2020-06-18", }, "presentation": { "heading": "Example heading", "description": "Example description", "link": "http://www.example.com" }, "redemptionChannels": [ { "type": "POS", "valueType": "EXTERNALOFFER", "value": "POS-CAMPAIGN-123", "instruction": "Example POS instruction" }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "ECOM-CAMPAIGN-1337" } ] } ``` Here is a successful response, containing the promotion's ID: ```json theme={null} { "id": "9125f018-1838-4f65-929d-87f0b4353661", "status": "Draft", "externalId": "externalIdentifier", "name": "Promotion Name", "validity": { "startDate": "2020-03-18T00:00:00+02:00", "endDate": "2020-06-18T23:59:59+02:00", "assignedValidity": null }, "presentation": { "heading": "Promotion presentation title", "description": "Promotion presentation description", "link": "http://www.example.com" }, "redemptionChannels": [ { "type": "POS", "valueType": "EXTERNALOFFER", "value": "POS-CAMPAIGN-123" }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "ECOM-CAMPAIGN-1337" } ] } ``` The maximum value for end date is "9999-12-30". If your creation request has been successful, you'll get a *HTTP 200 OK* response. Otherwise you'll get one of these: * *400: BadRequest* * *500: Error* As can be seen in the responses above, a promotion is created with status "Draft". Assigning the promotion via the API will automatically activate it. The creation response also contains the promotion `id` value which is needed when assigning the promotion to a contact (along with the contact ID). ## Update a promotion You can update (alter) a multichannel promotions over the Customer Engagement API. Only *draft* promotions can be updated, not active promotions. You can update the entire promotion or only its validity. Each method uses a different API endpoint: To update the entire multichannel promotion, used this endpoint: ```http theme={null} PUT /api/v3/promotions/multichannels/{id} ``` Send a payload in the same format as when you created the promotion, including your updates: ```json theme={null} { "id": "24138ab3-0421-4688-a4b0-40a3c410fa4f", "status": "Draft", "externalId": "externalIdentifier", "name": "Promotion Name", "validity": { "startDate": "2019-10-15T11:18:50.5258317+02:00", "assignedValidity": { "unit": "Months", "amount": 3 } }, "presentation": { "heading": "Promotion presentation title", "description": "Promotion presentation description", "link": "http://www.example.com" }, "redemptionChannels": [ { "type": "POS", "valueType": "EXTERNALOFFER", "value": "PromotionCode123", }, { "type": "ECOM", "valueType": "EXTERNALOFFER", "value": "PromotionCode123" } ] } ``` To update just the duration of an existing multichannel promotion, send a PUT to this endpoint: ```http theme={null} PUT /api/v3/promotions/multichannels/{id}/validity ``` With a payload formatted like this: ```json theme={null} { "startDate": "2019-10-14T09:18:50.885+01:00", "endDate": "2021-10-14T09:18:50.885+01:00", "assignedValidity": { "unit": "Months", "amount": 4 } } ``` ## Assign a promotion A promotion is of no use unless it is assigned to contacts. Multichannel promotions are assigned to contacts via the API like this: ```http theme={null} POST /api/v3/promotion-assignments ``` With this request you send the following kind of payload: ```json theme={null} { "contactId": "e0e63d15-1881-4f9a-9ce1-94c3e21c6f47", "promotionId": "132aa326-fa89-4a8b-af9f-0b232157f8a2" } ``` The `promotionId` value used here to assign the promotion is the `id` value returned when the promotion was created. If your request has been successful, you'll get a *HTTP 200 OK* response. Otherwise you'll get one of these: * *400: InvalidContactId, InvalidPromotionId* * *404: ContactNotFound, PromotionNotFound* ## Redeem a promotion As soon as a promotion is used in a purchase, it must be redeemed in Customer Engagement so it can't be used again. To do this you'll need to know: * the promotion's unique assignment ID * the redemption channel, either "POS" or "ECOM" Since the assignment ID is already connected to a unique contact, the contact's details are not needed when redeeming a promotion. There are two steps in redeeming: Redeem the promotion with a call to this endpoint, adding the assignment ID in the path: ```http theme={null} POST /api/v3/promotion-assignments/{id}/redeem ``` In the request body, add the redemption channel: ```json theme={null} { "redemptionChannel": "ECOM" } ``` As with every purchase, you will also call this endpoint with the full receipt information: ```http theme={null} POST /api/v3/receipts ``` Here you will add the assignment ID of the promotion used to the `usedPromotions` block in the body of the query: ```json theme={null} ... "usedPromotions": [ { "promotionId": "6374dfaa-31c5-4e34-bb88-7ccfc5fe8bf7" } ] ... ``` If you only call the `/receipts` endpoint but not `/redeem`, the promotion will still be redeemed, but often after a delay, bringing the risk that it could be used again. If you call `/redeem` but miss to add the promotion assignment ID to the `/receipts` call, the system will not know which transaction to connect the redeemed promotion to. It will try to work it out based on the timestamp, checking for transactions within ±5 minutes of the redemption. So you should always do both of these things whenever a promotion is used in a purchase. After 4.5 seconds the `/redeem` endpoint will time out, provided it has not already saved the promotion as redeemed. If the promotion *has* been redeemed (this is known as the "point of no return") when the 4.5 seconds limit is passed, the request will be processed until completion. It is therefore recommended to not have a timeout *shorter than 60 seconds*, in order to avoid situations where a promotion is redeemed in Customer Engagement but the calling system does not know it due to aborting the request. ### Redemption response codes If the request has been successful, you'll get a *HTTP 200 OK* response. Otherwise you'll get one of these: * *400: NotAValidRedemptionChannel, InvalidPromotionIdNoData* * *404: PromotionNotFound, PromotionNotAssociatedWithContact* * *409: PromotionNotValidForRedemptionChannel, PromotionAlreadyRedeemed, PromotionAlreadyRedeemed* * *504: GatewayTimeout* ## Delete a promotion You can delete a promotion using its Customer Engagement ID or its external ID (assuming an external ID was used when creating the promotion). Only *unassigned* promotions can be deleted. To delete a multichannel promotion using the ID, use this endpoint: ```http theme={null} DELETE /api/v3/promotions/multichannels/{id} ``` The `id` value here is the Customer Engagement ID ("31bc4f89-6e71-4086-8627-af8f01073331" or similar). To delete a multichannel promotion using the external ID, use this endpoint with `externalId` as the query parameter: ```http theme={null} DELETE /api/v3/promotions/multichannels?externalId=whatever ``` ## Export promotions by XML Exporting promotions by XML files are covered in this article: # Vouchers Source: https://developer.voyado.com/docs/loyalty/vouchers Vouchers are part of the **Promotions** module. Administrators can create vouchers based on the client configuration. If a contact meets the requirements then one or more vouchers can be granted to the contact. As a Customer Engagement user, you decide how a contact's points are converted to vouchers. Depending on how your model is set up, you can either: * Convert all earned points into the corresponding monetary value and then clear the points * Use a step model to send out vouchers with fixed amounts Your points model also determines what the minimum points are for vouchers to be paid out. ## Getting a contact's vouchers Once a contact has been identified, and you have their `contactId`, you can fetch their available vouchers with the following request: ```http Fetch active vouchers for customer theme={null} GET /api/v3/reward-vouchers/available ``` You can also fetch all of their vouchers (expired and redeemed as well as available) like this: ```http Fetch active, expired and redeemed vouchers for customer theme={null} GET /api/v3/reward-vouchers ``` Or you can fetch only those vouchers with the status redeemed: ```http Fetch only redeemed vouchers for customer theme={null} GET /api/v3/reward-vouchers/redeemed ``` In all these cases, the GUID of the contact is appended to the URL as the parameter contactId as in this example: ```http theme={null} GET /api/v3/reward-vouchers?contactId=a3835e0b-4acd-480f-9a24-ceac75337237 ``` ### Response Below is an example of a successful response with a *HTTP 200 OK* status and one voucher returned (in the items array). ```json Example payload response theme={null} { "count": 1, "offset": 0, "items": [ { "id": "af61c5c8-7ff8-1234-9b67-c75300738cd3", "voucherNumber": "2511233093766", "name": "Bonus gauntlets 02", "validFrom": "2023-02-08T12:15:53.3333", "expiresOn": "2023-04-08T12:15:59.4444", "redeemedOn": "2023-02-10T12:15:59.2222", "redeemed": true, "value": { "currency": "SEK", "amount": 195 }, "localValues": [ { "currency": "NOK", "amount": 195 }, { "currency": "EUR", "amount": 19.5 }], "points": 4869 } ], "totalCount": 1 } ``` ## Redeem a voucher Redeeming a voucher means telling Customer Engagement that the voucher has been used and that its status should now change from available to redeemed. To redeem a voucher for a specific contact, use this endpoint: ```http Redeem voucher for customer theme={null} POST /api/v3/reward-vouchers/{rewardVoucherId}/redeem ``` Since the `rewardVoucherId` is unique for the contact who has it, the `contactId` does not need to be sent. After 4.5 seconds this endpoint will give up and time out, provided it has not already saved the voucher as redeemed. If the voucher *has* been redeemed (this is also also known as the "point of no return") when the 4.5 seconds limit is passed, the request will be processed until completion. It is recommended to not have a timeout shorter than 60 seconds, in order to avoid situations where a voucher is redeemed in Customer Engagement but the calling system does not know it due to aborting the request. If the request has been successful, you'll get a *HTTP 200 OK* response. Otherwise you'll get one of these: * *400: InvalidRewardVoucherId* * *404: RewardVoucherNotFound* * *409: RewardVoucherAlreadyRedeemed, RewardVoucherExpired* * *504: GatewayTimeout* ## Reactivate a voucher A used voucher can be reactivated and have its status changed from redeemed back to available. To reactivate a voucher (which is connected to a specific contact) you use this endpoint: ```http Reactivate a voucher for customer theme={null} POST api/v3/reward-vouchers/{rewardVoucherId}/reactivate ``` Where `rewardVoucherId` is the reward voucher's unique ID. If the reactivation has worked, you'll get a *HTTP 200 OK* response. Otherwise, one of these: * *400: InvalidRewardVoucherId* * *404: RewardVoucherNotFound* * *409: RewardVoucherAlreadyActivated, RewardVoucherExpired, ReactivationLimitExceeded* In the voucher configuration in Customer Engagement you can specify how many times a voucher can be reactivated. 3 times is the default. It you try to reactivate the voucher more times than that, you will get the *409* error above. Reactivating a voucher does not change any of its data, such as the value, the expiry date and so on. These will remain as they were when the voucher was created. ## Webhooks Customer Engagement provides webhooks for working with points and vouchers. ## Voucher XML export Customer Engagement can export voucher data as XML files. Using this, you can export both new (unused) and redeemed vouchers. The XML files generated are saved into your tenant-specific folder on the Customer Engagement FTP server: ```bash theme={null} [base-ftp-url]/integration/bonusCheckExport ``` This is done using a scheduled job (called "Export reward vouchers"). How often this job runs can be configured. Your Voyado team can help you with setting up the FTP folders as well as the scheduled job. ### XML export fields Only one contact field (marked with \*) can be used together with **contactKey**. | Field | Example value | Description | | - | - | - | | checkNumber | 0000000111751 | An incremental numerical ID. The start value can be decided by the client and set by a product specialist. | | contactKey | 1000005760 | The key value set in the environment for the client. | | amount | 40.0000 | The monetary amount of the voucher, always shown in the group currency as configured for this Customer Engagement tenant. This is applied as a payment method or a discount on the transaction. | | validFrom | 2020-04-21T00:00:00 | Date and time from when the voucher is valid | | expires | 2021-04-21T00:00:00 | Date and time when the voucher expires | | bonusPoints | 2000.0000 | Number of bonus points withdrawn to generate the voucher | | usedDate | 2020-06-21T00:00:00 | Date and time when the voucher was redeemed in Customer Engagement. | | contactId\* | 67ea3358-0e09-484f-d1c4-aa3e00e1e29c | The contact's unique ID in Customer Engagement. | | externalId\* | 1526235 | Identification key for external systems. | | memberNumber\* | 12345678 | Contact's member number. | | socialSecurityNumber\* | 198101010101 | Contact's personal identity number. | | email\* | [example@example.com](mailto:example@example.com) | Contact's email address. | | mobilePhone\* | +46739111111 | Contact's mobile phone number. | ### Default XML format This export cannot be transformed or modified through an XSLT-transformation. The format is fixed. ### New (unused and available) vouchers ```xml theme={null} 0000000111751 1000005760 40.0000 2020-04-21T00:00:00 2020-12-31T23:59:59 2000.0000 +46702291216 ... ``` ### Used (redeemed) vouchers ```xml theme={null} ... 0000000110099 7000247 2020-04-23T10:27:27 ``` # Introduction to orders Source: https://developer.voyado.com/docs/orders/introduction-to-orders When Customer Engagement is integrated with a client's systems, the **Orders** module allows orders to be registered. The latest version of the `/orders` API stores all orders sent to the endpoint, which expands allows orders to be used in segmentation and in other areas of Customer Engagement. Other functions available are the ability to fetch all orders for a contact, or to delete an order through the API. This means that clients can correct order data errors by themselves and can update an order's data throughout its entire life-cycle. ## Orders vs Transactions The order entity is similar to the transaction entity, but there are some vital differences. An **order** represents the goods and services being purchased (or returned) at the time of the actual order, whether online or in-store. This data can *change* and the order can be updated multiple times during its life-cycle. Apart from the status changes, there may also be changes to the actual goods and services in the order, meaning a retailer can add, update or remove order line items throughout the whole order life-cycle. A **transaction** on the other hand represents an *actual exchange of goods or services* between the retailer and the customer. A transaction, once registered, can *never be changed*, since there are other processes within Customer Engagement that depend on that transaction data. For example, the loyalty points being given for a purchase, which can result in bonus payouts or other benefits. An order may result in many transactions, depending on how the goods and services are delivered, and if any are returned. An order with multiple deliveries could result in multiple transactions. Orders and transaction also use different endpoints: * The endpoint used for registering orders is `/api/v3/orders` * The endpoint used for registering transactions is `/api/v3/receipts` Both of these endpoints should be used in parallel. An order will only affect the order view and the segmentation of order data (and the sent communication using the `/action` endpoint) whereas a transaction affects multiple Customer Engagement modules, such the loyalty, targeting and reporting. ## Orders v3 API overview The orders v3 API endpoints can be divided into two types, **administration** and **action**. * The **administration** part is for creating, reading and updating orders. All changes are processed and moved into segmentation (currently available in “Filter tool”). In this group are all orders endpoints apart from `/action`. No communication is triggered when using the administration endpoints, except for the case where embedded order actions are used. * The **action** part is used when the client wants to trigger some action on an order that has been processed in Customer Engagement. For now, this basically means triggering automations, but there is a plan to add more functionality. To make sure the system uses the correct data when communicating, the client has to specify a `VersionTag` when triggering actions. This `VersionTag` is returned when a change to an order has been processed. It is available either via the `/jobstatus` endpoint when creating or updating the order, or from `/orders` endpoint. Here you can see the data flow for orders and order communication: The triggers available ## Embedded order actions When you change the status of an order or its delivery details via the API, you will receive a job ID in the response. This ID can then be used this to manually poll the system to see when the change you requested had actually been processed so that any subsequent actions, such as sending confirmation emails, can be triggered via the `/action` endpoint. Embedded order actions provide a neater way of doing this. When you change the status of an order or its delivery details, you now have the option of sending your “instructions” about what is to be done after the change is processed as an order action parameter in the payload. This allows follow-up activities such as sending confirmation email to be triggered automatically with no polling needed. Embedded order actions are an optional complement to the `/action` endpoint for triggering automations, but are *not* a replacement. If there are any issues with processing an order action (such as a contact not being found), the embedded order action will be discarded. You should consider having some form of counteraction in place in case this happens. ## The orders flow The full orders flow ## Viewing a contact's orders A contact's orders can be viewed in a tab on their contact card. The orders registered may be either purchases, returns or mixed (which includes both purchase line items and return line items). Orders on the contact card Choosing an order will bring up the order view showing order details, dates, sum of amounts and lists of line items, payments, discounts, fees, taxes and links to other orders. Purchase lines and return line are listed separately in the order view. Order details ## Segmentation of orders Order segmentation allows retailers to see if a contact has an ongoing order and the items on that order. This is useful to be able to exclude contacts from targeting campaigns or to target contacts who have an ongoing order. To take advantage of order segmentation, the retailer's instance of Customer Engagement must have migrated to the star schema, which is an enhanced version of the targeting engine. Once migrated to the star schema, it is important that the retailer's article registry in Customer Engagement is updated with the article information that the retailer wants to segment on (category, brand, color, size, etc). Segmentation of orders ## Integration log Generally, errors are raised and returned via the API endpoints synchronously when the data format is incorrect. Format validation errors may be retrieved via the `/api/v3/orders/jobs` endpoint. Errors may also be detected when the order data is validated and processed. These errors are reported to the integration log under the type “Orders”. Below is a list of the most important errors to look out for: ## GDPR support Since Customer Engagement stores contact's order data, it's important that the solution is GDPR compliant, both regarding the extraction of contact data and the right for the contact to be forgotten. The existing function “Personal data extract” is extended to also export any stored order data related to the contact. The existing functions to delete a contact on demand, or because of inactivity, also delete any order data related to them. # Order action schemas Source: https://developer.voyado.com/docs/orders/order-action-schemas Order action schemas allow you to define custom data structures for dynamic, untyped order data. This makes it possible to access and display external data fields in your Design Studio templates. This is especially useful for communicating logistics data like group-level tracking URLs, estimated delivery dates, or metadata that isn't part of the default order entity. This feature is in **beta**. This means it might at any time be changed, withdrawn from your account, or become part of a paid add-on module. You might also experience interruptions of this service, degradation of user experience or faulty behavior even in critical processes such as automations and sendouts. Voyado welcomes feedback on beta features, and encourages you to try them, but you should *not* build business critical logic on beta features. Only when a feature reaches its full release can you trust it to function reliably with no further breaking changes. ## Overview Order action schemas enable developers to define reusable schemas that will be used to validate and structure the dynamic data sent as part of an order action. * These schemas can be defined either via Config Hub or through the API * The schema is referenced when sending data to the endpoint `/api/v3/orders/{orderId}/action` or when embedding action data via another endpoint when updating an order. * The dynamic fields defined in a schema are made available directly in the Design Studio, enabling editors to insert these fields safely into order-related messages. ## Why use schemas? Previously, untyped custom data sent to the order action endpoint could only be referenced in classic email design themes. With the transition to the Design Studio, a stricter way to define and validate this data was needed. Schemas solve this problem by: * Providing structure and validation for external systems pushing data into Customer Engagement * Enabling safe and intuitive use of dynamic fields in the Design Studio UI * Supporting different data scopes such as order level, line-item level, or delivery-group level ## Data structure An Order Action Schema supports three levels of dynamic data: * **orderCustomData**: Metadata tied to the order as a whole. * **lineItemCustomData**: Data per individual order line item. * **groupCustomData**: Group-level data (such as tracking per delivery group). Each of these levels must be defined in the schema. If any are missing, the schema will not be accepted. As of now, only fields of type "string" are supported across all three data levels.  Other JSON Schema types such as numbers, booleans, arrays, or nested objects are not supported yet and should not be used in Order Action Schemas. Any non-string values sent in the payload will be converted to strings before being exposed in the Design Studio. This limitation ensures predictable behavior and consistent handling of dynamic data in order communication templates. ```json Example schema theme={null} { "schemaName": "MyLogisticsPartner", "jsonSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "orderCustomData": { "type": "object", "properties": { "longDescription": { "type": "string", "title": "Order long description", "description": "Free-text comment needed for the transactional emails" } } }, "lineItemCustomData": { "type": "object", "properties": { "id": { "type": "string", "title": "Line item id", "description": "Id used to map data to an order line item" }, "estimatedDelivery": { "type": "string" } }, "required": [ "id" ] }, "groupCustomData": { "type": "object", "properties": { "group": { "type": "string", "title": "Identifier for the group", "description": "Used to map additional custom data to a group on the order" }, "longDescription": { "type": "string" } }, "required": [ "group" ] } } } } ``` ## How to define schemas You can define order action schemas in two ways: ### 1. Via Config Hub * In the Config Hub, navigate to**Order action schemas** * Click **Add schema** * Paste your JSON schema and name the integration schema * Press "Save" to make the fields available in Design Studio ### 2. Via the API You can use the following endpoints to manage schemas programmatically: ```http List all schemas theme={null} GET /api/v3/order-action-schemas ``` ```http Get a schema by ID theme={null} GET /api/v3/order-action-schemas/{id} ``` ```http Get a schema by name theme={null} GET /api/v3/order-action-schemas/by-name/{name} ``` ```http Create a new schema theme={null} POST /api/v3/order-action-schemas ``` ```http Update an existing schema theme={null} PUT /api/v3/order-action-schemas/{id} ``` ```http Remove a schema theme={null} DELETE /api/v3/order-action-schemas/{id} ``` Each schema *must* include all three top-level keys: * `orderCustomData` * `lineItemCustomData` * `groupCustomData` The line item and group schemas must include the fields `id` and `group` and they need to be marked as "required". ## Fields in Design Studio Once a schema is in place and referenced in an order action, its fields will be available in the Design Studio UI under *Custom data*: * **Order** > Custom data * **Line items** > Custom data * **Groups** > Custom data ## Sending data via API When performing an order action (for example "confirmShipment"), pass your custom data along with a `schemaName` reference: ```http theme={null} POST /api/v3/orders/{orderId}/action ``` ```json Payload example theme={null} { "action": "ConfirmOrder", "versionTag": "yirz5f26ziyqyrvcv4d2mdoy4q", "language": "en-US", "schemaName": "MyLogisticsPartner", "data": { "orderCustomData": { "comment": "Order contains items with different fulfillment statuses. We will ship reserved items as soon as they are available." }, "lineItemCustomData": [ { "id": "item-1", "estimatedDelivery": "2026-02-06T09:57:16+01:00", "group": "group-1" }, { "id": "item-2", "estimatedDelivery": "2026-02-26T09:57:16+01:00", "group": "group-2" } ], "groupCustomData": [ { "group": "group-1", "longDescription": "Reserved — awaiting pick & pack. Estimated delivery: 6 February 2026." }, { "group": "group-2", "longDescription": "Not avalailable — awaiting stock. Estimated delivery: 26 February 2026." } ] } } ``` If validation against the schema fails, the API will return a *400 Bad Request* along with a list of issues. # Orders and actions Source: https://developer.voyado.com/docs/orders/orders-and-actions In the context of orders, an *action* is a message you send to Customer Engagement to tell it to do something based on the data in an order you've already saved in the system. In practice, it allows you to be sure that an order is ready and in the state you want before you tell Customer Engagement to use the data in it, for example to trigger an automation to send an email. Creation and updating orders are asynchronous operations. This means you must wait until Customer Engagement has finished processing the order and has returned a `versionTag` before using that to trigger an action. As processing time may vary, it is critical to have a retry mechanism in place that polls the status until a valid `versionTag` is returned. The endpoint used is: ```http theme={null} POST /api/v3/orders/{orderId}/action ``` You'll use the `orderId` of the order in question. The payload looks like this: ```json theme={null} { "action": "ConfirmOrder", "versionTag": "IXUA3ril8XnZJKUMSfP7dg==", "language": "en-US", "data": { "custom": "data", "list": [ "data1", "data2" ] } } ``` You determine what is in this string. Sent back when Customer Engagement has successfully processed the latest update to your order. Whatever else you need the automation trigger to have access to. ## **Response codes** If the action has been accepted, you'll get a *HTTP 202 Request has been accepted* response. Otherwise, one of these: * *400: Bad request* * *404: Order not found or VersionTag did not match the order* * *422: Order has no contact* ## **Data flow** Here you can see the data flow for orders and order communication: Order communication data flow ## **Order actions** These are the individual order actions a merchant can use to trigger events in Customer Engagement: When the order is confirmed. The merchant verifies and confirms that the order is legit and confirms that payment has been received. This assures the customer that their transaction was successful. When the order is shipped. The order has been handed over to a delivery service and is on its way to the customer. The customer will usually receive a tracking number to get updates on their package. When the order has been delivered. The package has been successfully delivered to the customer’s address. The merchant might want to alert the customer about the delivery. When the order has been cancelled. The order has been canceled either by the customer or the merchant due to some reason (such as out of stock or payment issues). When the order has been completed. The order has been shipped/picked up, receipt is confirmed. Or the client has paid for a digital product, and their purchase is available for download. When a custom order action is requested. # Orders API endpoints Source: https://developer.voyado.com/docs/orders/orders-api-endpoints Description of your new file. The v3 Order API is described in more detail in the Swagger definition. ## Orders endpoints summary Here follows a summary of the endpoints and what they are used for. ### Get orders by contact ```http theme={null} GET /api/v3/orders ``` Returns a paged result with links that can be used to get the next or previous result set (default of 10 results per request). ### Register a new order ```http theme={null} POST /api/v3/orders ``` Creates an order. The creation is asynchronous. Use the information in the response to follow the progress and catch any errors OR send an embedded order action in the payload which will be used when the new order is created. ### Get a specific order ```http theme={null} GET /api/v3/orders/{orderId} ``` Gets a specific order by its unique order identifier. If the optional `contactId` is supplied, a verified check is made that the order is connected to the contact. ### Replace an order ```http theme={null} PUT /api/v3/orders/{orderId} ``` Replaces an order. The replacement is asynchronous. Use the information in the response to follow the progress and catch any errors OR send an embedded order action in the payload which will be used when the order is replaced. The order must be replaced *entirely* and not partially. ### Delete an order ```http theme={null} DELETE /api/v3/orders/{orderId} ``` Deletes an order. The deletion is asynchronous. Use the information in the response to follow the progress and catch any errors. ### Get status of asynchronous request ```http theme={null} GET /api/v3/orders/jobs/{jobId} ``` Returns job status of the request made to change an order. All endpoints that create or change orders are asynchronous. These requests return a "201 Accepted" and a link to this endpoint where the client can poll for the progress of the change. If the request is successful, the return is a "302 Redirect" to the updated resource. If the request cannot be processed, the return is an Error. ### Trigger action ```http theme={null} POST /api/v3/orders/{orderId}/action ``` Trigger action events for a specific order. ### Update status ```http theme={null} POST /api/v3/orders/{orderId}/change-status ``` Update the internal and/or external status of a specific order. ### Update delivery method ```http theme={null} POST /api/v3/orders/{orderId}/change-delivery ``` Update delivery method, tracking information, or delivery date on an existing order. ## Reacting to a requested change When you request for an order to be changed or created in the system, you might then want to react to it, for example by sending a confirmation email. There are two ways you can handle this: 1. Use the **job reference ID** that is returned in the response to poll Customer Engagement and see when the requested change is done (which is when the job status has the value "Completed"). 2. include an **embedded order action** in the payload of your update/create request to instruct Customer Engagement what to do when the change is completed. This is similar to sending an order action to the /action endpoint, except you don't have to poll for "Completed" status. See more about order actions here. If using option 2, the order action parameter you'll send in the request payload will have this structure: ```json theme={null} ... "orderAction": { "action": "ConfirmOrder", "language": "en-US", "data": { ... } } ... ``` Must be one of: * ConfirmOrder * ConfirmDelivery * ConfirmShipment * ConfirmCancellation * ConfirmCompletion * Custom Optional UI language tag. Custom personalization data, used by the automation trigger. This order action, as soon as the change you've requested is completed, will cause the Order Action automation trigger to be executed with whatever data you've placed in the "data" property. For example, to send an email. When you POST or PUT an order containing an embedded order action, be aware that the embedded action will only be executed if the order is successfully processed and reaches the status “Completed”. If processing fails, due to data validation errors for example, any embedded action will be discarded and may need to be applied manually. To be absolutely sure, extra verification using the job ID can be added. ## Steps in the order handling process Here are the general steps in the order handling process: Only one order with the same orderId will ever be created. 1. The order request is posted to the /orders endpoint, where the format is validated. * If the format is valid then "202 Request has been accepted" will be received in the response along with a jobId, a status, the orderId and href, a HTTPS link at the job-status endpoint. * If the order format you sent was wrong, you'll get the response "400 Bad Request" along with a list of errors. * If an order with that orderId already exists, you'll get "409 Order already exists". * Due to asynchronous processing, in a high load scenario multiple requests might create the same order with the same orderId. Here the API might reply “202 Request has been accepted” but the order might already has already been created by another request. The job status endpoint in that case will respond with “409 Order already exists”. * There is also the case where the order data has the correct format, but the contact given does not exist in Customer Engagement. The system will indicate this by returning a status of "CompletedWithErrors". If this happens, you cannot continue, for example to try and send an action for that order. In this case, you will have to delete the order and create the contact first. Then you can try registering the order again. 2. The order is now put into order storage. 3. After the order is stored, the job status endpoint will return a *200 OK* with *Status: Completed*. 4. Later, the order is connected to a contact and the article and store references are validated. * If a contact is missing a warning is added to the Integration log. * If article or store references are incorrect, a warning is logged to the integration log. This process is easiest to navigate if you follow this pattern: 1. Create the order and ensure you get a "202 Request has been accepted" with a jobId in the response. If you intend to use an embedded order action, make sure to place it in your request payload. 2. The next step depends on your integration needs: * If you have no further processing to do on your side, you are done. The "202 Request has been accepted" response means that Customer Engagement has received the order and will process and store it. * If you need to continue working with the order, poll the status endpoint with the jobId until you get a *200 OK* with *Status: Completed* which means the order has been stored and is ready to use. Or, if you sent an embedded order action in the payload, this will now be automatically triggered. When the errorDetails value is "Version conflict", it indicates that a concurrent operation has been detected. This happens when more than one request tries to perform an update on the same order. The first request starts processing and creates / updates the order asynchronously. A subsequent request, arriving while the first is still finalizing the update, will trigger a version conflict error because the order has already been saved or updated by the first request. This mechanism ensures that every order is created or updated only once. ## More about change-status and change-delivery These asynchronous endpoints are used for updating the status and delivery details of existing orders. These are useful when you need to keep order information in sync with external systems like warehouses, logistics providers, or ERPs, without replacing the entire order. ### Change order status The endpoint used to change the order status is: ```http theme={null} POST /api/v3/orders/{orderId}/change-status ``` This endpoint is asynchronous. It returns a job ID which you can use to track the progress of the update. Alternatively you can send an order action in the payload as described above. ```json Example payload theme={null} { "status": "InProgress", "externalStatus": "Picked", "lastChangedAt": "2025-06-09T10:55:00Z", "orderAction": { "action": "ConfirmOrder", "language": "en-US", "data": { ... } } } ``` Current status of order: * "InProgress" * "Completed" * "Cancelled" Free-text external status ("Reserved", "Shipped"). Timestamp indicating the last change on the order. Optional. The data that will be sent to the Order Action automation trigger as soon as the requested change is completed. The expected response is *202 Accepted* along with a location header you can poll for the status. ### Change order delivery This endpoint allows you to update the delivery method, the tracking information, or the delivery date for an existing order. ```http theme={null} POST /api/v3/orders/{orderId}/change-delivery ``` ```json Example payload theme={null} { "method": "DHL Home Delivery", "trackingNumber": "1234567890", "trackingUri": "https://dhl.com/track?1234567890", "deliveryDate": "2025-06-08T12:00:00Z", "lastChangedAt": "2025-06-09T10:55:00Z", "orderAction": { "action": "ConfirmDelivery", "language": "en-US", "data": { ... } } } ``` Method of delivery. Tracking number for the delivery. Tracking number for the delivery. Expected delivery date/time. Timestamp indicating the last change on the order. The data that will be sent to the Order Action automation trigger as soon as the requested change is completed. The expected response is *202 Accepted* along with a location header you can poll for status. ### Use cases **Fulfillment Systems:** Update delivery status post-shipment with real-time data from logistics providers (e.g., DHL, PostNord, Bring). **Warehouse Automation:** Mark an order as “Picked” or “Packed” once physically handled, syncing external warehouse workflow with order status in Voyado. **Omnichannel Coordination :** Retailers using hybrid logistics (e.g., ship-from-store, click and collect) can ensure that Voyado reflects accurate fulfillment progress. Other things to remember: * Omitted fields are treated as intentional nulls and stored as null. * Both endpoints require a valid `orderId`. * Validation errors and missing orders return 400 or 404 respectively. # Orders data structure Source: https://developer.voyado.com/docs/orders/orders-data-structure The orders entity can represent a purchase or a return. This is referred to as the *order type*. Each order also contains several header attributes, which apply to the order as a whole as well as to the order line items that describe the individual goods or services involved. ## Structure of an order Here you can see the order entity as an overview and as a payload. Order data structure ```json theme={null} { "orderId": "order-1764771379", "contact": { "matchKey": "john@example.com", "matchKeyType": "Email" }, "externalStoreId": "Ecom-Europe", "status": "InProgress", "externalStatus": "Picked", "references": [ "order-123" ], "currencyCode": "EUR", "channel": "Ecom", "createdAt": "2025-12-03T15:15:19+01:00", "lastChangedAt": "2025-12-03T15:16:19+01:00", "lineItems": { "items": [ ... ], "subTotal": 1597 }, "payments": { ... }, "fees": { ... }, "discounts": { ... }, "delivery": { "method": "DHL", "trackingNumber": "1234567890", "trackingUri": "https://dhl.com/track/1234567890", "deliveryDate": "2025-12-06T15:16:19+01:00" }, "billingAddress": { "title": "Mr", "firstname": "John", "lastname": "Doe", "streetAddress": "Main Street 1", "postalCode": "12345", "city": "Springfield", "country": "US", "email": "john@example.com", "phone": "+1234567890" }, "deliveryAddress": { "title": "Mr", "firstname": "John", "lastname": "Doe", "streetAddress": "Main Street 1", "postalCode": "12345", "city": "Springfield", "country": "US", "email": "john@example.com", "phone": "+1234567890" }, "totalPrice": 1647, "taxes": { "items": [ { "amount": 411.75, "percent": 25, "description": "VAT" } ], "totalTax": 411.75 }, "orderAction":{ "action": "ConfirmOrder", "language": "en-US", "data":{ "customField1": "customValue1", "customField2": "customValue2" } } } ``` ### Required order attributes Order data must include the order ID (the order's unique identifier) and some contact identifiers to connect the order to a contact. Other required attributes are the status, currency code and total price. See the following list for more information. ### The order type The order type may be "Purchase" or "Return" but this is not somethng that is specified in by the client. Instead, it is calculated by the system based on the line item types present and the following rules: * The order type is “Purchase” if there is at least one purchase line item in the order (even if there are also some returns) * If there are *only* return line items, the order type will be "Return" ### Order attributes in detail The order ID is defined by the client’s system and must be *unique*. The order ID is used to get, update and delete a previously registered order. The contact object contains a `matchKeyType` (such as "Email") and a `matchKey` (in this case, the contact's email address). For Customer Engagement to understand the life cycle of the order, it is required to maintain and update the status property whenever it changes. An order always has one of these three predefined statuses: * InProgress * Completed * Cancelled The external order status is optional. It represents the client’s interpretation of the order status. The external status is not restricted and completely open for the client to define for themselves. The external status will be shown in order view and can be used in segmentation. Creating or updating an order's status or its delivery details are asynchronous processes. If an optional order action parameter (of type `EmbeddedOrderAction`) is included in the payload when creating or updating an order, that action will be applied as soon as the change specified is completed in the system. This removes the need to poll the system to check the progress of the change. This is useful when a change in an order triggers, for example, a confirmation email. This should be the date and time the order was placed in the client’s system. If omitted, it will default to the registration time in Customer Engagement. Using references, you can refer to another order by its order ID. This could be used, for example, to link a return order to its original purchase order. This is mandatory and only one currency code can be given, which then applies to the whole order. The currency code should be specified according to ISO 4217. Payments can be registered here, and if used it is mandatory to supply the correct payment method. See Swagger for the allowed payment methods An order includes one or multiple order line items, defining the different goods or services in the order. A line item always includes the item ID (SKU), quantity and price, and it may also include information about discount, tax and article description. The type tells you if the line item is a purchase or return (if omitted the default is "Purchase"). The amount given in `totalPrice` for a line item should be the total net amount with discounts and taxes included for that line, which means if quantity is more than one, all items are included int the total. Return details are optional and used to describe the return reason (free text) and the return type, to allow you distinguish between normal returns, exchanges and complaints. Discounts may be registered and this may be done on both order header level and/or line-item level. The discount type is optional. See Swagger for allowed values. The total price paid in the currency specified in `currencyCode`. Tax can be registered on both order header level and line-item level. See Swagger for allowed tax types. It is possible to register fees separate from line items, such as shipping fee and administration fee. Note the two types of order status: * The Customer Engagement "Order status" on order level, which is either "InProgress", "Completed" or "Cancelled". * The optional “External order status” on order level and line-item level which clients can use how they want. This status value is displayed as “Status detail” in the GUI. ## Line items data structure An order includes one or more **line items**, representing the goods or services involved. An example of this data can be seen here: ```json theme={null} ... { "id": "item-1", "type": "Purchase", "externalStatus": "Reserved", "quantity": 1, "article": { "sku": "1978-abc80-1", "name": "ABC 80", "description": "Advanced Basic Computer for the 1980s", "imageUri": "https://placehold.co/600x420", "targetUri": "https://example.com/abc80", "color": "Beige", "size": "Standard" }, "discounts": { "items": [ { "type": "Voucher", "description": "Reward voucher", "amount": 100, "code": "reward", "percentage": 7.7 } ] }, "pricePerUnit": 1199, "originalPricePerUnit": 1299, "totalPrice": 1199, "originalTotalPrice": 1299, "taxes": { "items": [ { "amount": 299.75, "percent": 25, "description": "VAT" } ] }, "group": "group-1" }, ... ``` Each line item can include the following information: Unique identifier for the line item. "Purchase" or "Return". Defaults to "Purchase" if this is omitted. Status of the line item in external systems (e.g., Reserved, Picked). Quantity of this item in the order. Details about the item, including SKU, name, description, and optional links to images or product pages. * `sku`: Unique stock-keeping unit identifier (required) * `name`: Name of the item * `description`: Description of the item * `imageUri`: URI pointing to an image of the item * `targetUri`: URI pointing to the item's product page Optional discounts applied to the line item, with details like "type", "description", and "amount" Optional details about returns for this line item, such as: * `type`: Can be "Normal", "Complaint", or "Change" (required) * `reason`: Free-text field with maximum length of 255 chars Net amount per unit, discounts applied, taxes included. Net amount per unit before discounts were applied, taxes included. Total net amount, discounts applied, taxes included. Total net amount before discounts were applied, taxes included. Breakdown of taxes for the line item, including amounts, percentages, and descriptions (e.g., VAT). Optional grouping information for categorizing line items. The value `lineItems.subTotal` is also present and is calculated from the sum of totalPrice for all line items. ### Article data When specifying articles on the line items of an order, only the SKU (Stock Keeping Unit) code is mandatory. The referred SKUs should be registered in advance in the article register for the segmentation based on order data to work. The SKU normally represents the item variant level of an article, used by retailers to identify and track its inventory, or stock. A SKU is a unique code consisting of letters and numbers that identify characteristics about each product, such as manufacturer, brand, style, color, and size. # Troubleshooting connected sessions Source: https://developer.voyado.com/docs/product-discovery/connected-sessions This guide is intended to be used alongside the core documentation for implementing the Customer Engagement to Product Discovery integration. For full reference on how discoveryKey works and how to implement it correctly, see [here](/docs/product-discovery/integration). To identify why Product Discovery sessions are not matching Customer Engagement contacts, resulting in a low share of connected sessions. Typical symptoms include: * Very low share of connected sessions (less than 5%) * Returning customers not being recognised * Limited personalisation impact ## Core principle A session becomes connected when, in an Product Discovery session, a customerKey of X is matched to a Customer Engagement contact with a discoveryKey of the same value X. To summarise, these conditions must all be true: 1. The Product Discovery session contains a customerKey 2. The Customer Engagement contact has a discoveryKey 3. customerKey = discoveryKey Some of the investigations below require access to Product Discovery Health Overview or Customer Engagement contact data. Depending on the access available to the developer or implementor, these steps can be carried out together with Voyado. ## 1. Establish current state Start by: * Checking the share of contacts in Customer Engagement with a discoveryKey assigned is the most important indicator and the right place to start. A well-implemented setup should have 90%+ coverage. If you are below this threshold, read more here about how to increase your share of contacts with a discoveryKey. If in-store registration is part of your model, see step 6 for guidance on how to handle this correctly. Then check: * Share of connected sessions in [Product Discovery Health Overview](https://help.elevate.voyado.com/hc/en-gb/articles/27612593730204-Health-overview-Connected-Engage) * Share of customers with purchases without a discoveryKey * Whether all purchases are sent to Customer Engagement or only member purchases Connected sessions typically range 5–40%. Where you sit in that range depends heavily on: * The share of contacts in Customer Engagement with a discoveryKey * How often visitors are identified, a loyalty-heavy retailer with mandatory login should be at the high end; a guest-checkout-heavy retailer will naturally sit lower. ## 2. Assignment of discoveryKey Is discoveryKey being set, and is it set correctly? When a user is identified, customerKey in Product Discovery and discoveryKey in Customer Engagement must have the same value. Check: * Which identifier is used as customerKey * Whether the same value is used as discoveryKey in Customer Engagement * When discoveryKey is set (login, checkout, purchase, etc.) * Whether discoveryKey is included in all Customer Engagement API requests that support it — discoveryKey is supported on contact create, update and bulk endpoints only Verify by inspecting the contact create/update API requests sent to Customer Engagement upon identification. Common issues: * discoveryKey not included in all Customer Engagement requests * discoveryKey set too late in the flow * customerKey and discoveryKey using different identifiers * A new customerKey generated instead of reusing the existing one * Existing Customer Engagement contacts missing discoveryKey entirely Resolution: * Send discoveryKey consistently in all Customer Engagement calls * Set discoveryKey as soon as the user is identified * Align customerKey and discoveryKey to the same identifier * Reuse the existing customerKey for returning visitors * Update existing contacts when they are next identified ## 3. ID consistency across systems Is the same identifier used consistently everywhere? Even if discoveryKey is being set correctly, inconsistent identifiers across systems will prevent sessions from connecting. The same identifier must flow through: * Product Discovery session → customerKey * Product Discovery events → customerKey * Customer Engagement contact → discoveryKey * Customer Engagement events (including purchase) → discoveryKey Common issues: * Different identifiers used across systems * The existing customerKey not being reused when a visitor is identified (e.g. at login), causing a mismatch with the discoveryKey in Customer Engagement * A new customerKey generated instead of reusing the existing one * Purchase events sent to Customer Engagement before discoveryKey has been applied in the same flow Resolution: * Use one stable identifier for both customerKey and discoveryKey * When a visitor is identified (e.g. at login), reuse the existing customerKey — or if a known customerKey is retrieved from the user account, switch to that and update the client side accordingly * Ensure discoveryKey is set and purchase events are sent in the correct order within the identification flow ## 4. Analyse unmatched customerKeys Product Discovery Health Overview can provide a sample of customerKeys for returning customers (purchase within last 6 months) that don't match any discoveryKey in Customer Engagement. This step can be done together with Voyado if access to Health Overview is limited. For each customerKey in the sample, check: * Does the customerKey exist in the ecom system, and which contact does it belong to? * Does that contact exist in Customer Engagement? * Does the Customer Engagement contact have a discoveryKey? * Does the discoveryKey match the Product Discovery customerKey? * Are multiple contact profiles present for the same user? Possible explanations: * discoveryKey is missing * discoveryKey is not equal to customerKey * Only member purchases sent to Customer Engagement * Incorrect identification at checkout * customerKey regenerated instead of reused If the volume is large, consider a bulk update. If your system holds both the Product Discovery customerKey and the Customer Engagement contactId, a bulk import can backfill discoveryKeys for existing contacts quickly. ## 5. Verify soft ID / email channel identification Visitors arriving via email links should be identified automatically via the soft ID (eclub) parameter. This is one of the highest-leverage identification touchpoints and a common gap. The following checks require Voyado's involvement, raise these with your Voyado team: * Is soft identification enabled for this environment? * Does the decrypted soft ID payload include the discoveryKey field? (Not on by default — requires explicit activation by the Voyado team) Depending on the configuration, identification works as follows: * If soft ID is active and discoveryKey is included in the payload, the website can use it directly as the customerKey in Product Discovery * If discoveryKey is missing from the payload, the website can still identify the visitor via the contactId and retrieve the associated customerKey from the e-com system * If soft ID is not used at all, identification relies on whether a customerKey is already available client-side, for example from a cookie ## 6. Check in-store recruits Members recruited in-store often have no discoveryKey because there is no browser session at the point of sign-up. This can be a significant source of unmatched contacts, particularly for retailers with large physical store networks. Resolution options: * Generate a customerKey at point of in-store registration and store it in Customer Engagement immediately * If that is not possible, send a verification SMS or email as a double opt-in. If email is used and soft ID is enabled, the visitor will be identified automatically on click. With SMS, add the contactId as a shortGUID in the URL and handle identification on landing. ## 7. Verify identification logic A code-level review to confirm the implementation matches the expected behaviour described in steps 2 and 3. Check: * On login or checkout, is customerKey aligned with discoveryKey? * Is the updated identifier used in all subsequent events and purchases? Common issues: * customerKey generated multiple times instead of reused * customerKey not updated after identification * discoveryKey not included in Customer Engagement events post-identification * Purchase events sent before identification has been applied ## Expected outcome A well-implemented setup typically delivers connected sessions in the 20–40% range for loyalty-driven retailers. Higher connected session rates directly enable: * More relevant product recommendations * Audience-based onsite personalisation * Behaviour-driven marketing in Customer Engagement For retailers this typically results in improvements to conversion rate, average order value, and revenue per visitor. # Boosting discoveryKey use Source: https://developer.voyado.com/docs/product-discovery/discovery-key The `discoveryKey` attribute lets you tap into the full potential of both Customer Engagement's customer insights and Product Discovery's product intelligence. Ensuring that a high number of contacts have this means that you'll be more equipped to personalize your site using Customer Engagement data, and sessions enriched by Product Discovery's AI-powered product understanding are more likely to link to a contact in Customer Engagement. Every implementation is unique and here “ecom“ could mean different things (one of several different platforms) depending on the retailer. Use this article as a source of ideas to further investigate with your tech team and your team at Voyado. Here are some tips to help you maximize the number of your contacts using `discoveryKey` in Customer Engagement. ## New member recruited onsite The simplest case. A key aspect of the Customer Engagement and Product Discovery integration is using the `customerKey` generated by the e-commerce platform as the `discoveryKey` when creating a contact in Customer Engagement. All new contacts in Customer Engagement should have a `discoveryKey`. You can use any internal ID as discoveryKey as long as: * It is the same the one as used for customerKey * It is a unique value in your system ## Existing member identified onsite Assigning a `discoveryKey` value to your existing members is likely the most important step in identifying as many visitors as possible. This is done by always using the existing the `customerKey` as the `discoveryKey` in Customer Engagement whenever a user is identified (no matter how or when). ## New member recruited in-store When recruiting a member in-store, create the `customerKey` at the same time, if possible, so it can be added when setting up the contact in Customer Engagement. If that's not an option, a good approach is to send a verification SMS or email as a double opt-in via an automation. The SMS or email should link to the e-commerce site, where additional customer data and consent can be collected. If you use email, and soft identification is enabled, the visitor will be identified automatically. With SMS, the identification needs to be managed by, for example, adding the contactId as a shortGUID in the URL. Once identified in the e-commerce platform, the customerKey should be assigned as the discoveryKey in Customer Engagement. ## Bulk update of contacts When a contact signs in or is otherwise identified, you can make an API call to Customer Engagement to update their information with their assigned `discoveryKey`. This will increase the share of contacts in Customer Engagement with a `discoveryKey` value. If there is a registry held by the e-com that includes both the Product Discovery `customerKey` and the Customer Engagement `contactId` consider making a bulk import to save these values in Customer Engagement and boost the rate. There are several ways to update contacts in bulk, and they are explained in detail here: # Integration with Product Discovery Source: https://developer.voyado.com/docs/product-discovery/integration Voyado Product Discovery (formerly Voyado Elevate) is a powerful search and recommendation engine, tailor-made for e-commerce. It analyses shopper behaviour (clicks, searches, purchases) to make tailored product recommendations. Recommendations adapt in real time based on behaviour on the site. With the Customer Engagement integration to Product Discovery, you can unlock a set of connected features where the two products work together in ways that aren’t possible on their own. This integration is powered by shared identification, contact data, and behavioral signals, enabling smarter personalization across channels. Examples include: * **Connected recommendations** – Product Discovery generates dynamic product recommendations that can be used directly in Customer Engagement’s Design Studio. * **Audience-driven personalization** – Product Discovery personalizes the on-site experience based on audiences created in Customer Engagement. * **Current interest audiences** – Customer Engagement audiences can be built using real-time behavioral data from Product Discovery. * **Smarter recommendations** – Product Discovery's recommendations are enhanced with in-store purchase data captured by Customer Engagement. By making Customer Engagement and Product Discovery agree on who a particular contact is, data such as audiences and behavior can be easily shared between the two products. This is done with a unified identifier called `discoveryKey`. The `discoveryKey` attribute is used to connect the same contact in Customer Engagement and Product Discovery. This article looks at how this integration is done. ## Customer key In Product Discovery, the `customerKey` is the unique identifier for a visitor on the website. A `customerKey` is required for every request to the Product Discovery API, whether for notifications or queries. To link a visitor in Product Discovery to a Customer Engagement contact, the `customerKey` from Product Discovery should be stored in the `discoveryKey` contact attribute within Customer Engagement. This enables cross-platform understanding of the visitor and enhances personalization capabilities across both Product Discovery and Customer Engagement. ## Identifying the visitor There are two general ways a visitor can be identified: * Upon entry * During the session When a visitor enters the site, they may arrive wither organically, from a referral or through a newsletter. What happens next in this case depends on the `customerKey`: * If a `customerKey` is already available (stored client-side or retrieved from `softId`), use it in all Product Discovery calls * If no `customerKey` exists, create one, store it client-side (localStorage or cookie) and then use it in Product Discovery calls * If a visitor arrives via `softId` (from a Customer Engagement newsletter), you'll need to decode the `softId`, retrieve the matching `customerKey`, and overwrite the local value of `customerKey` if it is different The customer key" For detailed behavior, see the specific visitor types below. If the customer is not identified at entry (see above), they can be identified during the session if they: * Creating an account * Logging in to an existing account * Signing up for a newsletter, wishlist, or reminder * Placing an order Visitor identification ### After identification When the visitor has been identified, check if they exist in Customer Engagement. * If they don't exist, create the contact and assign `discoveryKey` to the `customerKey` value * If they do exist, retrieve and compare the `discoveryKey` to the client's `customerKey` and if the values are different, overwrite the client-side `customerKey` with the Customer Engagement `discoveryKey` For detailed behavior, see the specific visitor types below. ### Best practices for identification * Ensure Customer Engagement is updated with the `discoveryKey` for all members (visitors who can sign in) * All `discoveryKey` values must be unique; duplicate values in Customer Engagement prevent personalization * Always call Product Discovery with the existing `customerKey` when a visitor is identified * Use Customer Engagement's `softId` for newsletters to maximize identification rate and cross-system linking * Define a single source of truth for the `customerKey`, for example the `discoveryKey` in Customer Engagement, to resolve conflicts between local and server values ## Integration flows Now we will examine the different flows for the three types of visitor: * New visitors * Returning visitor * Newsletter visitors ### New visitor flow When the visitor enters the site: * Generate a new `customerKey` * Store that `customerKey` value on the client side When the visitor browses, for example, a category or product page: * Read `customerKey` from the client side * Call Product Discovery API with `customerKey` When the visitor clicks on, for example, a navigation link, add to cart or add favorite: * Read `customerKey` from the client side * Call Product Discovery API with `customerKey` When the visitor signs up: * Read `customerKey` from the client side * Create user account with `customerKey` * Call Customer Engagement API to create new contact with `discoveryKey` * Update user account with `contactId` from Customer Engagement This should create a mapping between the user account, the Product Discovery `customerKey`, and the Customer Engagement `contactId`. New visitor flow This scenario also covers situations where an unknown visitor makes a purchase and a contact is created in Customer Engagement. ### Returning visitor flow When the visitor enters the site: * Read `customerKey` from the client side When the visitor browses, for example, a category or product page: * Read `customerKey` from the client side * Call Product Discovery API with `customerKey` When the visitor clicks on, for example, a navigation link, add to cart or add favorite: * Read `customerKey` from the client side * Call Product Discovery API with `customerKey` When the visitor signs in: * Read `customerKey` from user account * Call Customer Engagement API to update contact with `discoveryKey` * Update the client side with `customerKey` The visitor is now identified, and the existing `customerKey` from the user account may differ from the `customerKey` on the client side. Therefore it is important to use the `customerKey` from the user account when calling the Customer Engagement API and to update the client side value accordingly Returning visitor flow ### Newsletter visitor flow When the visitor enters the site: * Decode `softId` and identify visitor * Read `discoveryKey` or `customerKey` from decoded `softId` or from user account * Update the client side with `customerKey` When the visitor browses, for example, a category or product page: * Read `customerKey` from the client side * Call Product Discovery API with `customerKey` When the visitor clicks on, for example, a navigation link, add to cart or add favorite: * Read `customerKey` from the client side * Call Product Discovery API with `customerKey` When the visitor signs in: * Read `customerKey` from user account * Call Customer Engagement API to update contact with `discoveryKey` * Update the client side with `customerKey` Newsletter visitor flow ## Identifying email traffic by discoveryKey The `discoveryKey` is an integral part of the Customer Engagement soft login (previously called auto-login) and needs to be activated by your Customer Engagement specialist. Soft login is critical for maximizing identification across Customer Engagement, Product Discovery, and between the two products. A high identification rate is the foundation for realizing the full value of the integrated setup. The soft login happens via an encrypted query string parameter named "eclub" which holds information about the contact who has clicked on a link, such as a URL in an email campaign. The target site back-end can listen to this query parameter and decrypt it. The decryption of the soft login should never be done on the client side for security reasons and GDPR concerns. The encryption key property in the module settings should also never be shared or made accessible in client implementations. A soft (auto) login URL might look like this: ```http Example of soft login theme={null} https://ourdemostore.com/?vtid=9cle0_ctRE25K6kMABznMg&eclub=FwoE65oCjhuc70IBFnszvhZJ0R37pdOYIy0x3KYzsbj3kWtwP2ByD2KP4kuRvWtx7JlyrJpaYnIvI4yAnDbFcVfMJDsXW24MgIrDyqoSw09_7KIMxGri8dYkIhW7FdFFm98n4Vq87I77mA9EsBWI6ExC_MQQkHHqa0h-7pmicZtpQMl7-WJOZCsVV0aEU6E6EX7REldFBFGfHpoWw5fukAaK7nJcg68j_f1OlBgHnwM ``` The result of the server-side decryption will be an object with the following properties, containing the `discoveryKey` which is the identification key (`customerKey`) used in interactions with Product Discovery. ```json Decrypted payload theme={null} { "id": "60333", "email": "withnail@voyado.com", "contactId": "d35ec9f5-2df7-4d44-b92b-a90c001ce732", "dateTime": "2022-05-11T13:15:40Z", "discoveryKey": "0b05119e-eeb8-418a-bbfb-defa0dde417e" } ``` ### Soft identification A customer website running both Customer Engagement and Product Discovery must decrypt the soft login server-side and use the resulting `discoveryKey` as the `customerKey` in all subsequent calls to Product Discovery. This ensures that traffic originating from Customer Engagement (for example email campaigns) is correctly identified in Product Discovery, allowing personalization, tracking, and recommendations to be applied consistently from the first page view. If the decrypted payload does not contain a `discoveryKey`, it indicates that the contact has not previously been identified in Product Discovery and should be treated as a new or anonymous visitor. ## SKU and variant key need to match In Customer Engagement the SKU (stock-keeping unit) is a standard article property. Product Discovery, however, refers to articles as *products*, and the variations of products as *variants*. Each product and variant has a unique key and must use a `variantKey` that corresponds to the article `SKU` in Customer Engagement. Several features depend on these values being aligned. For example, Customer Engagement retrieves product metadata from Product Discovery to render product recommendations, and matching keys are also required for Product Discovery to consume in-store purchase data from Customer Engagement. If the Customer Engagement and Product Discovery feeds are not aligned, contact your Customer Engagement representative to discuss next steps. Be aware that some features will be unavailable until the feeds are aligned. ## Verifying discoveryKey implementation The e-com platform/site should include `discoveryKey` in every create/update contact request made to the Customer Engagement API. The discovery key There are three ways this identification can be done: ### Verify by API Verification can be done by API requests. The following endpoints include `discoveryKey` in the response: ```http theme={null} GET /api/v3/contactoverview ``` ```http theme={null} GET /api/v3/contacts ``` Discovery key in API response ### Verify end-to-end This is a simple way to test the `discoveryKey` implementation end-to-end. 1. Go to the contact card in Customer Engagement for your user. Note that the "Discovery key" field is empty. Contact card before 2. Go to the storefront of the retailer 3. Identify yourself on the site, either by signing in or creating an account. If possible, take note of the `customerKey` or ID if that is stored for a contact in the e-com/back-end system. This may be needed for future reference or troubleshooting. 4. Now back to the Customer Engagement tenant. Confirm that the `discoveryKey` has been applied to the contact by checking the field in the contact card. If you have noted the `customerKey` or ID in step 3, verify that it matches. Contact card after 5. This is an optional step you can do if the e-com/website creates a pre-membership account during newsletter sign-up. You can verify if the `discoveryKey` has been assigned to the pre-membership contact in the Customer Engagement tenant by signing up for a newsletter and checking the `discoveryKey` of the newly created contact in the Customer Engagement tenant. ### Verify in dev tools Sometimes it is possible to verify that the Product Discovery `customerKey` matches the `discoveryKey` in the Customer Engagement contact card by using the browser's developer console. Here you can analyze network communication and browser storage for clues to confirm the match between the `customerKey` and `discoveryKey`. This ensures that the correct user data is being used for current interests. Dev tools verification ## Handling duplicate discoveryKey values All contacts in Customer Engagement must have unique a `discoveryKey` value. If a `discoveryKey` is not unique, this can block contact creation and prevent personalization from working correctly. ### When a validation error occurs A `discoveryKey` validation error means that the `discoveryKey` you tried to save already exists in Customer Engagement. New or updated contacts are validated when added through the Contact API v2/v3. Other interfaces for creating or updating contacts, such as bulk endpoints, are not affected by this validation. In this case, an error will be returned: ```json Error when discoveryKey is already in use theme={null} { "errors": [ { "code": "DuplicateDiscoveryKey", "message": "The discoveryKey already exists and must be unique." } ] } ``` If you receive this error repeatedly, it may indicate that your website is not assigning unique `discoveryKey` values to visitors. This can sometimes be related to how your site generates and stores identifiers, including through connection with integrations (for example, to Product Discovery). ### Resolving the issue Your site must always assign visitors a new, unique `discoveryKey`. Once generated, this new `discoveryKey` must be used in: * Contact API requests * The visitor's cookie * Any other system where `discoveryKey` is stored (such as databases, Customer Engagement, Product Discovery) How this is implemented is site-specific, but the goal is always the same: to ensure that every visitor and member has a unique `discoveryKey` across all systems. ### Cleaning up duplicates Before cleaning up existing duplicates: * Make sure no new duplicates are being added. * Confirm that the normal flows for adding and updating contacts work correctly (without disabling validation) Here are three important cases when handling duplicates: If your e-commerce platform or another system is the master of `discoveryKey`, duplicates must be resolved there first. Once corrected, you will then send the updated `discoveryKey` values to Customer Engagement. If Customer Engagement is the master of `discoveryKey`, duplicates can be removed directly within Customer Engagement. Contact Voyado Support for assistance with this process. If you prefer to update duplicate records rather than remove them, the master system must: * Identify which contacts to update. * Send the changes to Customer Engagement (via API or integration). Depending on the scale and complexity, this process may require assistance from Voyado Support to export duplicate lists or perform bulk updates. # Ratings and Reviews (BETA) Source: https://developer.voyado.com/docs/ratings-and-reviews/ratings-and-reviews-integration This feature is in **beta**. This means it might at any time be changed, withdrawn from your account, or become part of a paid add-on module. You might also experience interruptions of this service, degradation of user experience or faulty behavior even in critical processes such as automations and sendouts. Voyado welcomes feedback on beta features, and encourages you to try them, but you should *not* build business critical logic on beta features. Only when a feature reaches its full release can you trust it to function reliably with no further breaking changes. Ratings and Reviews is a Customer Engagement feature that lets your customers submit and view product ratings and reviews directly on your storefront. This article covers: * How to add ratings and reviews widgets using Voyado's JavaScript SDK * Single Page Application handling * CSS custom properties for further customization ## Environments Voyado provides you with two isolated environments. Each has its own script URL, backend, and data store meaning that nothing is shared between them. | Environment | Script URL | Purpose | | :- | :- | :- | | Staging | `https://static.staging.onsite.voyado.com/widget/voyado.js` | Connected to your staging Voyado environment. Use for integration, testing, and QA. | | Production | `https://static.onsite.voyado.com/widget/voyado.js` | Connected to your production Voyado environment. Live, customer-facing. | Always integrate and verify in the Staging environment first. Once the setup is confirmed to work there, switch to the Production URL for your go-live. ## Setting up The basic steps to get ratings and reviews on your site are: 1. **Include the script** — Load the `voyado.js` script via GTM or a ` ``` ```html theme={null} ``` ```html theme={null} ``` ```html theme={null} ``` Here, replace `{{DL - ...}}` with your own GTM Data Layer variables. All calls to `voyado()` made before the script finishes loading are queued and replayed automatically when the script is ready, so tag ordering here is not critical. ### Adding queuing function (required) The GTM tag above loads `voyado.js` asynchronously. This means your page code may call `voyado('load-widgets', ...)` before the script has finished loading, resulting in a `ReferenceError: voyado is not defined`. To prevent this, add the following minimal stub to your `` **before** the GTM snippet. This creates a queuing function that captures any calls made before `voyado.js` arrives, which the SDK replays automatically on bootstrap. ```html theme={null} ... ``` This is a one-time addition to your page template. All `voyado()` calls made anywhere on that page are then safe regardless of the GTM load timing. GTM's own initialization snippet (`i[r]=i[r]||function()...`) performs the same setup but only after `gtm.js` has downloaded. The stub above runs synchronously during HTML parsing, before any network requests, closing the race window. ### Load widgets Create additional **Custom HTML** tags for each page type. Fire them on the appropriate triggers (for example a DOM Ready trigger filtered by the page path). ```html theme={null} ``` ```html theme={null} ``` Only list the widget types whose HTML containers actually exist on the current page. ### SPA route changes (if applicable) If your site is a SPA (Single Page Application) then create a tag that fires on your virtual page-view trigger: ```html Single Page Application theme={null} ``` ## 2 - Widget Containers (HTML) Place `
` containers anywhere in your HTML. These containers must be pure HTML, they *cannot* be injected by GTM. They must be part of your page templates. For all widgets below set **all** the following attributes on each container, in this order: | Attribute | Required | Description | | - | - | - | | `data-product-group-id` | **Yes** | Product group identifier (for variant grouping) | | `data-product-id` | **Yes** | Product identifier | | `data-variant-id` | **Yes** | Specific variant identifier | Example values are: * `data-product-group-id` = "Product" * `data-product-id` = "Product-Color" * `data-variant-id` = "Product-Color-Size" Here are the available widgets in more detail, along with where they are shown. * PLP = Product Listing Page * PDP = Product Detail Page Either the Badge widget ("voyado-badge") or the Add Review widget ("voyado-add-review") or both must be present on the product detail page (PDP). This is to ensure that the variant structure is known when the customer writes a review in the review pop-up. ### Ratings widget (PDP & PLP) This is an inline star \ score count. It is typically placed near the product title. ```html Ratings widget theme={null}
``` Variants: * `voyado-ratings--small` (16 px text) * `voyado-ratings--vertical` (stacked layout) * Or both combined. ### Badge widget (PDP — sidebar) This includes a large numeric score, stars, a review count and a "Review this product" CTA that opens the review popup. ```html Badge widget theme={null}
``` Variants: * `voyado-badge--compact` for a smaller score (48 px instead of 78 px with CTA hidden) ### Summary widget (PDP) This includes AI generated sentiment highlights from customer reviews. It is automatically hidden when there are no reviews. ```html Summary widget theme={null}
``` ### Reviews widget (PDP) This is a full review list with sort dropdown, pagination, author, date, star rating, text, verified-buyer badge, thumbs up/down voting, and comment threads. Authenticated users can edit their own reviews. ```html Reviews widget theme={null}
``` Variants: * `voyado-reviews--boxed` wraps each review in a card ### Highlights widget (PLP / PDP) This is a single AI-selected review excerpt including title, date, and a "Powered by Voyado" footer. It is lighter than the full Summary widget. ```html Highlights widget theme={null}
``` ### Distribution widget (PDP — rating breakdown) Five-row breakdown (5★ down to 1★), each row showing a mini star icon, the review count for that star value in parentheses, and a horizontal bar sized to that rating's share of all reviews. This is the widget referenced internally as "rating breakdown (5★/4★/3★…)." ```html Distribution widget theme={null}
``` ### Carousel widget (PDP — scrolling review cards) A horizontally-scrolling row of review cards under the heading "What our customers are saying," with prev/next nav buttons. Supports two optional filtering attributes not used by any other widget: * `data-min-rating`: Include only reviews at or above this star rating (for example, 4 means show only 4 and 5 star reviews) * `data-max-review-count`: Cap the number of review cards rendered (for example, to 10) ```html Carousel widget theme={null} ``` ### Add Review widget (PDP — standalone collection button) A standalone CTA button that opens the review-submission modal directly. ```html Add Review widget theme={null} ``` ## 3 - Loading widgets This is how the widgets are loaded: ```javascript Loading widgets theme={null} voyado('load-widgets', [types]) ``` You will replace \[types] here by the list of widgets you want to load. Available are: * "ratings" * "badge" * "summary" * "reviews" * "highlights" * "distribution" * "carousel" * "add-review" ```javascript PLP example theme={null} voyado('load-widgets', ['ratings']); ``` ```javascript PDP example theme={null} voyado('load-widgets', ['badge', 'ratings', 'summary', 'reviews', 'distribution', 'carousel', 'add-review']); ``` Make this call to load the widgets *after* the widget containers are in the DOM.. Only use the widget types that are present on the current page; listing absent types wastes DOM searches. ### Clearing widgets To clear the loaded widgets, call: ```javascript theme={null} voyado('reset-widgets') ``` Call this before a SPA route change, then call `load-widgets` again after the new view is rendered. ### Keeping widget attributes in sync It's important to always keep widget attributes in sync with the selected variant. This means you should always ensure that the attributes `data-product-group-id`, `data-product-id` and `data-variant-id` match what the shopper is currently viewing on the page and not just contain the values present when the initial page load was done. Whenever the shopper changes size, color, or otherwise selects a different variant, update these three attributes on *every Voyado widget present on the page* (such as .voyado-badge, .voyado-add-review, and any others you've added): If you don't do this, the attributes will be never updated, meaning a shopper who switches variant will end up submitting — or viewing — reviews for the wrong product and/or variant. Always keep widget attributes `data-product-group-id`, `data-product-id`, `data-variant-id` in sync with what the customer is currently viewing. #### If no variant is selected If the merchant hasn't selected a variant on the site (for example, the shopper is on a product page where no size or color is chosen yet), set `data-variant-id` to an empty value on the widgets. Do *not* leave a stale variant from an earlier selection. #### Links from review-request emails When a visitor arrives at your site through a link in a review-request email, the link also carries a `voyado-rating-item` parameter. The script then: * Looks for other Voyado widgets on the page, such as `voyado-badge` or `voyado-add-review` * Reads `data-product-group-id`, `data-product-id` and `data-variant-id` from the widgets and uses only the attributes that have values * If there is no widget, or all three attributes are empty, it will fall back to the value in `voyado-rating-item` For example, consider when a customer buys "variant 107-blue-xl", and that value is added to the email link as the value for `voyado-rating-item`. When the customer follows this link, the script gets this information on your site and tries to set it as `data-variant-id` in the `voyado-badge` and `voyado-add-review` widgets, but only if that attribute is empty. Here `data-product-group-id` and `data-product-id` have already been set by the script. The script only fills in an empty `data-variant-id`. It never overwrites a value you have already set. If the shopper then selects another variant, be sure to update all three attributes as described above. ## Starting from zero When starting off, when a product has no reviews and the goal is not to display but to start collecting, Voyado recommends using only the `add-review` widget and "Add Review" button. When there are no reviews, other display widget either will show an empty/zero state or hides themselves, which reads as broken or unfinished to a shopper. The "Add Review" button however works identically no matter how many reviews a product has. Once a product has a meaningful number of reviews, add the other display widgets. The "Add Review" button can still remain. ## SPA Handling If your site uses client-side routing (single page application), call `reset-widgets` before each navigation and then `load-widgets` after the new content is in the DOM: ```js SPA Handling theme={null} // Before navigating away voyado('reset-widgets'); // After new view is rendered voyado('load-widgets', ['badge', 'reviews']); ``` ## CSS Customization All visual tokens are CSS custom properties on `:root`. Override them to match your brand. ```css theme={null} :root { /* Colors */ --voyado-rating: #66C1BF; /* Filled star / accent */ --voyado-neutral: #D8D8D5; /* Empty star / dividers */ --voyado-text: #333333; /* Body text */ --voyado-cta-color: #437984; /* CTA button background */ --voyado-cta-text-color: #FFFFFF; /* CTA button label */ --voyado-review-interactions-color: #A7A7A7; --voyado-trophy-color: #E1C32E; --voyado-select-bg: #FFFFFF; --voyado-select-border-color: #E5E5E5; /* Typography */ --voyado-font-family: "Helvetica", sans-serif; --voyado-font-size-large: 28px; --voyado-font-size-medium: 22px; --voyado-font-size-small: 16px; --voyado-font-size-micro: 12px; /* Star sizes */ --voyado-star-size-large: 40px; --voyado-star-size-small: 16px; /* Spacing */ --voyado-spacing-xs: 5px; --voyado-spacing-sm: 10px; --voyado-spacing-md: 20px; --voyado-spacing-lg: 40px; /* Accessibility */ --voyado-focus-color: #005fcc; --voyado-focus-ring-width: 2px; } ``` ### Brand override example ```css theme={null} :root { --voyado-rating: #ff6b35; --voyado-cta-color: #4a90e2; --voyado-font-family: "Inter", sans-serif; } /* Or scoped to a wrapper */ .my-product-page { --voyado-rating: #FFD700; } ``` ### CSS classes for overrides | Class | Element | | - | - | | `.voyado-star--filled` | Filled star SVG | | `.voyado-star--half` | Half-filled star | | `.voyado-star--empty` | Empty star SVG | | `.voyado-ratings--value` | Numeric score text | | `.voyado-ratings--count` | Review count text | | `.voyado-badge--score` | Large numeric score | | `.voyado-cta` | CTA button (Badge, Rate) | | `.voyado-review--author` | Reviewer name | | `.voyado-review--text` | Review body | | `.voyado-review--interactions` | Thumbs + comment row | | `.voyado-reviews--select` | Sort dropdown | | `.voyado-summary--title` | AI summary heading | # Article register Source: https://developer.voyado.com/docs/setup/article-register Customer Engagement holds an internal article register for each tenant, containing all the articles (purchasable items) in the client's system. The article register is used to **enrich data within Customer Engagement** like transactions and products of interest. The register can be created and updated in two ways: * Using a product feed to populate the register * Using sources like XML files from a PIM or ERP system The line-items on a contact's transaction represent *articles*. By connecting these to the product data for those articles, you can more precisely segment and analyze a contact's shopping behavior. The more data you have about a product, the more you know about the person who bought it. ## Article vs product There is a difference between products (used for communication) and articles (used for data enrichment). These represent two separate functions and areas within the system. In more detail: An **article** is an individual line-item purchase on a transaction. * For example: A certain hat. A **product** is an article plus the other information relevant for this retail context. * For example: That certain hat, in the color red, available at a specific store. The **product metadata** is all the other pieces of information connected to a product. * For example: The hat's availability, review score, number of sellers, how many variations exist... The SKU (stock-keeping unit) is the most detailed designator for a particular article and these are always unique. Whereas article number and group number can be shared by color variants of the same product and can therefore encompass multiple SKUs. ## Article data An article in Customer Engagement includes standard fields and some flexible extra fields configured per client installation. These “spare fields” are of two types: * 10 "spare" string fields (spare1, spare2 ... spare10) * 5 "spare" date/time fields (datespare1, datespare2 ... datespare5) These fields are used to save product data. ## Article enrichment In receipts, articles generally lack any useful product data or metadata. This is to save space in the transmission and importing of transactions, but also to allow flexibility in adding product data later. This process is known as "enrichment". Enrichment involves linking the articles on a transaction with their product data. ### The SKU An article's SKU code is the key to enrichment. Since an SKU exists both for the article on the transaction and in the article register, this allows the two to be connected. The SKU is mapped to `ArticleNr` in Customer Engagement. Note that a line-item row in Customer Engagement always refers to the *most detailed version* of a specific article. You can read more about this here: # Integration best practice guide Source: https://developer.voyado.com/docs/setup/best-practice-guide This guide covers the step-by-step actions you'll take when integrating Voyado Customer Engagement to your system, as well as the basic concepts. There are many ways to integrate your systems with Customer Engagement. Here you'll see the integration best practice that Voyado recommends. This is a *high-level* overview so data fields and JSON specifications will not be covered in any detail. If you need that, consult your Swagger page, or the other relevant parts of this documentation. This guide assumes you'll be integrating your system to Customer Engagement *from scratch*. Customer Engagement also has prebuilt integrations (accelerators) supporting many third-party systems, enabling a faster and easier integration. ## 1 - Initial configuration Some initial configuration is needed in Customer Engagement before you can start integrating your data and flows. Make sure that these basic steps are done before you continue. * Confirm that **stores** are created and configured in Customer Engagement. This is needed since contact data usually has a reference to the store where the contact signed up. * Confirm that **contact attributes** are set up. This involves configuration of the contact identification key and the mandatory unique fields for contacts in Customer Engagement. * You need an **article register** in your Customer Engagement environment, so you can segment contacts based on article data attributes like category and brand. Connecting a system to Customer Engagement is always *synchronous*. So when the Customer Engagement API is called, your workflow needs to wait for a response before continuing. If Customer Engagement does *not* return a response in time, you'll need a fallback with a retry function to handle this. ## 2 - Contacts flow Contacts are the backbone of the data stored in Customer Engagement. Each one represents an individual customer. Here is how to work with them. ### Contact creation Creating a contact in Customer Engagement consists of these steps: First, check if the contact already exists in Customer Engagement, using this endpoint: ```http theme={null} GET api/v3/contacts/id ``` In the query string of this request you will send some identifier of the contact, such as their email or mobile phone number. If a contact **exists**, Customer Engagement returns their unique `contactId` value. If a contact **doesn't exist**, you'll get error code “404 contact not found” and must proceed to the next step, where you will create a contact for this customer. Now you know the customer does not already exist as a contact in Customer Engagement, you can create them using: ```http theme={null} POST api/v3/contacts ``` Customer Engagement returns *HTTP Code 201* if the creation has been successful. In the response payload you'll get a JSON with the contact's full information. In both of the steps above, the GUID of the created contact will be returned as the `id` value in the response payload. Store this value for that contact in your own system and use it in the future for looking them up. This simplifies your flows and reduces unnecessary delays and API calls to Customer Engagement. ### Contact creation error handling When no response is received from the Customer Engagement API, or when the response has errors, you'll need a plan in place to ensure that no data is lost. * **Validation errors**: Here, something in the data you sent was wrong. You should follow the error instructions to ensure good data quality before you retry. * **No-response error**: In the case of no answer, or when you get a 500 or 504 error, you should perform a retry. If errors remain, or no response is received, you should save contact changes to *local XML files in your system* while the disruption is underway. Then, when the connection has been restored, these files can be transferred to Customer Engagement to update the contact data, in one of these two ways: Here you copy the XML file you created to your FTP folder, where it is then uploaded to Customer Engagement and all affected contacts are updated. When uploading XML files via the FTP, new contacts might be created, but their contact GUIDs will *not* be returned from Customer Engagement. Therefore, the next time that contact is identified, you'll need to manually check if their GUID already exists in your systems, and save it there if it doesn't. You can send your saved contact changes to Customer Engagement using the bulk API: ```http theme={null} POST /api/v3/contacts/bulk ``` Posting data like this puts it in a queue that Customer Engagement handles asynchronously. In the meantime, you can continue with your normal workflow. ### Updating contacts When a customer changes some personal data (new address, new name, change in some consent) you'll need to do an update to sync those changes to Customer Engagement. If the Customer Engagement `contactId` has been saved in your system, as recommended above, you will not need to query Customer Engagement for it first and can just continue directly to the updating. However, if the `contactId`, for whatever reason, has not been saved, then a lookup to the Customer Engagement API must be done to fetch it, using the general lookup endpoint along with some identifying information: ```http theme={null} GET api/v3/contacts/id ``` When you have the `contactId`, the update is then done like this: ```http theme={null} PATCH api/v3/contacts/{contactId} ``` The contact attributes to be updated are sent in the request payload. You can't use this endpoint to update a contact's contact type. This has to be done using the `/contacts/{contactId}/updateContactType` endpoint. See the "Upgrading a contact" section below. When updating contacts in Customer Engagement, be sure to send only the **changes**. Empty or null values sent in an update payload will overwrite any existing values which is probably not what you want. Customer Engagement will return a *200* or *202* response code to show the request was successful. ### Contact update error handling Like in the contact creation case, it's important to have a fallback in place for when problems occur, such as when Customer Engagement is down, or does not respond, or return error code *500* is received. Again, if your first call is unsuccessful, perform a retry. If that fails, implement your *local file fallback*, saving changes that will be sent in later when the connection has been restored, using either the Customer Engagement FTP, or the bulk update endpoint. ### Fetching contact information The need will arise to fetch a contact's data, for example when presenting their personal information, their purchases or their active promotions on My Pages in your e-com. There are three endpoints used here: ```http Fetch a contact's personal data theme={null} GET /api/v3/contacts/{contactId} ``` ```http Fetch a contact's transaction history theme={null} GET /api/v3/transactions ``` ```http Fetch a contact's active promotions theme={null} GET /api/v3/promotion-assignments ``` Important to remember: * Cache the response you get here and use that data if the page was to be reloaded. This is to avoid repeated API calls, which can lead to loading delays and a worse user experience. * You should only fetch what you need. If you just want to view purchase history, then just use `/contacts/transactions`. For personal data, use only the first endpoint of the three above. * All three endpoints use `contactId` to identify the contact. Now we'll consider each one separately. To fetch the full contact data use the first endpoint: ```http theme={null} GET /api/v3/contacts/{contactId} ``` This returns a JSON with the contact's personal information (but not transactions or promotions). This is useful, for example, to get a contact's postal address, see the store where they signed up, or to display their personal information on their My Pages in your e-com. Which information you will display and how is up to you. Customer Engagement will return all contact information and you must then filter out what you wish to show for your customer. To fetch the contact's purchase and returns history, along with article data, use the second endpoint: ```http theme={null} GET /api/v3/transactions ``` The `contactId` is sent as a query string parameter. This endpoint allows you some control in paginating the response. Purchases and returns are both present in the data received from this endpoint. To fetch all of the contact's active promotions use the third endpoint: ```http theme={null} GET /api/v3/promotion-assignments ``` You have the option to restrict the response by promotion type (ECOM, POS). A promotion will be returned if both the assigned promotion AND the main promotion (the "template" it was created from) are active. Redeemed promotions may be included in this response if they otherwise fulfill the criteria for being active. ### Upgrading a contact Customer Engagement can handle multiple contact types. For example, you might want to separate your newsletter subscribers from account holders, so you will have a contact type for each. An account holder usually has contact type "Member". Ideally,this is where you want all your contacts to be. Changing a contact from other type to "Member" is called upgrading. Upgrading contacts is done using the following endpoint: ```http theme={null} POST /api/v3/contacts/{contactId}/updateContactType?contactTypeId=Member ``` When upgrading a contact to a member, it's common to *enrich* their information using the regular update method shown above. ## 3 - Transactions flow A transaction can be seen as the "receipt" showing what the contact has bought. A transaction can contain many items, those that were purchased as well as those being returned. Transaction are sent to Customer Engagement and saved there, using `contactId` to connect them to a specific contact. The endpoint used to save them is: ```http Save transactions to Engage theme={null} POST /api/v3/receipts ``` The `/orders` endpoint also exists which is used to trigger automations and transactional communication. Don't confuse these two. ### Saving a transaction 1. First check if the customer already exists as a contact in Customer Engagement. 2. If they don't exist, you'll need to create them (and remember to save their `contactId` in your system for future use). 3. Now POST the purchase information to the `/receipts` endpoint shown above, with the payload data in the correct JSON format. ### Saving a transaction error handling You'll need a plan in place to handle when no response is received from the Customer Engagement API, or if the response has error code *500*. This is to ensure that no data is lost until the connection is restored. As always, there are two approaches here: 1. **Retry**: If your first call is unsuccessful, perform a retry. 2. **Local XML**: If errors remain, or no response is received, you should be ready to save the transactions to local XML files in your system. Then, when the connection has been restored, these files can be transferred to Customer Engagement via the FTP server. If you've tried to create a contact during your transaction flow, or have updated contact information, then you'll have to handle that too when the connection is down. Remember also that when uploading XML files via the FTP, contact GUIDs will *not* be returned from Customer Engagement for any new contacts created. Therefore, the next time that contact is identified, you'll need to check if their GUID already exists in your systems, and save it there if it doesn't, to reduce unnecessary API calls and improve your flow. ## 4 - Promotions flow Promotions can be created and assigned to contacts in Customer Engagement, and then fetched for display in your e-com or POS. You will need to redeem (mark as used) any promotion that is applied to a purchase. Some things need to be done before a promotion can be used: 1. **Set up** the promotion in your own ECOM/POS/ERP using whatever discount rules might apply. 2. Set up a **matching promotion** in Customer Engagement. 3. In Customer Engagement, **assign the promotion** to one or several contacts. An active promotion can either be exported by XML from Customer Engagement to the integrating system, or it can be fetched over the Customer Engagement API in real-time. This section will focus on fetching promotions in real-time via the API. ### Promotions endpoints The relevant endpoints are: ```http Assign a promotion theme={null} GET /api/v3/promotion-assignments ``` ```http theme={null} PATCH /api/v3/contacts/{contactId} ``` ```http theme={null} POST /api/v3/promotions/codes/{promotionId}/redeem ``` ### Displaying promotions When working with promotions you'll need to decide if they are to be shown in the POS or in the customer's My Pages or both, and build the functionality for doing that yourself. These steps will just go through how promotions are fetched and redeemed, not how they are displayed. ### Redeeming promotions Redeeming a promotion in Customer Engagement is vital to prevent is from being used again, and to also connect it to the purchase where it was used, for purposes of segmenting and reporting. Fetch a customer's active promotions with this endpoint, using `contactId` in the query string: ```http theme={null} GET /api/v3/promotion-assignments ``` You can select promotions based on redemption channel (for example, some might only be available offline / online) by adding the "redemptionChannelType" attribute as "POS" or "ECOM" to the query string. In the API response, Customer Engagement returns the available promotions for that customer. You can now use your price logic to display and apply the promotion as a deduction at checkout. If a customer has updated their information during the purchase, Customer Engagement needs that updated information. Use the usual endpoint for updating contact data: ```http theme={null} PATCH /api/v3/contacts/{contactId} ``` Now redeem the promotion with the `/redeem` endpoint to stop it from being used again: ```http theme={null} POST /api/v3/promotions/codes/{promotionId}/redeem ``` This will set the promotion to “used” in Customer Engagement for that contact. The promotion identifier should also be included in the API call that is used to register the transaction, along with the rest of the transaction's data, to allow Customer Engagement to match the receipt to the promotion. ```http theme={null} POST /api/v3/receipts ``` ### Redeem promotions error handling You'll need a plan in place to handle when no response is received from the Customer Engagement API, or if the response has error code *500*. This is to ensure that no data is lost until the connection is restored. As always, there are two approaches here: 1. **Retry**: If your first call is unsuccessful, perform a retry. 2. **Local XML**: If errors remain, or no response is received, you should be ready to save the redeemed promotion to local XML files in your system. Then, when the connection has been restored, these files can be transferred to Customer Engagement via the FTP server. If you've tried to create a contact during your promotions flow, or have updated contact information, then you'll have to handle that too when the connection is down. Remember too that when uploading XML files via the FTP, contact GUIDs will *not* be returned from Customer Engagement for any new contacts created. Therefore, the next time that contact is identified, you'll need to check if their GUID already exists in your systems, and save it there if it doesn't, to reduce unnecessary API calls and improve your flow. ## 5 - Orders flow An order represents the goods and services being purchased (or returned) at a given point in time, whether online or in-store. Unlike a transaction, an order's data can change and may be updated multiple times throughout its life-cycle, for example when its status changes, or when line items are added, updated, or removed. Orders are sent to Customer Engagement using the contact's `contactId`, in the same way as transactions. The endpoint used to save them is: ```http theme={null} POST /api/v3/orders ``` ### Creating an order Creating an order in Customer Engagement consists of these steps: Before an order is sent, always check that the contact already exists in Customer Engagement, using this endpoint: ```http theme={null} GET /api/v3/contacts/id ``` Send an identifier such as email or mobile phone number in the query string. If the contact exists, Customer Engagement returns their `contactId`. If not, you'll get error code *404 contact not found* and must create the contact before continuing. If the contact doesn't already exist, create them first: ```http theme={null} POST /api/v3/contacts ``` Store the returned `contactId` in your own system, just as you would for a transaction, so it can be reused for future orders without an extra lookup. Once the contact is confirmed to exist, POST the order data to the orders endpoint: ```http theme={null} POST /api/v3/orders ``` The request is validated on receipt. If the format is valid, Customer Engagement responds with *202 Request has been accepted* together with a `jobId`, which can be used to poll the `/jobstatus` endpoint and confirm when the order has finished processing. Processing is asynchronous, so a successful *202* response does not yet mean the order is stored. If you need to trigger an action based on the order, either poll `/jobstatus` for a `Completed` status, or use an embedded order action as described below. ### Order error handling As with contacts and transactions, you'll need a plan for when no response is received from the Customer Engagement API, or when the response contains errors. * **Validation errors** — something in the sent data was incorrect. Follow the error instructions and correct the data before retrying. * **No-response error** — in case of no answer, or a 500/504 error, perform a retry. * **Contact missing** — if the order data is valid but the referenced contact doesn't exist, Customer Engagement returns status *CompletedWithErrors*. In this case, delete the order, create the contact ,and then register the order again. If errors remain, or no response is received, save the order data to local files in your system while the disruption is underway, and resend it once the connection is restored, in the same way as the fallback used for contacts and transactions. ### Triggering automations for an order Once an order has been processed by Customer Engagement, it can be used to trigger an automation, for example an order confirmation or shipping notification. There are two ways to do this. **Option 1 — Call the action endpoint with a VersionTag** Trigger the automation by calling the action endpoint: ```http theme={null} POST /api/v3/orders/action ``` To make sure Customer Engagement uses the correct data when communicating, this call must include a `VersionTag`. The `VersionTag` is returned once a change to the order has been processed, and can be fetched either from the `/jobstatus` endpoint after creating or updating the order, or from the `/orders` endpoint directly. This approach requires you to first poll for the order's `Completed` status (or fetch the current `VersionTag` from `/orders`) before calling the action endpoint. **Option 2 — Include an embedded order action** As a simpler alternative, include an embedded order action directly in the payload when creating or updating the order: ```http theme={null} POST /api/v3/orders PUT /api/v3/orders/{orderId} ``` Add your instructions as an action object in the request payload. As soon as the order reaches status `Completed`, Customer Engagement automatically triggers the automation — no polling or separate call needed. An embedded action is only executed if the order is successfully processed and reaches status `Completed`. If processing fails — for example due to a validation error — the embedded action is discarded and the automation must be triggered manually afterwards. In short: use embedded order actions whenever possible, since they remove the need for polling. Fall back to the action endpoint with a `VersionTag` when you need to trigger an automation independently of an order being created or updated — for example, later in the order's life-cycle. ## Summary The most important design considerations when connecting your client system to Customer Engagement from scratch are these: 1. The connection is usually synchronous, meaning you'll need to **wait for a response from Customer Engagement** before proceeding in your flow. The average response time for the API endpoints is generally short, but longer response times can happen. 2. Always have a **retry policy** in place, in case of network problems or the API slowing due to heavy traffic. Retry logic makes your solution more reliable. 3. Build any redundancy and backup solutions **on the client side**. For example, a purchase should always be possible to complete even if you can't get an answer from the Customer Engagement API in a reasonable time for your scenario. 4. Implement **error handling** for extraordinary situations when the connection to Customer Engagement is down. Do not make your vital business processes depend on Customer Engagement always being reachable. 5. Whenever possible **asynchronous flows** should be used for non-time-critical API calls, for example sending orders and transactions to Customer Engagement for storage of purchases and triggering automations. 6. Ensure that your client application can handle **heavy load** and high data volumes without exceeding the API rate limits, for example, by implementing queues for non-time-critical messages. 7. Any contact IDs returned from Customer Engagement should be **saved in your local systems** and used in following requests to avoid unnecessary traffic to Customer Engagement, to simplify and speed up look-ups, and to generally improve your customer's experience. # Consents Source: https://developer.voyado.com/docs/setup/consents A consent is something that an end-user of Customer Engagement has agreed to (or not agreed to) concerning their data or their account. Proper handling of consents is important from a legal perspective. Consents turn up in the POS or, in for an e-com, on the website, and are essentially requests from the retailer which the end-user can choose to allow or deny. An example would be: "Do you agree to receive our newsletter?" For each contact, the consent can be in one of three states: * **Yes** (aka True) - The contact has agreed to receive the newsletter * **No** (aka False) - The contact does not want to receive the newsletter * **Not set** - The contact has not yet given their answer Once its value has been set, a consent can only have the value "Yes" or "No". It can never again revert to “Not set”. This is by design. The consent can, however, be changed between "Yes" and "No". ## Setting up consents You can set up a consent in the Contact Attributes section of Config Hub. Once in the Contact Attributes section, select "Add Attribute" at the bottom of the Custom Attributes list. A consent definition contains a number of settings: Adding attribute example **Name:** The name of this consent as will be displayed in the POS or e-com. **Id:** A unique identifier for this consent. **Type:** Choose "Consent" here. **Default value:** If you want the consent to have a value before it's set. Use "Not set". The actual text shown for the contact, where they are asked to accept the consent, is made of two parts that are pasted together. These are entered as Display text and Link text: * **Display text:** The text displayed in the POS or e-com immediately before the link part. * **Link text:** The "link" part of the text that, when clicked on, leads to the full terms and conditions. **Condition text:** The full terms and conditions, in HTML format (shown truncated in the image above). Normally, these would be a whole page of HTML formatted text. Since Customer Engagement does no versioning of fields, the condition text is often stored at an external URL, managed by the customer. Your Voyado team will help you with setting this up. The other settings are the same as for all custom attributes. Read more about those [here](https://help.engage.voyado.com/hc/en-gb/articles/23406421398428-Contact-attributes). Be sure to hit "Save" when you are finished to save the consent in Customer Engagement. As seen in the image above, the texts in these three fields can be localized following the standard for language culture names (sv-SE, en-GB, fr-FR and so on). ## Seeing all consents for a tenant There can be more than one consent in use. To see the all consents for a tenant, this endpoint is used: ```http theme={null} GET /api/v3/consents ``` This returns a JSON array with all the consents for that tenant (or an empty array if there are no consents). Note that this holds no information about how individual contacts have responded to the consents. ```json See all consents theme={null} [ { "name":"Consent for Prescription Purchase", "id":"consentRX", "metaData":{ "displayText":{ "en-GB":"I consent to Rx purchases." }, "linkText":{ "en-GB":"Read the terms here." }, "conditionText":{ "en-GB":"The terms as a HTML block." } } }, { "name":"Consent for Membership Terms", "id":"consentMemberTerms", "metaData":{ "displayText":{ "en-GB":"Yes, I consent." }, "linkText":{}, "conditionText":{} } } ] ``` ## Getting a contact's consent states By fetching a contact's data using the /contactoverview endpoint, you can see what consents exist for this contact, and what their state is. ```json See consents for a contact theme={null} { "firstName": "Example", "email": "example@example.com", "countryCode": "SE", "contactType": "ExampleContactType", "preferences": { "acceptsEmail": true, "acceptsPostal": true, "acceptsSms": true }, ... "consents": [{ "id": "consentMemberTerms", "value": true, "date": "2022-01-01T00:00:00+0100", "source": "ECOM", "comment": "Approved member terms at checkout" }] } ``` Only consents with a value or "true" or "false" will be shown in this response. So if a consent is not set, it will not show up here. You can, however, fetch all the consents through /consents and combine that with the consent data returned for an individual contact. In this way, all consents can be shown for a contact (for example, in the POS) even if their value is not set. By combining all the consents for a tenant from /consents with a specific contact's consents fetched from /contactoverview, you can build up a complete picture of the consents for that specific contact, including the ones not set. ## Giving or revoking a consent Setting the state of a consent for an individual contact is done in the same way as for any contact update: ```http theme={null} PATCH /api/v3/contacts/{contactId} ``` In the body of the update request, add the consent like this: ```json Adding the consent theme={null} { "consents":[{ "id": "consentMemberTerms", "value": true, "comment": "Some comment here", "source": "POS" }] } ``` This example would set the state of the "consentMemberTerms" consent to true (or Yes). ## Double opt-in A double opt-in is where a contact must confirm their sign-up before they can receive marketing communication / membership. It basically makes signing up into a two-step process. How you set this up in Customer Engagement is covered in this article: # Custom triggers Source: https://developer.voyado.com/docs/setup/custom-triggers Customer Engagement's automation functionality lets you create individual marketing journeys for every contact in your system. Purchases, birthdays and other events can act as triggers to start up automated flows based on a specific contact's data. You can also set up your own custom triggers that can be used to manually start automations. For example, if you want to use some event in your POS that doesn't have a built-in trigger in Customer Engagement, a custom trigger is how you can do it. ## Setting up custom triggers You can set up custom triggers yourself in Config Hub (the Customer Engagement configuration area). Once a custom trigger is set up, you can find it in Customer Engagement when you select "New automation". Here you can see the list of all the triggers available, with the standard ones located on top. The triggers available At the bottom of this list, you should find your custom trigger or triggers. Your custom triggers From this point on, your custom trigger can be handled like a standard one. Use it to create an automation, and create branches based on the incoming data. Here is how a custom trigger automation looks: A custom trigger automation When this trigger is activated, contacts are moved into one of three branches, depending on a keyword found by a value split ("Bike", "Ski" or everything else) and then sent an SMS. The number of contacts directed into each of the three branches can be seen, as well as the total number who triggered this automation. The important points to remember about custom triggers are: * They are called over the API using `triggerId` plus some identifier (`contactId` or another key) * They can receive JSON data (data fields), the structure of which is defined per custom trigger The data fields in your custom trigger can be used both for filtering in the automation and also as variable data in the content produced by the automation. ## Custom triggers and Design Studio When working with Design Studio, you'll also need to define the JSON schema for the custom trigger so that it can be used. How you do that is described here: Important points: * Your data source must be set to the `triggerId` in Design Studio * Personalised fields in Design Studio can't be called unless defined in the custom trigger schema * Customers cannot edit a custom trigger's schema in Config Hub once created - this can be done only by the project team and support * Custom Triggers cannot be removed once created - this can be done only by the project team and support You can read more here about data sources and personalized fields ## Using a custom trigger In Customer Engagement, you can set up as many custom triggers as you want, as long as each `triggerId` is unique. Some identifying value for the contact also needs to be sent (`contactId` or whatever is configured). The key used should be decided in collaboration with your Voyado team. A custom trigger can only be triggered for an contact that exists in Customer Engagement. There are a few ways to activate a custom trigger over the API, depending on what information you have: ```http If you know the contact ID theme={null} POST /api/v3/automation/customTriggers/{triggerId}/triggerByContactId/{contactId} ``` ```http If you know contact type and the key identifier value theme={null} POST /api/v3/automation/customTriggers/{triggerId}/triggerByContactTypeAndKey/{contactType}/{keyValue} ``` ```http If you know the external contact ID theme={null} POST /api/v3/automation/customTriggers/{triggerId}/triggerByExternalContactId/{externalId} ``` ```http If you know the personal identity number theme={null} POST /api/v3/automation/customTriggers/{triggerId}/triggerBySocialSecurityNumber/{ssn} ``` ### Examples of values Here are some example values for the properties in the endpoints above: | Attribute | Type | Example value | | :- | :- | :- | | triggerId | String | myCustomTrigger | | contactId | GUID | a67f201f-d86d-4ee2-bdc7-abce4444a804 | | contactType | String | Member | | keyValue | String | 46739666666 / [test@example.com](mailto:test@example.com) | | externalId | String | 012345 | | ssn | String | 198101010101 | Note that a mobile phone number is sent *with* the country code ("46" in the example above) but *without* a `+` at the start. The actual kind of value sent for `keyValue` depends on how that is set up in the Customer Engagement back-end. It could for example be a value representing "memberNumber", "email", "mobilePhone" or "externalId", depending on your particular configuration. So if `keyValue` is configured as "email" then the email address of the contact in question is sent in the API request. ## Payload example Custom triggers are often used for transactional communication from the e-com or business layer to the end user. In these cases, a payload is often sent. Below is an example of the setup in the backend for a custom trigger that is used to populate a reset password email. ```json Custom trigger payload example theme={null} { "oldAccount":"true" OR "false", "username":"[username]", "resetUrlHTML":"[http address of unique reset link]" } ``` The value of the `oldAccount` flag can be used to filter different kinds of communication or trigger a specific automation. All automations using a certain trigger will listen for any responding API call but only the responding data sent in the payload will start the specific automation. # Product feeds Source: https://developer.voyado.com/docs/setup/product-feeds A product feed is a detailed representation of a retailer's entire product catalog. It has two main uses in Customer Engagement. The first is when Customer Engagement needs to *communicate products in emails*. This applies both to manual use cases, such as product feed modules where specific product IDs are inserted, and automated scenarios like abandoned cart, browse abandonment, and back-in-stock flows. In this context, the product feed is purely used for communication. The second use of a product feed is to populate the article register. See more about that here: ## Product feed structure Product feeds must be in a standard XML format, such as the Google Merchant Center feed format (which Voyado recommends) and must be *directly accessible through an open URL*. While a product feed URL must be open, it is possible to restrict access by allow-listing only those IP adresses that Customer Engagement will use. Talk to your Voyado account manager if this is something you need. The URL for a retailer's product feed (or feeds) is entered into the Customer Engagement back-end, by their Voyado team. If the feed exists in different languages, a feed can be defined for each language. Voyado recommends 800 000 items as the maximum in a product feed, to assure the best experience with Customer Engagement. If you need to have a product feed with more items, contact your Voyado account manager. Products of interest are connected to the *article register* and not the product feed. This simplified example above contains one item node. An actual product feed will usually have all of a retailer's products, which can be thousands or even millions of item nodes. ```xml theme={null} ExampleShop.dk - Example https://www.ExampleShop.dk Taljebælte til line Showmaster® https://www.ExampleShop.dk/hund/aktivering-traening/traening/taljebaelte-til-line Taljebælte med indbygget forlængelse, som dæmper ryk fra din hund. Benstropper forhindrer, at bæltet kryber op over hofterne. Justerbart mellem 75 - 130 cm. 041213-10-00000 Hund > Aktivering & Træning Showmaster® https://www.ExampleShop.dk/pub_images/large/041213-10-00000_1.jpg?timestamp=1618572964 new in stock 199.00 DKK 7315470000683 20381 TRUE Sort DK Standard 0.00 DKK https://www.ExampleShop.dk/pub_images/large/041213-10-00000_2.jpg?timestamp=1618572965 281 ``` ## Importing a product feed To import a product feed into Customer Engagement, the feed needs to contain all the products that can be purchased at all of a retailer's locations (e-commerce web sites as well as physical shops) and it needs to be in a single master language. It is recommended to let the PIM (Product Information Management) system generate the product feed XML file rather than the web shop. When migrating historical transactions, the product feed won't be able to add metadata, since the feed normally contains current data (the e-com does not need “old” data to be sent out to, for example, Google Shopping). Therefore, you might end up missing historical data. In some cases, a separate migration is recommended. Talk to your Voyado PM about the best approach. # Stores Source: https://developer.voyado.com/docs/setup/stores All your stores, whether physical or digital, are represented in the Customer Engagement database. They are either added manually by you, or created automatically whenever a purchase references a store that does not yet exist in your Customer Engagement environment. ## Stores and contacts Every contact in Customer Engagement has two stores connected to them: **Created-in store**: Also known as the recruited-in store, this is the store where a contact was first added to Customer Engagement. Once this is added to a contact's data it cannot ever be changed. **Regular store**: This is the contact's "usual" store as determined by RFM values. This can change based on the contact's behaviour. When a contact is created, the regular store is by default the created-in / recruited-in store. ## Working with stores Your active stores should be set up in Customer Engagement before contacts are created. Additionally, any new stores that open should also be added. This allows customers to be linked to the correct recruited-in store or regular store through the store ID in their purchase data. If a transaction is processed by Customer Engagement using a store ID that doesn't exist, then Customer Engagement will create that store. ## Creating a store Here is how to create a store in Customer Engagement: In Customer Engagement, go to **Administration**, then select **Stores** and **Stores** again. On the **Stores** page select "Create store" in the upper right corner. Fill in the fields. Required fields are "Store", "Time zone" and "External ID". The "Store" field is the display name for this store as it will be shown in Customer Engagement. It should be unique to reduce confusion but doesn't have to be. The "External ID" is the one on the receipt. It is usually found in your ERP system. This is the store's unique ID in Customer Engagement and can't be the same as another store in Customer Engagement. Adding a store Ensure the "Active" checkbox is checked and then select "Save". ## Changing store's External ID A store's External ID is used to connect imported receipts with that store. If you change a store's External ID in Customer Engagement, but are still sending receipts using the old External ID, Customer Engagement will then create a new store using that old External ID. That is not good. To correctly change a store's External ID, the customer needs to stop sending receipts using the old External ID, then change the External ID value in Customer Engagement, and then begin sending receipts using the store's new External ID. Doing it this way avoids any issues. Be sure, if changing a store's External ID, that you do it the way described above. ## Sending email as specific store This is for the classic editor only. When designing an e-mail send-out, there's a way to decide which product feed will be used to populate it. To get this working, there's a few things to do in the Customer Engagement back-end and front-end. Your Voyado team needs to make adjustments to the Customer Engagement back-end. Contact them for this step. When that's done, go to the Customer Engagement frontend and select **Administation** then **Stores** and then **Stores** again. You'll see a list of all your stores. Selecting one takes you to **Edit store**. Scroll down to the **Spare fields** section. Locate the "Locale" field. This is where you will enter the same locale as the one specified in whatever product feed you want to use for this store. Configuring spare field Now, when you are creating an email, you can select "Send as a specific store" and select the store (and, indirectly,the locale or product feed connected to it) that will be used for this send-out. Configuring store settings If you define the "locale" attribute for all stores, even physical, you can send as *any* store using the correct language/currency when creating a email send-out. You are not restricted to just e-com stores. It's only possible to get data from ONE specific product feed for every email send-out. Therefore, in Customer Engagement, you can only use one language (and one currency) per send-out. ## API endpoints for stores Here are the endpoints used for working with stores. ### Create a store To create a store, use this endpoint with the following payload structure: ```http theme={null} POST /api/v3/stores ``` ```json Payload structure theme={null} { "name": "string", "externalId": "string", "type": "string", "timeZone": "string", "city": "string", "county": "string", "countryCode": "string", "email": "string", "adjacentZipCodes": "string", "emailUnsubscribeText": "string", "emailViewOnlineText": "string", "footerHtml": "string", "headerHtml": "string", "homepage": "string", "phoneNumber": "string", "region": "string", "senderAddress": "string", "senderName": "string", "street": "string", "zipCode": "string", "active": "boolean", "locale": "string" } ``` The required fields here are `name` and `externalId`. The POST request will return a *400* error if these are missing from the payload. While not required, you should also specify: * `timeZone`: Since the Customer Engagement UI requires it and you'll probably be accessing the store through the UI at some point * `type`: Given as either "ECOMM" or "RETAIL" or "OTHER" * `countryCode`: For example "SE", "FI", "IR" - Used to improve reporting and segmentation The string value for the country will be calculated from the `countryCode` value so you don't need to send that. Note that `county` also exists as an attribute. This can be seen as an area that's between `city` and `region` in size. ### Get all stores Use this endpoint to fetch all your stores. ```http theme={null} GET /api/v3/stores?includeInactive=true ``` Optionally add `includeInactive` as "true" as shown above to include inactive stores in the response. Leave it out if you don't need it (the default is "false"). The response will have this structure: ```json theme={null} [ { "name": "010", "externalId": "010", "city": null, "countryCode": null, "county": null, "email": null, "adjacentZipCodes": null, "emailUnsubscribeText": null, "emailViewOnlineText": null, "footerHtml": null, "headerHtml": null, "homepage": null, "phoneNumber": null, "region": null, "senderAddress": null, "senderName": null, "street": null, "type": "RETAIL", "zipCode": null, "active": true, "timeZone": null } ] ``` This example contains just one store object, but a real response will usually contain many. ### Get a single store You can fetch a single store's data if your know its external ID: ```http theme={null} GET /api/v3/stores/{externalId} ``` ### Update a single store You can update a store using the same endpoint: ```http theme={null} POST /api/v3/stores/{externalId} ``` The payload will be similar to the one you used to create the store: Only send the values you want to change (which means a smaller payload than the one shown in "Create a store" above). Any empty or null values sent in the payload will overwrite the existing values for that store. The payload you'll usually send to update a store looks more like this, containing only the value or values to change: ```http theme={null} { "homepage": "newhomepage.com" } ``` # Back in stock Source: https://developer.voyado.com/docs/tracking/back-in-stock In Customer Engagement you can set up back-in-stock automations. These are used to notify customers when a particular product they have indicated an interest in is available to buy again. The main steps of a back-in-stock flow in Customer Engagement are: * Create a back-in-stock subscription * Receive a stock level update * Start the back-in-stock automation * Send a back-in-stock email This article will focus on the back-in-stock and stock-levels API endpoints. ## Back-in-stock API endpoints In order to receive back-in-stock notifications, a contact needs to have a back-in-stock subscription registered in Customer Engagement. This subscription connects a Customer Engagement contact to a specific item (by the SKU). Any back-in-stock subscription created will be deleted as soon as it has triggered a back-in-stock automation for that contact. The subscription is then seen as "used". ### 1 - Create a new subscription A back-in-stock subscription is created using the following API endpoint: ```http theme={null} POST /api/v3/inventory/backinstock/subscriptions ``` With the following kind of payload: ```json theme={null} { "contactId": "f5774867-8358-111f-85a4-af1200eced95", "locale": "en-US", "sku": "AD_0397_V_685_006_66", "externalId": "12165253-34345" } ``` The subscriber's Customer Engagement contact ID. The SKU of the item being subscribed to. Locale of the SKU and therefore which product feed to fetch product data from. A reference to an entity in an external system, only required if the value is included in the stock level update. A successful request gives a *HTTPS 201 Created* response. Since the `contactId` is sent when creating a back-in-stock subscription, that contact needs to exist in Customer Engagement. If they don't exist, you'll need to create them. For example, when a user enters their email to request a back-in-stock message, and that email can't be connected to any existing contact. When doing this, Voyado recommends using your default contact type and collecting the contact's consent for future communication. This will allow you to stay in touch after the back-in-stock notification has been sent, and communicate with that contact outside of the back-in-stock scope. ### 2 - Delete an existing subscription A back-in-stock subscription is deleted like this: ```http theme={null} DELETE /api/v3/inventory/backinstock/subscriptions/{subscriptionId} ``` The unique ID of the back-in-stock subscription. A successful deletion gives a *HTTPS 204 No Content* response. ### 3 - Get all subscriptions for a contact You can fetch all back-in-stock subscriptions for a contact like this: ```http theme={null} GET /api/v3/contacts/{contactId}/backinstock/subscriptions ``` The standard GUID identifying a contact in Customer Engagement. A successful request returns a *HTTPS 202 OK* response and this kind of data: ```json theme={null} { "data": [ { "id": "cba5aba5-cf3c-40f5-a4db-b0d100bb2c7a", "contactId": "48fba7e7-e447-4d90-a9ca-a86e00b1d8f4", "sku": "AD_0743_V_395_001_31x65", "locale": "en-US", "externalId": "12165253-34345", "createdOn": "2023-12-07T12:21:28+01:00" }, { "id": "bfca224c-de4c-4d61-903d-b0d100bb331c", "contactId": "48fba7e7-e447-4d90-a9ca-a86e00b1d8f4", "sku": "AD_0397_V_685_006_29", "locale": "en-US", "externalId": "12823253-89054", "createdOn": "2023-12-07T12:21:34+01:00" } ] } ``` ### 4 - Update one or several stock levels This endpoint updates stock levels of one or more specific SKUs in Customer Engagement. This is then used to trigger the automations that will send back-in-stock communication to subscribed contacts. Stock level updates are done like this: ```http theme={null} PUT /api/v3/inventory/stock-levels ``` With this kind of payload: ```json theme={null} [ { "quantity": 15, "locale": "sv-SE", "sku": "AD_0397_V_685_006_29", "externalId": "12165253-34345" }, { "quantity": 10, "locale": "en-US", "sku": "AD_0743_V_395_001_31x65", "externalId": "48165253-23345" } ] ``` For Customer Engagement to trigger a back-in-stock email, all products (including out of stock products) must be present in the product feed that is connected to Customer Engagement. If a product is not in the feed, a BIS email cannot be sent for it. Latest stock quantity of the SKU. The SKU of the item which has received a stock level change. This SKU has to exist in the product feed. Locale of the SKU and therefore which product feed to fetch product data from. Reference to an entity in external system. A successful request gives a *HTTPS 202 Accepted* response. Customer Engagement should be notified whenever the stock level of the given SKU has changed, or when it is considered back in stock. This enables Customer Engagement to keep track of the entire inventory regardless of the stock level. A stock quantity threshold of what the client considers to be "back in stock" can be configured in the automation trigger. ## locale and externalId in stock level updates Although neither `locale` nor `externalId` are required for stock level updates, it's strongly recommended to include at least one of these attributes when a stock level update is done. Which one you use depends on what your subscription look like. Consider the following situation. Five subscriptions are set up using these values (the `externalId` attribute is optional when setting up a subscription): | Subscription | sku | locale | externalId | | - | - | - | - | | #1 | SKU-1 | sv-se | | | #2 | SKU-1 | pl-pl | | | #3 | SKU-1 | sv-se | SE-SKU-1 | | #4 | SKU-1 | pl-pl | PL-SKU-1 | | #5 | SKU-2 | sv-se | | **Case 1:** The retailer sends `sku`, `quantity` and `locale` when doing a stock level update. An event is then triggered if both the `sku` and `locale` in the request match one or more of the subscriptions. The event created will be enriched based on the value in `locale`. * Example: The retailer sends `sku` as "SKU-1" and locale as "sv-se", triggering subscriptions #1 and #3. **Case 2:** The retailer sends `sku`, `quantity`, `externalId` in a stock update. Now for an event to be triggered, `sku` and `externalId` have to match a subscription. The event will be enriched based on `locale`. * Example: The retailer sends `sku` as "SKU-1" and `externalId` as "SE-SKU-1", triggering subscription #3. **Case 3:** The retailer sends only `sku` and `quantity` in a stock update. All subscriptions containing that `sku` will now be triggered regardless of their `externalId` or `locale` value. The event will be enriched based on the `locale`. * Example: The retailer sends the `sku` as "SKU-1" which triggers subscriptions #1, #2, #3 and #4. ## Triggering the back-in-stock automation For Customer Engagement to trigger a back-in-stock email, all products (including out of stock products) must be present in the product feed that is connected to Customer Engagement. If a product is *not* in the feed, a BIS (back-in-stock) email cannot be sent for it. A stock level update sent to Customer Engagement triggers any back-in-stock automation triggers. The automation trigger The following conditions must be met for a back-in-stock automation for a subscription to proceed. 1. The stock quantity must be higher than 0 2. A product feed must be configured for the specified locale 3. Product data must exist for the specified SKU As well as these predefined conditions, you can also define custom conditions for your back-in-stock automation trigger based on: * Locale * Quantity * SKU * ExternalId * Product data * Subscription time * Purchase * Contact attributes Once the automation has started (and passed all trigger conditions) that specific subscription will be deleted, regardless of whether the automation included communication to the subscriber or not. A value split for `locale` is also supported in back-in-stock automations. ## Logging of back-in-stock The integration log will show you which SKU has passed the predefined conditions in the automation, and the number of subscriptions that have been sent to the back-in-stock automation trigger. Back-in-stock logging # Identification and cookies Source: https://developer.voyado.com/docs/tracking/identification-and-cookies How the tracking script links a website visitor to a contact in Customer Engagement, and the cookies it uses to do it. *Identification* means linking a visitor browsing your website to a specific contact ID in Customer Engagement. Until that happens, their activity is stored against their browser instead. Once a visitor is identified, all their activity from that browser — including the activity recorded **before** they were identified — is connected to their contact record in Customer Engagement. Because anonymous activity is connected retroactively, never restrict `cart()` or `productview()` calls to logged-in or identified visitors. Send events for everyone. ## How a visitor gets identified The tracking script identifies a visitor in three ways: 1. **They click a link in an Customer Engagement email.** The link carries a `vtid` query parameter, which the script reads from the URL. 2. **Your site identifies them and calls `setContactId()`**, for example on login, registration, newsletter signup, completed purchase, or [soft identification](/docs/contacts/identify-contact/soft-identification). 3. **A `_vaI` cookie already exists** from a previous visit. The identification then applies to all subsequent events from that browser, until the cookie expires or changes (such as when a different person logs into the same browser). ### The order of events 1. The visitor gives consent for cookies. **No cookies are created before this.** 2. Activity in that browser is recorded anonymously against the client ID in the `_va` cookie. 3. The visitor is identified, and their contact ID is stored in the `_vaI` cookie. 4. Customer Engagement connects the earlier anonymous activity to that contact. ## The cookies Both cookies are first-party. | Cookie | Contains | Example value | Lifespan | | - | - | - | - | | `_va` | The **client ID**, generated by the script. Links all events from that browser together. | `VA671.1135834162` | 1 year | | `_vaI` | The **contact ID** (guid or shortguid) of an identified contact. Links those events to a contact in Customer Engagement. | `a8tPsy8eqUm-yKoeAMPv7Q` | 1 year | `_vaI` is `_va` followed by an uppercase letter **I** (I as in Ingemar), not a lowercase L. In Safari, any cookie created with JavaScript lives for a **maximum of 7 days**, regardless of the expiry date set. See [server-side cookies](#server-side-cookies) for the workaround. ### About the vtid parameter `vtid` holds the contact ID in an encoded form, which Customer Engagement decodes internally. An Customer Engagement user cannot retrieve the contact ID or identify the contact from the `vtid` value alone. When you send events, the `contactId` field accepts either the full contact ID guid or the encoded value from `vtid`. ## Using setContactId() Call `setContactId()` with the visitor's `contactId` at the moment your site identifies them: ```javascript theme={null} va("setContactId", "contactId"); ``` When using soft identification on a site running the tracking script, take the `contactId` from the decrypted `eClub` query parameter. `setContactId()` does not generate a request to the Collect endpoint. Never call `setContactId()` with zero, an empty string, `null`, `undefined` or any other invalid value. A visitor may already be identified without being logged in. For instance, if they arrived from an email link. Calling `setContactId()` with an invalid value clears that existing identification. Voyado recommends also including `contactId` explicitly in every `cart()` and `productview()` event for identified visitors. ## Server-side cookies Apple's Intelligent Tracking Prevention (ITP) caps client-side cookies and LocalStorage in Safari at 7 days. Since `_vaI` is created by the script, an identified visitor who doesn't return within 7 days loses their identification. The cap does not apply to cookies created server-side. Script version 0.1.7 and later can therefore read a server-created cookie named `_vaI_server` and use it to recreate `_vaI`. Identified visitors then stay identified when they return, even after months away. ### How to implement it Server-side, check whether the incoming request contains a `_vaI` cookie. If it does, copy its value into a new `_vaI_server` cookie with a one-year expiry and add it to the response. ```csharp An example in C# theme={null} if(HttpContext.Request.Cookies.TryGetValue("_vaI", out var contactId) && !string.IsNullOrWhiteSpace(contactId) && contactId != "undefined") { var option = new CookieOptions { Expires = DateTimeOffset.Now.AddYears(1), HttpOnly = false }; HttpContext.Response.Cookies.Append("_vaI_server", contactId, option); } ``` Server-side cookie ### When \_vaI is recreated The script recreates the `_vaI` cookie in any of these cases, checked in this order: 1. Your site calls `setContactId()`, or includes `contactId` in an event. 2. The URL contains a `vtid` parameter. 3. LocalStorage contains a `vtid` key. 4. A `_vaI_server` cookie exists. # The tracking script Source: https://developer.voyado.com/docs/tracking/tracking-script How to configure and implement the Voyado tracking script on your website. The tracking script is a small JavaScript snippet placed in the HTML of your website. It collects checkout, cart changes, and product views and sends them to Customer Engagement. The tracking script is required for: * abandoned cart * abandoned browse * abandoned checkout * products of interest * product recommendations The tracking script is not the same thing as the tracking parameters attached to a URL. The script runs on your pages while the URL parameters (such as `vtid`) are read *by* the script. ## Configure the script This is the Voyado tracking script: ```javascript theme={null} ``` Replace these two placeholders before you use it: The path to the JavaScript analytics package. There are two options, depending on the environment you are running in. ```http Production theme={null} https://assets.voyado.com/jsfiles/analytics_0.1.8.min.js ``` ```http Staging theme={null} https://assets.voyado.com/jsfiles/analytics_0.1.8.staging.min.js ``` Use the path that matches your environment. Running the production script in staging (or the other way round) means tracking will not work. `analytics_0.1.8` is the latest version. You can pin an earlier version if you need to, but note that server-side cookie support requires 0.1.7 or later. Your unique client (tenant) ID. It is the same in production and staging, and is usually the subdomain in your Customer Engagement root URL. For example, if your URL is `supershop.voyado.com`, the tenant ID is `supershop`. Contact your Voyado Customer Engagement team if you are unsure. A configured script looks like this: ```javascript Complete tracking script theme={null} ``` ## Implement the script The script must be present, initialized and running on *every page* of your site. including landing pages, category pages and other non-product pages. The script is what identifies a visitor who arrives from a Customer Engagement email link. If an email link lands on a page without the script, identification will fail. Implement the script either directly in your page templates or through a tag manager. Copy your configured snippet and paste it immediately after the `` tag of every page on your website. 1. Log into your tag manager. 2. Select **New Tag**. 3. Give the tag an easily recognizable name. 4. Open **Tag configuration** and select **Custom HTML** as the tag type. 5. Paste your configured tracking script into the box. 6. Open **Triggers** and select **All pages** (recommended). 7. Save the tag. 8. Publish the new version of your tag container. ## Script methods Once the script is running, the following functions are available on your pages. | Method | Call it | Sends a request to Collect? | | - | - | - | | `checkout` | When the checkout changes in any way | Yes | | `setTenant` | Once, in the snippet itself | No | | `setContactId` | When your site identifies the visitor | No | | `cart` | When the cart changes in any way | Yes | | `emptyCart` | When the cart is emptied or checkout completes | Yes | | `productview` | What a product is shown to the visitor | Yes | Even though you will see these referred to as functions, they are not actual functions in JavaScript. Instead, the va() function is called with two arguments: the function name as a string, and then the value / payload. For example: ```javascript theme={null} va("emptyCart", {"cartRef": "354354"}); ``` ## Improving identification in Safari Safari caps the lifetime of cookies created with JavaScript at seven days. Script version 0.1.7 and later can fall back to a cookie your server creates, which keeps visitors identified for a year. # Web activity tracking Source: https://developer.voyado.com/docs/tracking/web-activity-tracking How Customer Engagement collects checkout, shopping cart, and product view data from your website, and what that data is used for.” Web activity tracking gathers three kinds of behavioral data from your website to Customer Engagement: * checkout changes * shopping cart changes * product views Customer Engagement links that data to a contact, either immediately or later once the visitor is identified, and uses it to trigger automations and personalize communication. Visitor activity is tracked whether or not they are identified. Anonymous activity is stored in the browser and connected to a contact as soon as that visitor is identified. ## What it enables | Feature | What it needs | | - | - | | **Abandoned checkout (beta)** — trigger an automation when a checkout is not completed (no purchase happened) | A product feed, the tracking script, and [checkout tracking](/docs/tracking/tracking-checkout-changes) | | **Abandoned cart** — trigger an automation when a cart is left behind | A product feed, the tracking script, and [cart tracking](/docs/tracking/tracking-cart-changes) | | **Abandoned browse** — trigger an automation when a visitor browses but adds nothing to the cart | Everything for abandoned cart, plus [product view tracking](/docs/tracking/tracking-product-views) | | **Products of interest** — a per-contact list of up to 25 products, usable in segmentation | A product feed, the tracking script, and [product view tracking](/docs/tracking/tracking-product-views) | | **Product recommendations** — personalized recommendations in emails | Products of interest, which are combined with purchase and return history | ## Two ways to send the data The two ways of implementing tracking are: * With the tracking script * With the tracking API Almost all implementations use the *tracking script*. However the tracking API also exists for cases where a script can't be used, but this shifts more of the work onto you. | | Tracking script | Tracking API | | - | - | - | | Anonymous visitors | Tracked | **Not possible** — `contactId` is required | | Identified visitors | Tracked | Tracked | | `sessionId` | Added automatically | You must add it to every payload | | Cookies and identification | Handled by the script | You must handle it yourself | | Identifying visitors arriving from email links | Automatic, from the `vtid` URL parameter | You must read `vtid` and pass it as `contactId` | ## Key terms | Term | What it means | | - | - | | Client ID | An ID the script generates to represent a **browser**. Stored in the `_va` cookie. | | Contact ID | The Customer Engagement ID of an **identified contact**. Stored in the `_vaI` cookie. | | `vtid` | A query parameter added to links in Customer Engagement emails. Holds the contact ID in encoded form. | | `sessionId` | A unique ID for one browsing session. Customer Engagement uses it to decide whether abandoned cart or abandoned browse should trigger. | | Collect endpoint | The endpoint the tracking script posts to. It is **not** part of the Customer Engagement REST API — it is used only by the tracking script. | | `cartRef` | Your own unique reference for one shopping cart. It must stay the same for that cart's entire life. | | `itemId` | The product identifier you send. Matched against the SKU in Customer Engagement. | | `locale` | The language/market of the site, as an IETF language tag such as `sv-SE`. Must match a product feed in Customer Engagement. | `_vaI` is `_va` followed by an uppercase letter **I** (I as in Ingemar), not a lowercase L. The two are hard to tell apart in many fonts. ## Where to start Cart and product view data is enriched from the product feed. Without a feed for the relevant locale, no automation can trigger. One snippet, on every page of the site. Call `setContactId()` at the moment your site identifies someone, and understand how the cookies work. Track every cart change, and every product view. Work through the checklists before going live. # Discounts Source: https://developer.voyado.com/docs/transactions/discounts In Voyado Customer Engagement, vouchers and promotions are applied as discounts. This avoids having a customer be awarded points from a voucher that is used in a purchase. There are two kinds of discounts, line-item and total, and they work in similar ways. The discount amount is always given as the *gross* amount, which includes VAT / MOMS. ## Line-item discount A line-item discount is applied directly to the specific line-item in the transaction. For example, if the discount is 25%, the price for that specific line-item will be the original price \* 0.75. This new amount is then what the customer pays. ## Total discount A total discount is applied across the whole transaction and is not recorded as its own separate line-item. Essentially, it is a series of line-item discounts. For example, a 30% total discount will result in the price of every line-item being multiplied by 0.70, and this new total is what the customer pays. ## Example If a customer makes a purchase containing multiple items for an amount of €15, and uses a voucher with a total discount value of €5, the price the customer pays is then €10. The discount from the voucher (€5) should be evenly spread out across all line-items, meaning the amount discounted on each line-item may vary. In this example, the customer will only receive points on the amount paid by other payment methods besides the voucher (which is €10 in this case). ## The preferred method If you as a merchant are not able to make vouchers-as-discount configurable for every client, it is possible to do it other ways, but the preferred method is to always handle vouchers as a discount. ## Applying a discount Both line-item and total discounts need to be applied before the transaction is sent. The `grossPaidPrice` should always describe the amount actually paid for the item, with all discounts already removed. Regardless of the nature of the discount, `item.discount` can be used to tell Customer Engagement which discounts have been used. If an article is exposed to both a total discount (for instance 20%) and a 100 SEK line-item discount, both of those will contribute to the total amount withdrawn from the original price. For example, an article for 240 SEK will cost (240 \* 0.80) – 100 = 92 SEK after both these discounts have been applied. These aggregated row values are stored as a parameter for searching the total discount per sold article in Customer Engagement. A Customer Engagement instance needs to be configured to allow searching by discounts. Ask your Voyado team about this. ## Redeeming vouchers When a voucher is included in a purchase, it should be redeemed by [a separate API call](/docs/loyalty/vouchers#redeem-a-voucher), and also be included in the `usedBonusChecks` array in the call to `/receipts`: ```json theme={null} "usedRewardVouchers": [ { "voucherNumber": "2342" } ] ``` In the API v3 `usedRewardVouchers` has replaced `usedBonusChecks` and `voucherNumber` has replaced `checkNumber`. ## Redeeming promotions A promotion is handled in the same way as a voucher. But if a Customer Engagement promotion is used in the transaction, it should be redeemed by separate API call to the `/redeem` endpoint and should also be included in the `usedPromotions` array in the call to `/receipts`: ```json theme={null} "usedPromotions": [ { "promotionId": "6374dfaa-31c5-4e34-bb88-7ccfc5fe8bf7" } ] ``` It is important to do both of these things to ensure the promotion is properly redeemed. See the process in more detail here: # Group currency Source: https://developer.voyado.com/docs/transactions/group-currency The group currency is the main, default currency of your Customer Engagement instance. It's used in BI exports, in purchases and returns, for generating points and in reward vouchers. The format of the Customer Engagement group currency is [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217). For example: * EUR * SEK * GBP * NOK * USD Your group currency is configured in the Customer Engagement back-end. You can send transactions in other currencies, which will then be converted into the group currency in Customer Engagement. The currencies you wish to use must be configured in your Customer Engagement instance along with the exchange rates to and from the group currency, to allow correct value conversion. For example, if your group currency is set as SEK but you want to also sometimes send in transactions in EUR and GBP, you'll need to add this configuration (often called "the conversion table") in the Customer Engagement back-end: ```json Example of conversion table theme={null} [ {"currency": "EUR", "toFactor": 0.1, "fromFactor": 10}, {"currency": "GBP", "toFactor": 0.075, "fromFactor": 13.3} ] ``` A group currency value multiplied by `toFactor` gives you the value in the secondary currency. A secondary currency value multiplied by `fromFactor` gives you the value in the group currency. Clearly, `fromFactor` multiplied by `toFactor` for a currency has to equal 1.0. ## Group currency in transactions If a transaction is sent to Customer Engagement in a currency that isn't the group currency, it will be converted to the group currency using the values given in the conversion table. These values, though, are static and don't consider the daily fluctuations in the currency. A more exact way is to send the currency conversion at the moment the transaction was made, using the `exchangeRateToGroupCurrency` field in the transaction payload. Here is an example when the group currency in Customer Engagement is set to EUR: ```json Example of EUR group currency theme={null} { "contact": { "matchKey": "1c333c0a-ed33-3333-33cb-b0333333c333", "matchKeyType": "contactId" }, "uniqueReceiptId": "20180906468464468", "receiptNumber": "468464468", "createdDate": "2018-09-06T15:55:54+01:00", "storeExternalId": "321", "currency": "GBP", "exchangeRateToGroupCurrency": 0.877969, "totalGrossPrice": 11.00, "paymentMethods": [{ "type": "Card", "description": "Debit Card", "value": 11.00 }], "items": [{ ... ``` So if `exchangeRateToGroupCurrency` exists in the payload, it will be used to convert the amount to the group currency. Otherwise the default value in the conversion table will be used to do that. # Introduction to transactions Source: https://developer.voyado.com/docs/transactions/introduction-to-transactions A transaction, as defined in Customer Engagement, is when a customer visits a physical or online store and purchases (or returns) one or more items. The transaction contains its own data -- such as the transaction's unique ID, the store's ID, the timestamp -- plus the data for each individual item that was purchased or returned (known as the line items). All fields in the transaction data model can be used for segmentation and analysis in Customer Engagement. Transaction data is sent to the endpoint called `/receipts`. You might see the term "receipt" turning up other places in the Customer Engagement API or in field names. Keep in mind that "receipt" means "transaction". You might also come across an API endpoint named `/transactions`. This is a legacy endpoint maintained for a few older clients, so, despite the name, do NOT send your transaction data here. ## Processing of API calls Transactions posted to the `/receipts` API are not relayed to the database immediately. Instead, they are collected and, at a preset interval, converted into an XML file. This XML is processed and applied to the database. By default, an XML is generated every 15 minutes, meaning it could take this amount of time for a transaction to be registered in the database. It’s possible to change this interval, all the way down to 0 minutes if needed, to speed up testing. Talk to your Voyado Customer Engagement team about this. The transaction XML is converted through XSLT in the same way as historical transaction which are imported through Customer Engagement's ftp. However, no XSLT is required in this case as long as no special transformations are used. ## Duplicates Any transaction imported that has the same `uniqueReceiptId` as an already existing transaction in the database will be seen as a duplicate and ignored. ## Discounts (line-item and total) Total discounts (ex. 30% on an entire purchase) should not be recorded as a separate line item. Instead, the discount should be applied on each line-item so that each item reflects the actual price paid for the item. The same applies for line item discounts (ex. 25% on a specific item), where the discount should be deducted from the specified line item price (i.e. original price \* 0.75).”. Read more about discounts below and in the section [Discounts](/docs/transactions/discounts) ## Updates to a transaction Once a transaction is imported into Customer Engagement it is not possible to update it with new information. ## Group currency and local currency Group currency is the default currency for the specific Customer Engagement instance, normally decided in the pre-study. The property `currency.conversion.factors` sets the different exchange rates used when converting between the group currency and the local currency. ## Product metadata and spare fields Customer Engagement can access all product metadata from the transaction as well as the specific line items. Read more about other options [here](/docs/setup/article-register). There are also a limited amount of fields per product that can be used for storing extra data for a transaction: * 10 spare string fields (spare1, spare2 ... spare10) * 10 spare date fields (datespare1, datespare2 ... datespare5) These fields can be found in the API and XML under “extraData”. If they are named the same as in the client configuration, no transformation needs to be done. ## Freight In Customer Engagement's transaction model, freight should not be included, to avoid the end-user being awarded bonus points based on the freight cost. Bonus points should only be based on what the end-user has actually paid for their purchase. There are also some reports and KPIs in Customer Engagement that require the freight fee to be excluded. For example, most purchased articles, number of articles purchased, total purchase amount and the average amount on the transaction. So freight should always be excluded from the transaction's total, even if the e-com handles it separately, whether per item row or as a total amount. Regardless on how the e-com handles it, the amount total on the receipt sent to Customer Engagement is calculated *without* the freight cost, and the amount for each item must sum to the total amount. This is only true for the endpoint `/receipts`. For the /orders endpoint, Customer Engagement uses an object called freightFee to inform the end customer, in their order communication emails, how much they have paid for freight. ## Validations * `discounts.value` should always be negative * A return should have `item.type` as "return" and `quantity` should be negative * A purchase should have `item.type` as "purchase" and `quantity` should be positive * quantity should never be 0 * Sum of all `paymentMethods` should equal `totalGrossPrice`. * Sum of all `grossPaidPrice` should equal `totalGrossPrice`. * `taxAmount` should equal `grossPaidPrice` – `grossPaidPrice` / (1 + taxPercent / 100) * `createdDate` should be in ISO8601 with time zone designator # Purchase history summary Source: https://developer.voyado.com/docs/transactions/purchase-history-summary This endpoint can be used to get a summary of all a contact's previous purchases. ```http theme={null} GET /api/v3/contacts/{contactId}/purchasehistorysummary ``` ## Responses A successful update will return *HTTP 200 OK* along with the contact's purchase history summary as the response body. That will look like this: ```json theme={null} { "lastUpdated": "2025-09-05T14:53:50.4905776+02:00", "purchaseAmountAll": 2219.26, "numberOfArticlePurchasesAll": 3, "averageReceiptAll": 2219.26, "latestReceiptDate": "2023-09-05T14:53:50.4905776+02:00", "purchaseFrequencyAll": 1, "purchaseAmountLastYear": 0, "numberOfArticlePurchasesLastYear": 0, "averageReceiptLastYear": 0, "purchaseFrequencyLastYear": 1, "purchaseAmountLastTwoYears": 2219.26, "numberOfArticlePurchasesLastTwoYears": 3, "averageReceiptLastTwoYears": 2219.26, "purchaseFrequencyLastTwoYears": 2219.26 } ``` Amounts shown are given in the group currency. If the request has not been successful, you'll get this HTTP error code: * *404: RetailKpiNotCalculated*: The contact ID given was not found in the database. # Register a purchase Source: https://developer.voyado.com/docs/transactions/register-a-purchase When an identified contact finalizes a purchase, it should be sent to Customer Engagement and made available for segmentation and reporting. Usually a finalized purchase means that payment has been successfully processed. The data is sent to the following endpoint: ```http theme={null} POST /api/v3/receipts ``` Data sent here cannot be reversed or updated, hence the importance of only sending finalized purchases. ## Identification To be able to store a transaction in Voyado, you’ll need to connect it to a specific contact. The key has to be a unique data field for the specific contact type. One of these fields is used: * contactId * email * mobilePhone * memberNumber * externalId * socialSecurityNumber (actually a Swedish or Finnish Personal Identification Number) However, the key used does **NOT** have to be the key value for the specific contact type. The default contact type is usually “Member”. To send transactions for any other contact type, include contactType in the request. This is not required if contactId is used as matchKeyType, since it uniquely identifies the contact. ### Example ```json theme={null} "contact": { "matchKey": "e8a5a8ff-1616-4990-b16d-af9412999824", "matchKeyType": "email", "contactType" : "contact" } ``` ### Example payload In the below example payload the key type is “contactId”. When using this key type, you do not need to specify contactType as contactId is always unique. If you do send contactType, it will be used in the matching logic and has to match. ```json theme={null} { "contact": { "matchKey": "1c277c0a-ed93-4928-86cb-b0820090c111", "matchKeyType": "contactId" }, "uniqueReceiptId": "20180906468464468", "receiptNumber": "468464468", "createdDate": "2018-09-06T15:55:54+01:00", "storeExternalId": "321", "currency": "NOK", "exchangeRateToGroupCurrency": 1.0, "totalGrossPrice": 200.00, "paymentMethods": [{ "type": "Card", "description": "Debit Card", "value": 200.00 }], "items": [{ "type": "PURCHASE", "quantity": 2, "grossPaidPrice": 200.00, "taxAmount": 40.00, "taxPercent": 25.00, "sku": "10015563434", "articleNumber": "2324234", "articleName": "Gambeson", "articleGroup": "Jackets", "extraData": [{ "name": "campaign", "value": "Socks 3 for 2" }, { "name": "size", "value": "M" }], "marginPercent": "25.00", "awardsPoints": false, "discounts": [{ "type": "325", "description": "50% autumn discount", "value": -200.00 }] }], "usedRewardVouchers": [{ "voucherNumber": "2342" }], "usedPromotions": [ { "couponId": "272cd4e4-5cb4-472f-8747-a61112083ccb" } ] } ``` In the API v3 `usedRewardVouchers` has replaced `usedBonusChecks` and `voucherNumber` has replaced `checkNumber`. If `contact.matchKeyType` is set to "contactId", then `contact.contactType` is not a required field. If using any other matchKeyType, and contactType is not provided, Customer Engagement will just use whatever your default contact type is (normally Member). A successful POST operation returns a 202 (Accepted). Transactions can also be imported through an XML file format. Read more about in this article, [Receipt XML import](/docs/file-based-transfer/transactions-xml-import). Find the complete list of transaction field definitions [here](/docs/transactions/transaction-field-definitions). In the "usedPromotions" section of the payload, send either ´couponId´ OR ´promotionId´ but not both. Voyado recommends that you use the ´couponId´ as shown here. # Register a return Source: https://developer.voyado.com/docs/transactions/register-a-return In Customer Engagement, a return is viewed as just another transaction, but with a different transactional status. There is no direct connection between a purchase and a return that comes after, it’s considered a new transaction and handled accordingly. Some important points to remember: * The `quantity` field has to be **negative**, reflecting the number of returned items. * The line item `type` needs to have the value “RETURN”. * The `grossPaidPrice` on a return should be sent as a **positive** value of type "decimal". * The `taxAmount` should be sent as a **positive** value of type "decimal". In the same way as for a purchase, the payload is sent to the following endpoint: ```http theme={null} POST /api/v3/receipts ``` ## Example payload This example transaction is a return for a customer with contact type “Member”. Note that `quantity` is negative, the price is positive and the line item type is “RETURN”. ```json theme={null} { "contact": { "matchKey": "1c277c0a-ed93-4928-86cb-b0820090c111", "matchKeyType": "contactId" }, "uniqueReceiptId": "20180909468464496", "receiptNumber": "468464496", "createdDate": "2018-09-09T12:05:23Z", "storeExternalId": "321", "currency": "NOK", "exchangeRateToGroupCurrency": 1.0, "totalGrossPrice": -400.00, "paymentMethods": [{ "type": "card", "description": "Debit Card", "value": -400.00 }], "items": [{ "type": "RETURN", "quantity": -2, "grossPaidPrice": 400.00, "taxAmount": 80.00, "taxPercent": 25.00, "sku": "10015563434", "articleNumber": "2324234", "articleName": "Ipsum Wool Jacket", "articleGroup": "Jackets", "marginPercent": "25.00", "awardsPoints": false }] } ``` ## Return matching logic When a customer returns an item, Customer Engagement needs to match that returned item with the original purchase to display it in the GUI but also to ensure the correct adjustment of points. If the purchase awarded points, for example, the return should then remove them again. The process of return matching involves retrieving potential matches and confirming them like this: 1. Fetch all returned items for a specific import. 2. Attempt to find purchase items that match the returned items by SKU. Or, if an SKU is not found, by `ArticleNumber`. 3. If no matching purchase item can be found, exit the process here. If a matching purchase can be found then update the purchase item in the Customer Engagement database to indicate that it has been returned.  If there are multiple purchases that match the return, the return will be connected to the latest purchase made before the return date. 4. If points are being used, the points total will have to be adjusted. If the price differs between the purchase and the return, the points deduction will be based on the points earned from the purchase, not those calculated from the return price. For example: A purchase with value 299 SEK awards 299 points. A return is made and the matching purchase is located. However the return value, for whatever reason, is 250 SEK. The points removed will be 299 since that's what was originally earned for that purchase. # Transaction field definitions Source: https://developer.voyado.com/docs/transactions/transaction-field-definitions Customer Engagement expects dates and times to be formatted according to [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) with Time Zone Designators. For example: ```bash theme={null} 2021-11-12T10:14:00+01:00 ``` This represents 10:14 on the 12th of November, 2021, in a time zone one hour ahead of [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time). Use local time with an offset, like in the example shown, instead of converting to UTC time. ## Transactions The fields are as follows: A value to match against the specified `customerKeyType` and `contactType`. If a transaction is submitted with a contact match key that does not exist in the system, that transaction will be *skipped* and not imported. What the `matchKeyType` value represents. Options are: contactId, memberNumber, socialSecurityNumber, mobilePhone, externalId, email. If used, matching will only be done for this contactType. If NOT used the default value will be "Member". If a transaction already exists with this `uniqueReceiptId` the new transaction with the same value will be seen as a duplicate and ignored. Datetime when order was first created formatted according to ISO 8601. Example: 2017-09-06T15:55:54+01:00. The external id for the store (usually a numerical code). The currency code according to ISO 4217 for the currency that the customer paid in (local currency). Example: NOK, SEK, EUR. The total price paid by the customer including VAT. A decimal point (.) is always used as decimal separator. If the transaction is of type RETURN then `totalGrossPrice` should be negative. All payment methods used in this transaction. One row per line item in the transaction. The transaction number, does not need to be unique. Currency values for a transaction should be in the local currency. This multiplier is used to convert those to a group currency when storing them for segmentation and reporting. Omitting this will cause problems. One row per used reward voucher. One row per used promotion. While `exchangeRateToGroupCurrency` is not mandatory, it's recommended in order to ensure that currency conversions are correct. If no exchange rate is sent, Customer Engagement will use the fallback conversion set up in the back-end. If `contact.matchKeyType` is set to `contactId`, then `contact.contactType` is not a required field. In this case, if the value is not given, Customer Engagement will just use whatever your default contact type is. ## paymentMethods\[row] A string, for example: “card”, “cash”, “creditcard”, “swish”, “klarna” (max 245 characters). Amount as a decimal. This can't be empty. Descriptive text (max 255 characters). Additional data regarding the specific payment method. This field is accepted in the request but is not stored, processed, or available for segmentation in Customer Engagement. If you need to act on or segment by this information, contact your Voyado team so they can advise on the best approach. ## items\[row] A line item can either be a PURCHASE or a RETURN. Stock keeping unit. Can be the same as the SKU. The name. Number of units sold in this line item. Must be negative for a return. The paid price including VAT for this line item. Note: always withdraw discount values for the item itself or the distributed discount value for a total discount. Read more under Discounts. The VAT paid for this line item. The VAT (moms) percentage for this line item. The group. The margin in percent on this order line. Can't be empty or negative. Note that your Customer Engagement instance needs to be activated for searching on margins. Please contact your Voyado team for configuration requests. Flag that controls if the line item should award points or not when generating reward points. Values are "true" or "false". If not specified, this will default to "true". Key/value pairs for additional data regarding the transactional line item apart from the article meta-data. Could be several sources of discounts for the total discount. One row per discount type. The `grossPaidPrice` value sent to Customer Engagement always reflects the total price paid for a specific item or multiples of that item. So if `quantity` for an item is greater than 1 (or less than -1) then `grossPaidPrice` is the total price of all those items. Or to put it another way, the calculation of (quantity \* item price) is already done. Of course, if `quantity` is 1, then `grossPaidPrice` is just the single item price. For returns, `quantity` is always negative but `grossPaidPrice` is still positive. ## Discounts - items\[row].discounts\[row] Customer Engagement requires discounts to be taken care of before the data is sent. The `grossPaidPrice` should always describe the amount the contact has paid for the item, meaning that the discount values withdrawn with the following patterns: * Full discounts (*e.g. 20% off the whole purchase*) should always be applied for every line item, equally distributed. * Specific transaction item discounts should be withdrawn for the specific line item only. Regardless of the nature of a discount, `item.discount` can be used to tell Customer Engagement what discounts have been used. If a line item is exposed to both a full discount, for instance 20% on a whole purchase, and a 100 SEK off for that article, these are combined to make the total value withdrawn from the original price. These aggregated row values are stored as a parameter for searching in Customer Engagement. Screenshot of Customer Engagement showing how the aggregated row values can be used as a search parameter In Customer Engagement the search locates every transaction connected to a particular contact that contains one or more items that have been exposed to discounts greater/less/equal to a total value or total percentage of the original price. Searching using discounts needs to be activated in your set-up. Please contact your Voyado team about this. Discount value in the local currency. Should always be negative. Searching for discounts in Customer Engagement is possible. Type of discount. A description. ## usedRewardVouchers\[row] The unique ID number of the reward voucher to be redeemed when saving the transaction. ## usedPromotions\[row] The ID for the main promotion. Can be used to link the purchase to a promotion. The ID for the "instance" of the promotion assigned to a contact.\ Don't confuse the `couponId` (for an individual contact) with the `promotionId` (the "template" used).\ Voyado recommends you use `couponId` over `promotionId` if possible, to avoid ambiguity in matching. ## Validations * `discounts.value` should always be negative * A return should have `item.type` "RETURN" and the `quantity` should be negative * A purchase should have `item.type` "PURCHASE" and the `quantity` should be positive * A `quantity` should never be 0 * Sum of all `paymentMethods` should equal `totalGrossPrice` * Sum of all `grossPaidPrice` should equal `totalGrossPrice` * The `taxAmount` should equal `grossPaidPrice` - `grossPaidPrice` / (1 + `taxPercent` / 100). The rounding error precision of this validation is by default 0.01. This value can be configured in the Customer Engagement backend to other values, or also turned off completely. You can download the XSD schema [on the example files page](/docs/file-based-transfer/example-files). # Webhook use cases Source: https://developer.voyado.com/docs/webhooks/webhook-use-cases Here are some examples of working with webhooks in Customer Engagement. A complete list of events and latest payload specification can be found in the client's Svix portal. ## 1 - Contact preferences & consents **Preferences** are a contact's communication channel opt-ins — the acceptsEmail, acceptsPostal, and acceptsSms boolean flags that define how a contact wants to be contacted. **Consents** are formal agreements a contact makes regarding their data or account (e.g., "Do you agree to receive our newsletter?" or membership terms). Once set, a consent can never revert to "Not set." The contact preferences in use in Customer Engagement are: * acceptsEmail * acceptsSms * acceptsPostal Other data regarding a contacts legal settings can be encoded as consent values in Customer Engagement. These are handled for every contact as custom fields on a per brand/client level. To get all consents for your Customer Engagement environment use the following endpoint: ```http theme={null} GET /api/v3/consents ``` As a retailer, you will often need to show the preferences, and sometimes the consent values, for a contact. For example on **My Pages** or at the checkout. And you will need to be sure the values shown are correct. Since Customer Engagement often is the master of these settings, the preferences and consents will either have to be fetched every time they are needed, or you'll need some system in place to **constantly poll Customer Engagement** for the current values. Neither way is efficient. This is why webhooks are a good solution. ### Webhook solution For this situation, you create a webhook for the systems you want to keep updated about contact preference changes to your contacts in Customer Engagement. Then, whenever a preference is changed in Customer Engagement, the webhook will be used to inform all the subscribed external systems, keeping them all in sync with Customer Engagement. The webhooks available for syncing of contact preferences are: Triggered whenever the `acceptsEmail` preference changes Triggered whenever the `acceptsSms` preference changes Triggered whenever the `acceptsPostal` preference changes Customer Engagement also supports a dedicated webhook for changes in consent-typed fields: Triggered whenever a consent value for a contact is changed ```json Payload structure theme={null} { "eventId": "ac01d383-b83f-45b1-a60b-275bc7bba6b2", "eventType": "contact.consent.changed", "isEncrypted": false, "payload": { "consentAlias": "myConsentAlias", "consentValue": true, "contactId": "e5f70fe1-a0cf-4d16-898e-b3d800ff4103", "dateChanged": "2026-01-20T16:28:38.985631+01:00" }, "tenant": "myTenantName" } ``` The webhooks for contact preferences and consents are only triggered for *already existing contacts*. They will not be triggered when a contact is created. Be aware of the [legal requirements](/docs/webhooks/webhooks#legal-requirements) when working with webhooks. ## 2 - Contact changes Many retailers operate several systems that need access to up-to-date customer data, such as e-commerce, POS, CRM, customer service, or data platforms. If Customer Engagement is used as the source of truth for your customer data, contact webhooks can be used to keep those systems synchronized in your system whenever this data changes. The webhooks available for changes to contact data are: Triggered when a contact is created in Customer Engagement. ```json Payload structure theme={null} { "eventId": "4ff4d68b-d299-4a17-81cc-e81abbf8932f", "eventType": "contact.created", "isEncrypted": true, "payload": { "careOf": "c/o John Smith", "city": "Stockholm", "contactId": "515c1b2a-7124-4bb9-826b-aedbbe5da8c4", "contactTypeId": "Member", "countryCode": "SE", "creationTime": "2024-03-15T10:30:15Z", "customAttributes": { "myCustomAttribute": "myCustomAttributeValue" }, "dateOfBirth": "2004-02-17", "email": "john.doe@example.com", "externalId": "CRM-12345", "firstName": "John", "gender": "male", "language": "sv", "lastName": "Doe", "marketingGroupId": "ce62abff-aeb5-45b1-90f0-909194f33c29", "memberNumber": "1234567", "mobilePhone": "+46701234567", "postalCode": "11122", "registrationByStoreId": "560d70fe-7a47-43af-87d4-875afe4f8cc3", "registrationDate": "2024-03-15T10:30:00Z", "registrationSourceType": "API", "registrationSpecificSource": "newsletter-signup", "street": "Storgatan 1", "swedishFinnishPersonalIdentityNumber": "20040217-7935" }, "tenant": "myTenantName" } ``` Triggered when a contact is updated in Customer Engagement. ```json Payload structure theme={null} { "eventId": "4ff4d68b-d299-4a17-81cc-e81abbf8932f", "eventType": "contact.updated", "isEncrypted": true, "payload": { "careOf": "c/o John Smith", "changedValues": [ { "path": "Email", "previousValue": "previousEmail@email.com", "value": "myNewEmail@email.com" } ], "city": "Stockholm", "contactId": "515c1b2a-7124-4bb9-826b-aedbbe5da8c4", "contactTypeId": "Member", "countryCode": "SE", "creationTime": "2024-03-15T10:30:15Z", "customAttributes": { "myCustomAttribute": "myCustomAttributeValue" }, "dateOfBirth": "2004-02-17", "email": "john.doe@example.com", "externalId": "CRM-12345", "firstName": "John", "gender": "male", "language": "sv", "lastName": "Doe", "marketingGroupId": "ce62abff-aeb5-45b1-90f0-909194f33c29", "memberNumber": "1234567", "mobilePhone": "+46701234567", "postalCode": "11122", "registrationByStoreId": "560d70fe-7a47-43af-87d4-875afe4f8cc3", "registrationDate": "2024-03-15T10:30:00Z", "registrationSourceType": "API", "registrationSpecificSource": "newsletter-signup", "street": "Storgatan 1", "swedishFinnishPersonalIdentityNumber": "20040217-7935", "updatedViaSourceType": "API", "updatedWithSpecificSource": "myEcom" }, "tenant": "myTenantName" } ``` Triggered when a contact is deleted in Customer Engagement. ```json Payload structure theme={null} { "eventId": "6a21bf40-56c1-49cf-ae6e-53b987ff2da4", "eventType": "contact.deleted", "isEncrypted": true, "payload": { "contactId": "d3bdcad4-3f90-46ce-be4c-b3d800dc7979", "deletionReason": "cancelled membership" }, "tenant": "myTenantName" } ``` A typical use case for these webhooks is to create, update, or remove customer records in downstream systems whenever the customer profile changes in Customer Engagement. The `contact.updated` event includes delta tracking through changedValues, which helps the system receiving the data to understand what has changed without having to infer it from the full payload. ## 3 - Loyalty There are webhooks available for syncing of points and vouchers in Customer Engagement: Triggered when points are added to a contact via the Customer Engagement UI or through an automation. The transaction is sent in the webhook. This is *only* for customers using the point follower solution. ```json Payload structure theme={null} { "eventType": String, "id": String, "isEncrypted": Boolean, "payload": : { "amount": Decimal, "contactId": String, "pointDefinitionId": Int, "source": String, "description": String, "userId": String, "userName": String, "reason": String, "sourceId": String, }, "tenant": String } ``` Triggered whenever a contact's point balance is changed. ```json Payload structure theme={null} { "eventType": String, "id": String, "isEncrypted": Boolean, "payload": : { "balance": Decimal, "contactId": String, "accountId": Int, "definitionId": String, "balanceExpires": DateTimeOffset }, "tenant": String } ``` Triggered when a new reward voucher is created for a contact. ```json Payload structure theme={null} { "eventId": String, "eventType": String, "isEncrypted": Boolean, "payload": { "id": String, "contactId": String, "validFrom": DateTimeOffset | null, "validTo": DateTimeOffset | null, "currency": String, "uniqueCode": String, "rewardVoucherPayoutId": String | null, "value": Decimal, }, "tenant": String } ``` The `rewardVoucherPayoutId` is the ID of the payout that the voucher was issued from. After you have migrated to the new reward voucher flow, `rewardVoucherPayoutId` will be populated with a value and `currency` and `value` will always reflect the group currency you have configured. If your integration relies on these values, make sure it handles them correctly. In some retail setups, it is Customer Engagement that decides if a customer has been assigned a promotion, while another system is responsible for making that benefit available in checkout, e-com, or POS. This, the multichannel promotion assignment webhook, can be used to notify external systems when a promotion has been assigned, so they can create or expose the corresponding coupon or voucher. This supports event-driven orchestration across channels and helps external systems react when a promotion is assigned in Customer Engagement. The event is intended to be triggered regardless of where the assignment originated: via the UI, an automation activity, or over the API. ```json Payload example theme={null} { "eventId": "171c62d9-1007-4453-ab0b-e5bda4ace6e2", "eventType": "promotion.multiChannel.assigned", "isEncrypted": false, "payload": { "contactId": "a0e0a130-9fd1-4180-9f2e-0d38b1c2a2e4", "promotionAssignmentId": "7e5866cb-441f-433e-87c7-048bf8c9e41d", "promotionId": "d22615d8-dd23-4b40-832b-3fbafb4f2fbf", "timestamp": "2026-07-15T14:06:47.301Z" }, "tenant": "test-tenant" } ``` # Introduction to webhooks Source: https://developer.voyado.com/docs/webhooks/webhooks A webhook can be seen as a reverse API. Instead of a user polling an API endpoint in a system (Customer Engagement, for example) to check for new data, the user can instead configure an URL in that system that can be called only when there is new data. This, essentially, is a webhook. Structurally, a webhook is just a HTTP POST to an open API. They are simpler than an API endpoint, but have less functionality. Webhooks, for example, don't allow pushing, deleting or updating data in another system, only receiving data. When you're only sending *specific data occasionally* webhooks might be a good solution. You only receive data over a webhook when an event occurs. So if the other system doesn't call your webhook with data you can't be sure if it's because no events are occurring, or if the other system is down. Keep this in mind. The advantages of webhooks are therefore: * A simpler integration * Less data being passed * No polling needed * Updates only sent when changes happen ## Webhooks currently available Currently these webhooks can be used in Customer Engagement: This webhook with the ID `contact.acceptsEmail.changed` is triggered whenever the `acceptsEmail` preference changes for an already existing contact. This webhook with the ID `contact.acceptsSms.changed` is triggered whenever the `acceptsSms` preference changes for an already existing contact. This webhook with the ID `contact.acceptsPostal.changed` is triggered whenever the `acceptsPostal` preference changes for an already existing contact. The webhook with the ID `contact.consent.changed` is triggered for changes in consent-typed fields on a contact. This webhook with the ID `loyalty.addPoints` is triggered whenever points are added to a contact via the Customer Engagement UI or through an automation. The transaction is sent in the webhook. This is used *only* for customers using the point follower solution. This webhook with the ID `point.balance.updated` is triggered whenever a contact's point balance has changed. This webhook with the ID `reward.voucher.created` is triggered whenever a reward voucher is created for a contact. This webhook with the ID `promotion.multiChannel.assigned` is triggered when a multichannel promotion is assigned to a contact in Customer Engagement. This webhook with the ID `contact.created` is triggered whenever a new contact is **created** in Customer Engagement. This webhook with the ID `contact.updated` is triggered whenever a new contact is **updated** in Customer Engagement. This webhook with the ID `contact.deleted` is triggered whenever a new contact is **deleted** in Customer Engagement. Read more about how each webhook event can actually be used in the use-cases article: Even though you are free to configure subscriptions to all the available events, you'll still need to reach out to your Voyado Account Manager or Voyado support to activate the events that you want to subscribe to, so that the data actually gets sent from Customer Engagement. ## Requirements Here are the various requirements for working with webhooks in Customer Engagement. ### Basic client requirements To receive webhooks from Customer Engagement you will need to develop and deploy a web service. This is basically an API with a POST endpoint. It's common to use one service for all webhooks, and then distribute the incoming events internally to your different systems. When developing your webhooks, a service like [Svix Play](https://www.svix.com/play) or [ngrok](https://ngrok.com/) can be used to receive webhooks from the internet on your local development machine. Your web service must be accessible over the internet. Fixed source IP addresses can be used if your endpoint is behind a firewall or NAT. There should be no authentication at the receiving endpoint. Instead, you'll verify that the events received are sent from Voyado Customer Engagement using signatures and a shared signing secret. The service you provide must be able to scale with the load. Voyado's webhooks service provider Svix has good documentation on how to consume webhooks. ### Technical client requirements You, the client, must develop functionality to verify the webhooks are from the correct sender. Voyado's webhooks service (Svix) has SDKs and libraries to handle most of what is needed. Libraries are available in Go, Python, Typescript/Javascript, Java, Kotlin, Ruby, C# (dotnet), Rust and PHP. You can see source IP addresses [here](https://docs.svix.com/receiving/source-ips). This integration uses the EU region. It's possible to add custom headers to webhooks and also rate limiting, if needed. If you want to use the optional feature of end-to-end encryption, you must develop functionality to decrypt the payload data (in the same way as with soft identification. ### Legal requirements When you are implementing webhooks in Customer Engagement and move to a production environment, you'll need to approve Svix (Voyado's webhooks provider) as a GDPR sub-processor. ### Security considerations The security involved in using the webhooks solution can be seen as many nested layers. 1. **The webhook itself**: No security. 2. **Signing the webhook message** with Signing Secret: The recipient can use this to verify that the webhook comes from Voyado. The Signing Secret is found in the portal (meaning that Svix controls it). The client can rotate the key whenever they want. 3. **IP lock**: By using this, messages can only be accepted from specific IP addresses. 4. **Payload encryption**: All data from Customer Engagement is encrypted with a key that only Voyado and the client know. It is configured in Customer Engagement, meaning that Svix cannot read any of the data that passes through it. While it is the client themselves who decides how many layers to implement, layers 1 and 2 above are the absolute minimum. ## Setting up a webhook in Customer Engagement Once you have created your webhooks, you'll need to set them up in Customer Engagement. The steps are: Contact your Voyado account manager to enable webhooks in your Customer Engagement instance. Depending on your agreement, a supplement might be required. The webhooks module must then be activated in the Customer Engagement back-end, and the correct permissions granted to users. Now you'll configure the client endpoint in the Customer Engagement UI. Go to **Administration/Webhooks Dashboard** to see the Webhooks already set up. Then select **+ Add Endpoint** to add another. Webhooks already set up After adding your endpoint, select it and then choose which event you want to send to it: Choose event to send Get the Signing Secret used for verification, as well as the Payload Encryption Key (if you are using it) from your Voyado account manager. When sending these to somebody else, be careful to use the same trusted method as when sharing API-keys. The signing secret is created and handled by Svix, and can be rotated if needed. Choose event to send You can use the “Testing” functionality in Svix to send test-events to the endpoint. Check the log for errors. If you need it, configure a rate limit so the client endpoint can handle the load. You can also choose to configure a custom header with some kind of token to further increase security. ## Disabling of failing endpoints If all attempts to a specific endpoint fail for a period of 5 days, the endpoint will be disabled and an operational webhook (EndpointDisabledEvent) will be sent. For example, if your receiving service responds with 404 for whatever reason, such as as a contact not being present, over a 5 day period, this behaviour is triggered and the endpoint will be disabled. See more details at the Svix docs: # App push Source: https://developer.voyado.com/docs/app-push/app-push Voyado's App Push API enables your mobile app to send native push notifications (APNs on iOS, FCM on Android) to your customer's mobile device through Customer Engagement. It also reports on which devices your customers are using, and lets your app report back on what happened to a push after it was sent, whether it was delivered, opened or dismissed. That engagement can then be used in automations and shown in analytics. Here you will learn how to integrate with the App Push API. This is intended for: * **Mobile engineers** (iOS or Android) integrating push notifications into an app * **Backend engineers** building a server that proxies push registration for a mobile app ## Base path Each endpoint in the App Push API uses a *base path* URL which is unique to your Customer Engagement environment. The value you'll use can be found on Config Hub. Your base path URL will be referred to as `` from this point on. ## Authentication Every call to the App Push API requires a Bearer token: ```http Authorization theme={null} Authorization: Bearer ``` Identifies your app to Customer Engagement. Passed as a Bearer token. You will set this in [Config Hub](https://help.engage.voyado.com/hc/en-gb/sections/21844508239004-Config-Hub) as part of configuring push settings. The `PublicApiKey` is best thought of as an identifier for your app, and not a secret to be protected. It ships inside your compiled app binary, so it can be found by decompiling your IPA/APK. But that is fine - the key is write-only and scoped to registering devices and reporting events for one specific app configuration. It cannot read, modify, or delete any other tenant's or app's data. Treat it how you'd treat any other client-visible app identifier. If you'd rather avoid shipping this identifier inside your app at all, see [Backend-mediated integration](#backend-mediated-integration) below. ## The endpoints There are three endpoints and you use your base path to build the URLs like this: | Endpoint | POST request URL | | :- | :- | | Register | `POST /installations` | | Unregister | `POST /installations/unregister` | | Event | `POST /events` | In each case, insert your own version of the base path. Here are the endpoints in more detail: Used to register a device to a certain contact or update its record. The JSON payload contains the attributes: * `platform` * `push_token` * `contact_id` * `app_version?` * `os_version?` * `device_model?` * `locale?` * `timezone?` ```bash Example call theme={null} # Register curl -X POST /installations \ -H "Authorization: Bearer $PUBLIC_API_KEY" -H "Content-Type: application/json" \ -d '{"platform":"ios","push_token":"","contact_id":""}' ``` Used to stop a certain device from receiving pushes, for example on logout or opt-out. The JSON payload contains the attributes: * `push_token` ```bash Example call theme={null} # Unregister (logout) curl -X POST /installations/unregister \ -H "Authorization: Bearer $PUBLIC_API_KEY" -H "Content-Type: application/json" \ -d '{"push_token":""}' ``` Used to report what happened when a push arrived or was interacted with. Sent as an "events" array of JSON objects: * `type` * `message_id` * `contact_id` * `recipient_id` * `occurred_at` * `app_state?` * `deeplink?` * `action_id?` ```bash Example call theme={null} # Report an opened notification curl -X POST /events \ -H "Authorization: Bearer $PUBLIC_API_KEY" -H "Content-Type: application/json" \ -d '{"events":[{"type":"opened","message_id":"","contact_id":"","recipient_id":"","occurred_at":"2026-07-02T10:20:42Z"}]}' ``` ### Failure modes | Situation | Response | | :- | :- | | Missing or malformed `Authorization` header | 401 unauthorized | | Key doesn't match the tenant/app config | 401 unauthorized | | Invalid value of base-path | 400 invalid\_path | ## Core concepts Here are some core concepts for working with the App Push API. ### 1. An installation is a device An "installation" is a single app install on a single device. It is identified solely by its push token (the APNs device token or FCM registration token). If the push token changes, the installation should be updated. ### 2. Binding a contact The `contact_id` is the existing Customer Engagement contact identifier for the signed-in user. Registering an installation with a `contact_id` *binds* that device to that contact. Customer Engagement can then send targeted pushes to it. ### 3. Registration is idempotent Calling the register endpoint with the same push token twice doesn't create a duplicate, it just updates the existing record (for example by giving a new `contact_id` or refreshed device metadata). ## Integration guidance This section describes *how* to use the API. These are the patterns every integration should follow. ### 1. Authenticate first Only call the register endpoint once you know, with certainty, who the current user is — that is, you have a valid `contact_id` from your own app's authentication/session system. Voyado does not know or derive your contact identity; your app must supply it. Registering a push token using the wrong (or a stale) `contact_id` means personalized pushes could go to the wrong person, so this ordering matters: confirm identity first, then register. ### 2. Register early Register the device as early as possible in your app's lifecycle but only when *both* of these conditions are met: You have their `contact_id` from your own authentication system. This condition is implicit: you cannot obtain a push token from the OS at all until the user has granted notification permission, so simply having a token in hand already satisfies this. In practice this usually means to *request notification permission* at an appropriate point in your onboarding flow, and as soon as you have both a push token and a signed-in user, *call register*. For most apps that's at app launch, if already logged in or immediately after login. ### 3. Minimize redundant calls Registration is idempotent and safe to call repeatedly, but you should still avoid calling it more than necessary. So you should: * **Cache** the last successfully registered `(push_token, contact_id)` pair locally on the device * **Only call register again** when either value changes, such as when the push token rotates, the user logs in as a different contact, or you haven't registered yet at all. * Don't call register "just in case" on every single app launch if neither value has changed since your last successful call. Doing this will not break anything, but it creates unnecessary network traffic and battery cost. ### 4. Unregister on logout When the user logs out, call the unregister endpoint along with the device's current push token: ```http theme={null} POST /installations/unregister ``` This is the recommended way to fully remove the installation from Customer Engagement. If the same device is used to log back in later (by the same or a different contact), your app should call the *register* endpoint again with the current push token and the newly authenticated `contact_id`. As in, a completely fresh registration. Also call the unregister endpoint whenever a user explicitly opts out of push notifications in your app's settings, or when you detect that OS-level notification permission has been revoked. ## Endpoints in detail Here is a more detailed run-down of the three endpoints mentioned above and how they are used. Important points: * All requests and responses attribute names use JSON with *snake\_case* field names. * All endpoints use the same base path and require the `Authorization: Bearer ` header ### Register installation The endpoint is: ```http Register endpoint theme={null} POST /installations ``` This creates or updates an installation. This call is idempotent, which means it is safe to call every time your app has a current push token and a signed-in contact, without checking whether the device is already registered. The following attributes can be used: This will be "ios" or "android". The APNs device token or FCM registration token. This is the installation's identity. Your Voyado contact identifier for the signed-in user. Your app's version string. (See note below.) Device OS version. (See note below.) Device model identifier. (See note below.) Device locale, for example `en-US`. (See note below.) IANA timezone, for example `Europe/Stockholm`. (See note below.) `app_version`, `os_version`, `device_model`, `locale`, and `timezone` are metadata that Voyado does not currently use for any targeting, filtering, or business logic. It is recommended to send them anyway as they are likely to power future capabilities (such as minimum-OS-version rules or locale-aware send scheduling), and it costs you nothing to include them now rather than adding them retroactively later. This is an example request for tenant "acme" (using a likely value for ``): ```Http Request details theme={null} POST https://app-push-prod.voyado.com/api/v1/acme/abc123/default/installations Authorization: Bearer Content-Type: application/json ``` The payload: ```json Registering an installation theme={null} { "platform": "ios", "push_token": "a1b2c3d4e5f6...", "contact_id": "496977ad-1d09-4993-9957-ef019eaa1eb3", "app_version": "3.4.1", "os_version": "26.4.4", "device_model": "iPhone15,2", "locale": "sv-SE", "timezone": "Europe/Stockholm" } ``` A successful request gives a *HTTP 202 Accepted* response with an empty body. | Status | Code | When | | :- | :- | :- | | `400` | `invalid_json` | Malformed JSON body. | | `400` | `invalid_request` | Missing `push_token` or `platform`. | | `400` | `invalid_platform` | `platform` is not "ios" or "android". | | `401` | `unauthorized` | Missing/invalid Bearer token. | ### Unregister installation The endpoint is: ```http Register endpoint theme={null} POST /installations/unregister ``` This entirely removes the installation identified by the push token sent. Use this on logout and when the user explicit opts out. The push token is sent *in the request body*, not in the URL. A push token is a sensitive value and should never appear in a URL path, since URLs are logged by proxies, browsers, and monitoring tools far more readily than request bodies. The push token identifying the installation that is to be removed. ```http Request details theme={null} POST https://app-push-prod.voyado.com/api/v1/acme/abc123/default/installations/unregister Authorization: Bearer Content-Type: application/json ``` The payload: ```json Registering an installation theme={null} { "push_token": "a1b2c3d4e5f6..." } ``` A successful request gives a *HTTP 202 Accepted* response with an empty body. | Status | Code | When | | :- | :- | :- | | `400` | `invalid_json` | Malformed JSON body. | | `400` | `invalid_request` | Missing `push_token`. | | `401` | `unauthorized` | Missing/invalid Bearer token. | ### Report events An engagement events describes what happened to a push after it was sent. The endpoint to send one or a batch of events: ```http Report events endpoint theme={null} POST /events ``` Events are validated as a *whole batch* before any of them are accepted. If any event in the batch is invalid, the whole batch is rejected. ```json Events request body theme={null} { "events": [ { /* event */ }, { /* event */ } ] } ``` One of `delivered` or `opened` or `dismissed` - See "Event types" below for more details. Echoed back from the push payload your app received. Echoed back from the push payload your app received. When the event happened, from the device's clock. ISO-8601 format. The Voyado contact this event relates to. This will be "foreground", "background", or "killed", which is the app's state when the event occurred. Echoed from the push payload, if the notification carried one and the user tapped it. The identifier of a custom notification action button the user tapped, if the notification included custom actions and tapping one opened the app. Reported alongside `opened`. | Type | Fire when: | | :- | :- | | `delivered` | The push arrived on the device (before the user does anything with it). | | `opened` | The user tapped the notification (or a custom action button that opens the app). | | `dismissed` | The user dismissed the notification without opening it (Android only — iOS has no reliable equivalent). | Every push Voyado sends includes both a `message_id` and `recipient_id` in its payload specifically so your app can echo it back on every event. This is how Voyado correlates "delivered" and "opened" events with the original send. Consult your platform's push payload documentation for exactly where this value appears in the payload your app receives. ```http Reporting a batch of events theme={null} POST https://app-push-prod.voyado.com/api/v1/acme/abc123/default/events Authorization: Bearer Content-Type: application/json ``` The payload: ```json The events theme={null} { "events": [ { "type": "delivered", "message_id": "msg-abc123", "contact_id": "496977ad-1d09-4993-9957-ef019eaa1eb3", "recipient_id": "572c44be-945d-879-41a2-77826b6bb21b", "occurred_at": "2026-07-02T10:20:15.123Z", "app_state": "background" }, { "type": "opened", "message_id": "msg-abc123", "contact_id": "496977ad-1d09-4993-9957-ef019eaa1eb3", "recipient_id": "572c44be-945d-879-41a2-77826b6bb21b", "occurred_at": "2026-07-02T10:20:42.456Z", "deeplink": "myapp://product/sku-123" } ] } ``` A successful request gives a *HTTP 202 Accepted* response: ```json theme={null} { "accepted": 2 } ``` | Status | Code | When | | :- | :- | :- | | `400` | `invalid_json` | Malformed JSON body. | | `400` | `invalid_request` | Empty or missing `events` array. | | `400` | `invalid_event` | An event is `null`, or missing `message_id` / `occurred_at` / `contact_id`. | | `400` | `invalid_event_type` | `type` is not one of the event types above. | | `401` | `unauthorized` | Missing/invalid Bearer token. | Every error from every endpoint uses the same envelope: ```json Error response theme={null} { "error": { "code": "invalid_request", "message": "platform and push_token are required", "request_id": "0f1e2d3c-..." } } ``` The `request_id` value uniquely identifies the request server-side; include it when contacting support about a specific failed call. What the delivered event is designed to mean is that your app push has “reached" the user, and that it is now possible for them to see it (think of an email reaching an inbox). However the notification permissions on a user’s device can vary wildly. For example, a user may allow pushes but choose to show them not as a banner but as a red notification badge on the app icon. Therefore it is hard to give exact guidance on when you should send the “delivered” event and it is up to you to implement it in a way that works for you. ## Sending a push Here are example payloads for sending a push on Android and on iOS. ```json theme={null} { "voyado_push_service_message_id": "3f2c9b1e-8a4d-4e77-9c2a-1d7b6f0a5e3c", "voyado_push_service_contact_id": "322c4bbe-9ccd-3e79-91aa-17826b6bb21a", "voyado_push_service_recipient_id": "572c44be-945d-879-41a2-77826b6bb21b", "body": "20% off everything this weekend only!", "title": "Weekend sale", "voyado_push_service_deeplink": "https://shop.example.com/campaigns/weekend-sale", "voyado_push_service_image_url": "https://cdn.example.com/campaigns/weekend-sale.png" } ``` ```json theme={null} { "aps": { "alert": { "title": "Weekend sale", "body": "20% off everything this weekend only!" }, "sound": "default", "mutable-content": 1 }, "voyado_push_service": { "message_id": "3f2c9b1e-8a4d-4e77-9c2a-1d7b6f0a5e3c", "contact_id": "322c4bbe-9ccd-3e79-91aa-17826b6bb21a", "recipient_id": "572c44be-945d-879-41a2-77826b6bb21b", "deeplink": "https://shop.example.com/campaigns/weekend-sale", "image_url": "https://cdn.example.com/campaigns/weekend-sale.png" } } ``` ## Migrating from another provider If you're currently sending push notifications through a different push provider and want to move to Voyado, it's recommended to follow a *gradual, dual-provider transition* rather than a sudden total cut-over. This works best when your app's implementation can actually support running two providers side by side. So check that first. Some existing push SDKs register their own OS-level callbacks (such as `UNUserNotificationCenterDelegate` on iOS or `FirebaseMessagingService` on Android) and don't forward those callbacks anywhere your own code can intercept them, which can make it difficult or impossible to also wire up Voyado registration and event reporting alongside them. Only your app's developers can assess whether your current setup allows for this. If it doesn't, talk to your Voyado contact about alternative migration approaches. Add Voyado registration (this API) to your app alongside your existing provider's SDK. Your app now registers with *both* providers whenever a user authenticates. As your existing users open the app over the following days and weeks, each one registers with Voyado automatically — no forced re-login, no push notification required to trigger it, no big migration event. Coverage grows organically with normal app usage. How many registered contacts is "enough" to switch is entirely a business decision on your side — Voyado doesn't enforce or suggest a threshold. Once you're satisfied with coverage, you cut over. Registering a device with Voyado never causes a push to be sent, this must still be explicitly done. This means that running both providers' registration side by side during the transition does *not* risk duplicate pushes on its own. Do this when you're fully cut over and confident that you no longer need the legacy SDK. You should not send pushes from both the old provider and from Voyado at the same time during the transition. Doing this creates a risk of sending the push twice. ## Backend-mediated integration Not every customer wants their mobile app talking to Voyado directly. If you'd rather route push registration and event reporting through your own backend, that's fully supported. In this case your backend just calls the same API as discussed above on your app's behalf. ```mermaid theme={null} flowchart LR A["📱 Mobile app"] -->|"registers device"| B["⚙️ Your backend"] B -->|"App Push API"| C["🔔 Voyado App Push API"] subgraph yours ["You build this"] A B end subgraph voyado ["Voyado provides this"] C end style A fill:#f0f4ff,stroke:#4a6cf7,stroke-width:2px style B fill:#f0f4ff,stroke:#4a6cf7,stroke-width:2px style C fill:#fff4ed,stroke:#f77f4a,stroke-width:2px ``` Important points to consider are: * Your backend calls **the exact same Voyado endpoints** (register, unregister, events) as those described above, with the exact same fields. * **Your backend can supply `contact_id` itself** when calling Voyado, since it already knows which of your own users is making the request (via your existing session/auth system), Your mobile app only needs to hand its push token to your backend, and not to Voyado directly. * The key **never ships inside your distributed app binary** where it could be extractable by unpacking the IPA/APK. This is a meaningful security advantage. If keeping the key off-device matters to you, this pattern is worth the extra implementation effort. Everything else in this guide — when to register, when to unregister, the migration approach — applies in the backend-mediated case. It's only *who* makes the HTTP call to Voyado that changes. # Bounced and unsubscribed Source: https://developer.voyado.com/docs/campaigns/bounced-and-unsubscribed Two important concepts when dealing with send-outs in Customer Engagement are *bounced* and *unsubscribed*. ## Bounced When an email or SMS sent from Customer Engagement can't be delivered, this is known as a *bounce*. There are two kinds of bounces, with different consequences in Customer Engagement: * A **soft bounce** means the email or SMS can't be delivered. This can happen because the user's inbox is full, or the mail server has temporary issues. When the next send-out is done from Customer Engagement, addresses / mobile numbers that have soft bounced will be tried again. If an email address soft bounces 10 consecutive times, Customer Engagement will block it for further communication (although this is not the case for SMS). * A **hard bounce** means that the ISP (for email) or the mobile carrier (for SMS) is permanently not reachable. This might happen because the domain name doesn’t exist, the recipient is unknown, or the phone number is no longer active. Customer Engagement will deactivate an email address / mobile number that has hard bounced, which will then be shown in red and struck through on the contact card. ## Unsubscribed An email address or mobile number is considered unsubscribed for a contact when the contact owning it has actively opted out of receiving marketing information. Their email / phone number is probably still valid, but the contact does not want it used for marketing send-outs. ## Starting up with Customer Engagement When you're moving to Customer Engagement from another platform it's important to realize that Customer Engagement will *not* automatically get updated about historical email bounces and unsubscribed contacts. Therefore you'll need to manually update Customer Engagement with the emails and mobile numbers that should not be used for send-outs. Doing this will ensure you don’t send communication to old and inactive addresses, since that can be devastating to your sender reputation and risk flagging you as a spammer, which you don't want. While it is possible to have the same email / phone number attached to several contacts, each email or phone number can only be used once in the same send-out. So if two contacts share an email (unusual, but it can happen) and are in the same send-out, only one will be mailed. ## Bounced and unsubscribed lists Updating Customer Engagement with hard bounced and unsubscribed emails / phone numbers is done using a bounce list, which is a CSV file with a single row containing the emails that Customer Engagement can’t communicate with. These lists can be uploaded in the Config Hub. Once imported, hard-bounced emails are then stored in the database to make sure they are never used in email send-outs from Customer Engagement. Read more about this in the Help Center: Email addresses and phone numbers imported via a bounce list will be blocked for future send-outs. This applies both to contacts who currently exist in Customer Engagement with one of those emails / phone numbers, as well as future contacts created with one of these as their identifier. # Email content population Source: https://developer.voyado.com/docs/campaigns/email-content-population In emails created in Customer Engagement, it's possible to add content that has been fetched from an external source. This requires the use of specific email modules that match the specific type of content. The most common use case is populating product modules from a XML product feed (the Google format is recommended). Content can also be fetched from an external API endpoint. Using an XML product feed is easy as long as you've already set up a suitable feed. Talk to your contact at Voyado about setting up this integration. Your product feed need to be accessible through an open URL so that Customer Engagement can fetch it. Your product feed can't be on a file or located in an FTP folder; it *must* be located on an open HTTP endpoint. ## Content from API endpoint The information below covers the classic email editor. There are also dedicated articles about working with external / custom data in the new email Design Studio. Customer Engagement has functionality to fetch data from external platforms in order to populate emails. Here's more detail about how that works. The data model presented below is fixed and designed for product information but can also be used for populating emails with, for example, related products, campaign information or other types of information from external systems. ### Fields for classic email editor Even though it's possible to have any field in the API response, the ones defined below should always be included when using Customer Engagement's standard email modules for products. The images returned in the response will we displayed as-is in the outgoing emails. Hence, the format of the image (the size and aspect ratio) needs to match the format given in the mail template module to ensure they look how you want them to look The image format should be decided together with the person responsible for your email template as well as your technical application specialist at Voyado. | Field name | Example value | Instructions | | - | - | - | | productNumber | 23874625 | | | description | Superproduct 2000 Ultra, matte black | | | discountPrice | 1 549 kr | | | imageUrl | [https://www.customerdomain.com/product/1234.jpg](https://www.customerdomain.com/product/1234.jpg) | | | link | [https://www.customerdomain.com/product/1234](https://www.customerdomain.com/product/1234) | | | name | Superproduct 2000 Ultra | | | originalPrice | 1 699 kr | OriginalPrice should ONLY be used when product is on sale | | price | 1 699 kr | | | fromPrice | 899 kr | | | discountPrice | 1099 kr | | | sellText | Looking for Superproducts latest groundbreaking innovation? With the 2000 Ultra Superproduct has done it again. The 2000 Ultra offers… | | This is only relevant for the classic email editor and not for Design Studio. ### Request URL Your custom API endpoint should be able to handle a search query with multiple product IDs that are comma separated, like in this example. Besides this requirement, the actual URL can be determined by you. ```http theme={null} GET https://[customerdomain].com/api/endpoint/exampleID1,exampleID2,exampleID3 ``` It is also possible to add several extra parameters to personalise the URL. The examples below use a parameter called locale which enables you to dynamically call for an endpoint connected to a specific country/language. ```http theme={null} GET https://[customerdomain].com/api/{locale}/endpoint/ ``` Or you could also do it this way: ```http theme={null} GET https://[customerdomain].com/api?locale={locale}&id=exampleID1,exampleID2 ``` These extra parameters are decided per project and configured by a product specialist at Voyado. ### Response JSON This is an example of how a response should look like to match the Customer Engagement format: ```json theme={null} [ { "productNumber": "12334", "description": "Superproduct 2000 Ultra, matte black", "imageUrl": "https://customerimages.imgix.net/71303.jpg", "originalPrice": "1 699 kr", "discountPrice": "1 549 kr", "discount": "- 9 %", "link": "https://www.customerdomain.com/product/71303", "fromPrice": "1 699 kr", "name": "Superproduct 2000 Ultra", "price": "1 549 kr", "sellText": "Looking for Superproducts latest groundbreaking innovation? With the 2000 Ultra Superproduct has done it again. The 2000 Ultra offers..." }, { "productNumber": "12335", "description": "Superproduct 2003 Ultra, matte white", "imageUrl": "https://customerimages.imgix.net/product/71304.jpg", "originalPrice": "1 699 kr", "discountPrice": "1 549 kr", "discount": "- 9 %", "link": "https://www.customerdomain.com/product/71304", "fromPrice": "1 699 kr", "name": "Superproduct 2000 Ultra", "price": "1 549 kr", "sellText": "Looking for Superproducts latest groundbreaking innovation? With the 2000 Ultra Superproduct has done it again. The 2000 Ultra offers..." } ] ``` ## QR and barcodes in emails Voyado has an online tool for generating QR and barcode images which can then be sent out in Customer Engagement emails. Such an image could encode something personal like a member number (for easier identification), a personal reward voucher number or an order number. These images could then be used by a POS to easily read that information from a customer’s mobile phone screen. # Email deliverability Source: https://developer.voyado.com/docs/campaigns/email-deliverability Email deliverability is one of the most important factors for good inbox placement and to avoid unnecessary spam-related issues. To ensure your setup is properly configured for your emails send-outs, there are some important setup and preparations that must be done. All the steps below should be considered **mandatory** to ensure the best possible email deliverability. ## Step 1 - The course “Introduction to Email Deliverability” This online training course will give you the basics of emails deliverability and is strongly recommended. It serves as a foundation for your warmup phase before launch and shows you how to keep in the loop after the launch, helping you monitor and act when needed. ## Step 2 - Authenticate your sender domains Follow this guide to ensure that you have authenticated the sender domains you'll be using to send out emails from Customer Engagement. As well as authenticating the sender domains, don't forget to: * Have a DMARC-policy in place * Set up postmaster@ and abuse@ addresses for your sender domain ## Step 3 - Plan your email warmup You've most likely been sending mails from your sender domain and sender address for some time now. This means that your sender domain has an established sender reputation. As you are switching to a new platform (Customer Engagement), you'll need to warm up your sender reputation again. Follow the warmup processes outlined here to retain a good sender reputation. The warmup process can be done in different ways, and how you do it depends on your database volume and your sending pattern: * If you send **less than one million emails per month**, you will benefit from sending from Voyado's Shared IP-Pool. You will be assigned to the IP-pool automatically. * If you send **more than one million emails per month**, you will be assigned a dedicated IP-address that only you will send from. ## Warmup planning - low volume case This is for when your monthly send-out volume is *less than* a million emails per month, and your contact database has *less than* 80 000 contacts with email marketing consent. In this case, the recommended new contact allowance per week are: **Table 0: Max number of new contacts per week** | Week | Max number of new contacts mailed from Customer Engagement | | - | - | | Week 1 | 15 000 | | Week 2 | 30 000 | | Week 3 | Send to all | This table above shows the *recommended* maximum number of new contacts you can mail from Customer Engagement each week. For the recommended maximum send-out frequency per week, see the section “Suggestions for the warmup period” at the bottom of this article. ## Warmup planning - high volume case This case is for when your monthly send-out volume is *more than* a million emails per month, or your contact database has *more than* 80 000 contacts with email marketing consent. For this case, we suggest two different variants: “Activity based warmup” and “Randomized split warmup”. Read through both variants to decide which version suits your organization the best. If possible, and you have the relevant data, Voyado recommends “Activity based warmup”. ## Activity based warmup To ensure you have the highest possible engagement level, and that you send to the best possible addresses, it is recommended to divide up your send-out groups and send-out volumes per week according to the tables below: **Table 1: Sendout groups** | Sendout group | When contacts last opened an email | | - | - | | Warmup Group 1 | 0 - 7 days | | Warmup Group 2 | 8 - 14 days | | Warmup Group 3 | 15 - 30 days | | Warmup Group 4 | 31 - 60 days | | Warmup Group 5 | 61 - 90 days | | Warmup Group 6 | 91 - 120 days | | Warmup Group 7 | 121 - 150 days | | Warmup Group 8 | 151 - 180 days | | Warmup Group non-active | 181 days and higher | **Table 2: New contact allowance per week** | Week | Max number of new contacts mailed | | - | - | | Week 1 | 25 000 | | Week 2 | 50 000 | | Week 3 | 75 000 | | Week 4 | 100 000 | | Week 5 | 125 000 | | Week 6 | 125 000 | | Week 7 and every week after | 100 000 | This table shows the recommended maximum number of new contacts you should mail per week from Customer Engagement during the warmup. Voyado recommends that you work with the so-called "Primary Warmup” and “Secondary Warmup”. See the following sections for how these work. ## The primary warmup Here you will focus on warmup groups 1-8. Start with warmup group 1 and add more and more volume as you get further into the warmup period. The weekly sending allowance is stated in the table “Recommended new contact allowance per week” below. Voyado strongly suggests that you follow this for optimal deliverability and proper warmup of your IP-address. **Table 3: Sendout structure** | Warmup Group | When contacts last opened an email | Amount | | - | - | - | | Warmup Group 1 | 0 - 7 days | 32 000 | | Warmup Group 2 | 8 - 14 days | 42 000 | | Warmup Group X | ... | ... | | Week | Warmup Group | Amount | | - | - | - | | Week 1 | Warmup Group 1 | 25 000 | | Week 2 | Warmup Group 1 (7 000) + Warmup Group 2 (42 000) | 49 000 | | Week X | ... | ... | ## The secondary warmup Recipients who haven't opened any email in the last six months are a potential risk for your email deliverability. It could even be argued that inactivity over the previous 90 days is enough to be a potential risk. This all depends on how often you send and how you segment your send-outs. For many businesses, however, even 180 days of inactivity may not be long enough to merit complete exclusion from future email send-outs. The recipients, for example, may be interacting in physical stores but might not be so active in the email channel. Or they may be “seasonal customers”. How then should this be handled? It's all about balance and planning. Which takes us to the secondary warmup group. The idea behind this is to gradually move contacts in and continuously monitor the effect of each group. Some suggestions to optimize this are: * Before you start sending to this group, consider if you might benefit from a re-activation program for these contacts, such as giving some incitement for inactive contacts to re-engage in your email program. You could, for example, use an automation in Customer Engagement. If you choose to start sending to these contacts, our recommendation is that you add them in small groups (maximum of 5% new inactive contacts per week). This will give you good control in understanding how these contacts are performing. * Even after re-engagement activities, you'll most likely still have some inactive recipients. Just as above, ff you choose to start sending to these contacts, the recommendation is that you add them in small groups (maximum 5% new inactive contacts per week). * Even after these steps, you might still have contacts that are not re-activated. If these contacts are still eligible for communication from you, try alternative channels. Maybe SMS, for example, might be a better communications channel for them. ## Sendout groups and new contact allowance per week (randomized split) If you don't have access to "Last Opened Date" for each recipient, consider splitting the list up randomly and sending according to the recommended send-out volumes below. With this approach, you'll send emails to both your active and inactive contacts from the beginning. It is therefore important to have a slower and more careful pace. **Table 4: Recommended new contact allowance per week** | Week | Max number of new contacts mailed from Customer Engagement | | - | - | | Week 1 | 10 000 | | Week 2 | 20 000 | | Week 3 | 40 000 | | Week 4 | 60 000 | | Week 5 | 80 000 | | Week 6 | 100 000 | | Week 7 and on | 80 000 | The table above shows the recommended maximum number of new contacts you should send to form Customer Engagement each week. For the recommended maximum sendout frequency per week, consult the section “Suggestions for the warmup period" at the bottom of this article. ## Use your current platform in parallel with Customer Engagement to maintain your reach As you only have a limited send-out volume in Customer Engagement during the warmup period, it's likely you'll want to reach out to the rest of the groups from your current platform. This is perfectly fine to do. This is how a split between the platforms can look: **Table 5: Example 1 - Shared IP Pool (65 000 contacts)** | Week | Platform | Contacts | | - | - | - | | Week 1 | New contacts emailed from Customer Engagement | 25 000 | | Week 1 | Contacts emailed from your current platform | 40 000 | | Week 2 | New contacts emailed from Customer Engagement | 40 000 | | Week 2 | Contacts emailed from your current platform | 0 | **Table 6\_ Example 2 - Dedicated IP and Activity based warmup (300 000 contacts)** | Week | Platform | Contacts | | - | - | - | | Week 1 | Warmup group 1 from Customer Engagement | 25 000 | | Week 1 | Warmup Groups 2 - 8 | 275 000 | | Week 2 | Warmup group 2 from Customer Engagement | 75 000 | | Week 2 | Contacts emailed from your current platform | 225 000 | Ensure that all unsubscribed contacts from your current platform also are unsubscribed in Customer Engagement before the first send-out of each warmup week. ## Suggestions for the warmup period Here are some important suggestions for the warmup period. ### Sendout frequency Frequency of newsletters (which are defined as bulk sendouts to all or a large part of your total database) can be kept as normal during the warmup period, if that frequency is 3 sendouts per week or less. However, if the desired sendout frequency is higher, you should anyway keep it to a maximum of 3 newsletters per week from Customer Engagement during the following periods: * The first three weeks (if sending less than a million emails per month) * The first six weeks (if sending more than a million emails per month) ### Batchwise delivery For mails sent during the warmup period, it is recommended that you split up your send-outs into batches (4 batches with an hour in between each batch) using Customer Engagement's “Batchwise Delivery”. Even after the warmup period, you should continue using “Batchwise Delivery”, the recommendation now being to send in at least 4 batches with 30 mins between each batch. ### Monitoring your sendouts during the warmup period Follow your open rate (and also your click rate trend) to understand potential effects of email filtering at the ISP:s. While this is extra important during the warmup period, it is also very important for your day-to-day business. Find out more here: # Get historical messages Source: https://developer.voyado.com/docs/campaigns/get-historical-messages The Customer Engagement API has three endpoints to fetch the latest SMS, email and postal messages saved for a specific contact. ```http Emails theme={null} GET /api/v3/contacts/{contactId}/emailmessages ``` ```http SMS theme={null} GET /api/v3/contacts/{contactId}/smsmessages ``` ```http Postal theme={null} GET /api/v3/contacts/{contactId}/postalmessages ``` By default, the newest 100 messages will be fetched. By adding the parameter `count` to your query, you can request any number up to the maximum of 500. An actual request to this endpoint might look like this: ```http theme={null} GET /api/v3/contacts/111d3329-7eef-45c0-37f9-ae9300a0dacei/emailmessages?count=350 ``` This endpoint is primarily for first-line support platforms, allowing customer service employees to quickly troubleshoot a contact's sent messages without having to log into Customer Engagement. ### Response codes You'll get one of these two response codes: * '200' : OK * '404' : ContactNotFound ### Example response and fields ```json theme={null} { "count": 2, "items": [ { "messageId": "c524e468-pee9-43d5-8f33-af3801140666", "name": "test", "sendDateTime": "2022-10-24T16:47:45+00:00", "type": "SmsMessage", "source": "Manual", "isTransactional": false, "status": "Delivered", "messageLink": "https://myurl.customer.staging.eclub.se/Open/SmsOnline/Show/aOQaxknO1UOPRq84ARQDBB", "messageText": "Hello and welcome to ExampleShop. Please add your consent and email here [[registrationUrl:123456c7-79bb-41d4-8db8-af2a00894f33]]" }, { "messageId": "d4afac12-2222-4d17-b009-af11009b9a1a", "name": "Testauto", "sendDateTime": "2022-09-15T07:26:32+00:00", "type": "EmailMessage", "source": "Automation", "isTransactional": false, "status": "Delivered", "messageLink": "https://myurl.customer.staging.eclub.se/open/email/online/Eqyv1ICQF02wCa8FUKlejA/n0pgJu8BBkySna8RAHnP8w/8coDxC_inkCP_K8RAJuajA", "messageText": "This is a test email." } ] } ``` Unique message ID. Name of the message in Voyado. Date and time when the message was sent, including offset. Type of message. Possible values include `EmailMessage`, `SmsMessage`, and `PostalMessage`. Where the message was sent from in Voyado Customer Engagement. Possible values include `Automation`, `Manual`, and `System`. Indicates whether the message was sent as a transactional message. Does not take opt-in status into account. Delivery status of the send-out. Example: `Delivered` URL to the web version of the email. The text content of an SMS message or the subject line of an email. # Sending SMS Source: https://developer.voyado.com/docs/campaigns/sending-sms This is how to get started sending SMS to your customers from Customer Engagement. In the Customer Engagement UI, go to **Administration/Config Hub** and then [Contact attributes](https://help.engage.voyado.com/hc/en-gb/articles/23406421398428-Contact-attributes). Edit the standard attribute "Mobile phone" to make it active and visible on the contact card. Decide the sender name for your SMS (this is the name shown in the SMS inbox). Decide on the opt-out method. * A stop word, allowing the customer to send a certain word to a certain number to unsubscribe. For this, contact your Voyado specialist or account manager. * The default, which is standard Voyado opt out link. In this case, do nothing. Decide where the customer can enter their mobile number: * In My Pages. * At the POS terminal. * Opting in by sending a certain word to a number (for example, "Voyado in" sent to 123 123). Contact your Voyado specialist or account manager to arrange this. In your integration, be sure to always include the `mobilePhone` attribute in the create contact JSON payload. This makes sure your contacts is always reachable by SMS. Also include the preferences object in the create contact JSON payload, with `acceptsSms` set to "true" when the customer opts into SMS communication. This is required for them to receive SMS. ```json theme={null} { "firstName": "Example", "email": "example@example.com", "countryCode": "SE", "birthDay": "1966-06-26", "lang":"en", "contactType": "ExampleContactType", "preferences": { "acceptsEmail": true, "acceptsPostal": true, "acceptsSms": true }, "consents": [{ "id": "memberConsent", "value": true, "date": "2022-01-01T00:00:00+0100", "source": "ECOM", "comment": "Approved member terms at checkout" }] } ``` Now create an SMS. [Here you can see](https://help.engage.voyado.com/hc/en-gb/articles/17233924490780-Sending-SMS-messages) how that's done. Send a test SMS to your phone. Opt out through the method you selected in step 4. Now verify that the `acceptsSms` attribute goes from "true" to "false" on the contact card. Here's a payload example for creating a contact including mobile number and consent: ```json theme={null} { "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "mobilePhone": "+46700111222", "preferences": { "acceptsEmail": true, "acceptsSms": true } } ``` And here's an example of the payload sent to do a contact update: ```json theme={null} { "mobilePhone": "+46700111222", "preferences": { "acceptsSms": true } } ``` ## Unsubscribe from messages The Customer Engagement API has endpoints to allow unsubscribing of contacts from emails and SMS messages. ### Unsubscribe from specific message Using these two endpoints, a contact can be unsubscribed from a specific email or SMS. This is effectively the same as them unsubscribing via the unsubscribe link in an email, or through the unsubscribe link in an SMS. ```http theme={null} POST /api/v3/contacts/{contactId}/unsubscribeEmail?messageId={messageId} ``` ```http theme={null} POST /api/v3/contacts/{contactId}/unsubscribeSms?messageId={messageId} ``` If messageId is not sent in the request, the messageId of the last message received by the contact will be used. | Field | Type | Example value | | - | - | - | | contactId (mandatory) | GUID | a67b201f-d86d-4ee2-bdc7-abce0080a803 | | messageId | GUID | f0370cc7-5428-4cc3-85c9-ad0800f0bc28 | ## The sendToPhoneNumbers endpoint The Customer Engagement API has the following endpoint: ```http theme={null} POST /api/v3/sms/sendToPhoneNumbers ``` This allows the client to send out SMS messages via Customer Engagement (using it as a proxy) without any connection to a specific contact. Even if the mobile number used happens to exist for a contact in Customer Engagement, no link will be made to them. This is a service that can be used as a workaround in case the sending out of SMS can't be solved using an automation. The payload used to send out SMS looks like this: ```json theme={null} { "description": "Message to our members", "invoiceReference": "Week 3", "message": "We have exciting news for you...", "phoneNumbers": [ "46701234567", "443456789123" ], "sender": "MyBrand" } ``` The `sender` field can consist of the characters a-z, A-Z, 0-9, and "+". It can be 3-11 characters in length. This method should only be used for transactional communication as it doesn't validate consents or accept flags for the receivers, and it doesn't record any statistics. Any SMS sent in this way will show up on the client's SMS usage account as "Send to phone numbers" but no more details will be given. # Suggested products Source: https://developer.voyado.com/docs/campaigns/suggested-products A personalized suggested product is an item that a customer might be likely to buy, based on their previous purchases, activity and other factors. Customer Engagement's suggested products engine looks at all historical purchases and identifies how products relate to each other through the behavior of your customers. Once the AI engine learns your customers' purchase and return behavior, it can use their favorite product categories, favorite brands, viewed relevant products online, and abandoned carts to present them with suggested products. If you want to control which products are included in the recommendation engine and which are not, you can use a separate product feed for suggested products. However, if you do this, be aware that it needs to be structured the same way as your other product feeds. ## Prerequisites To get started with the suggested products engine, your Customer Engagement environment (tenant) needs to have these features activated: * Product feed (only products in this feed can be recommended) * A BI-export to the Customer Engagement BI-lake Your Voyado PM will need to help you with this. ## Fetching suggested products There is an API endpoint to do this. You'll need the contact's unique contact ID. ```http theme={null} GET /api/v3/contacts/{contactId}/productrecommendations ``` The response payload will be something like this: ```json theme={null} { "skus":[ "1234","2345","3456","4567","5678","6789","7890","8901","9012","0123"] } ``` If the request has not been successful, you'll get one of the following HTTP error codes: * *400 - InvalidContactId* * *404 - ContactNotFound* ## Promote/demote suggested items It is possible to promote or demote items that come from your product feed. This is done with a number between -100 and 100 (referred to as the product's **weight**). A weight of -100 will demote the item to the maximum extent while 100 will promote it to the maximum extent. The "maximum extent" is determined by a setting in the back-end which control the maximum (and minimum) boost effect. So you'll need to inform your Voyado PM if you plan to use this promote/demote functionality. The default maximum / minimum boost effect is 20% and usually this is the best value. The field in the XML that contains the promotion weight for a product should be named "productRecommendationPromotionWeight": ```xml theme={null} 50 ``` Contact your Voyado PM if you have any more questions about this. # About partner integrations Source: https://developer.voyado.com/docs/introduction/about-partner-integrations The partner integrations (third party integrations) articles have been moved to a dedicated portal. Here, you can find information on our partner integrations with, for example: ## [E-com integrations](https://partner-integrations.voyado.com/docs/ecom) * Adobe Commerce (formerly Magento) * Big Commerce * Centra * Salesforce * Shopify * Shopware ## [POS integrations](https://partner-integrations.voyado.com/docs/pos) * Centric * Front Systems * LS Central * Orisha Commerce (formerly CowHills) * Shopify * Sitoo ## [Extensions](https://partner-integrations.voyado.com/docs/extensions) * Crobox * Gamifiera * Ingrid * Inretrn * Lipscore * Loyyo * OneSignal * Playable * Sift Lab * TikTok leads * Trustvoice * Vipps * Walley * Zapier # What's new? Source: https://developer.voyado.com/docs/introduction/whats-new Here you'll find updates that might affect integrations and system behavior (new API endpoints, webhook changes, changes to data structures and data products). If you're building or maintaining integrations with Customer Engagement, this is the place to stay informed. You can also subscribe to [the RSS feed](/docs/introduction/whats-new/rss.xml) for this page. Find changes to Customer Engagement features, functionality and UI at [the Help Center](https://help.engage.voyado.com/hc/en-gb). ## Engage -> Customer Engagement The name "Engage" has now become "Customer Engagement" and you'll see that change across the docs. ## Elevate -> Product Discovery The name "Elevate" has now become "Product Discovery" and you'll also see that change across the docs. ## Abandoned checkout Documentation added for [abandoned checkout](/docs/tracking/tracking-checkout-changes). ## Tracking script version Latest tracking script version is now `0.1.8`. ## Search results page reverted The search results page functionality was removed due to a possible vulnerability. A reworking is underway. ## App Push Added article for [new app push functionality](/docs/app-push/app-push). ## Ratings & Reviews Updated article - [see it here](/docs/ratings-and-reviews). ## NPS article Added article covering the [v3 NPS endpoint ](/docs/contacts/nps). ## Contact Data product expanded The value\_json (VARIANT) field has now been expanded into [explicit, typed columns](/docs/data-access/delta-shares/contact-data-product). ## Added validFrom attribute A GET request to `/api/v3/reward-vouchers` now returns the `validFrom` attribute in the payload. ## Web activity tracking The [Web Activity Tracking](/docs/tracking/web-activity-tracking) article was updated, clarified and separated. ## Restructuring and getting started guide The layout of developer has been refreshed and a new [initial setup guide](/docs/introduction/getting-started#initial-setup) was added. ## Search results page added to docs Search results in developer can be viewed on [a page which can be linked to](/search?q=result). ## Message services data product expanded Seven new datasets were added to the [message services data product](/docs/data-access/delta-shares/message-services-product). ## Point Shop for personalized offers The Point Shop and its API allow you to create more flexible and personalized loyalty offers. [Read about it here.](/docs/loyalty/point-shop). ## Store location data Support for longitude and latitude added to Engage stores, including API support, validation, admin Ul, and import/export flows. This lays the foundation for future location-based services. ## Value split in product purchases Value Split for product purchases has additional attributes: brand, main category, product category, subcategory, and size, enabling more precise automations and segmentation. ## Receipt deletion Added the ability to delete receipts (transactions ) via contact card and API, including reason capture and audit logging for traceability. Deleting a receipt will *not* remove any points or checkpoints that were previously created based on that transaction. ## New Delta Share data products Articles added for new data Products [Reviews](/docs/data-access/delta-shares/reviews-product) and [Targeting](/docs/data-access/delta-shares/targeting-product). ## Improvements to data access articles Data access articles (Delta Share and BI Exports) were [expanded and restructured](/docs/data-access/overview). ## New endpoint to confirm connection Added information about the `/livez` endpoint. [See more here](/docs/api/the-api#liveness-check). ## Promotions payload Updated example payload for getting promotions for a contact. [See the new payload here](/docs/loyalty/promotions#get-promotions-for-contact). ## New webhooks New webhooks added for consents, contact changes and assignment of promotions. [See it here](/docs/webhooks/webhooks). ## XML import article updated Expanded article about XML import of promotions to cover both creation and assignment. [See it here](/docs/file-based-transfer/promotions-xml-create-assign). ## New Delta Share product: Contact Data A new Delta Share data product was added: Contact Data product. [Read about it here](/docs/data-access/delta-shares/contact-data-product). ## New docs portal New developer documentation portal launched for current API version and other integration information. Legacy v2 docs (for now) are hosted separately [here](https://legacy.developer.voyado.com/) and partner integrations [now have their own portal](https://partner-integrations.voyado.com/). ## New BI exports Three new [BI exports added](/docs/data-access/bi-export) for users of Design Studio: Email, Sendout, Marketing Group. ## Transaction XML change Transaction XML import now supports the parameter `usedRewardVouchers` as well as `usedBonusChecks` making it consistent with the API. ## Integration logging The logging for when an article is created due to missing SKUs was lowered from "warning" to "info" in the integration log. ## Move to Help Center Moved [Double Opt-In article to Help Center](https://help.engage.voyado.com/hc/en-gb/articles/17253837916316-Double-opt-in) and expanded it. ## Delete in Delta Share A \_change\_type value for Delta Share named “delete” [was added](/docs/data-access/delta-share). A delete record will now be generated for any row where the import\_date\_time is older than 30 days. This serves as a technical log entry indicating that the row will be removed. ## Config Hub move All [Config Hub articles](https://help.engage.voyado.com/hc/en-gb/sections/21844508239004-Config-Hub) were moved to the Engage Help Center. ## Embedded order action added [Embedded order action](/docs/orders/orders-api-endpoints) added for creating or updating orders. ## Non-functional requirements article Added an article outlining the [non-functional requirements for integrations](/docs/api/non-functional-requirements "Non-functional requirements"). ## New reference material Replaced the back-end and front-end packages lists (which were static pages) with a [link to a dynamically generated list.](/docs/introduction/reference-material "Packages and licenses") ## Value-added services moved Integration documentation for value-added-services moved to the [Engage Help Center](https://help.engage.voyado.com/hc/en-gb/sections/19126553699740-Value-added-services). ## API timeouts change API timeout updates. See here about timeouts [when creating a contact](/docs/contacts/create-contact "Create contact") and here for the [general information and API retry policy](/docs/api/the-api#retry-policy "API retry policy"). ## Testfreaks become Trustvoice Testfreaks changes name to Trustvoice and [docs were updated to reflect this](https://help.engage.voyado.com/hc/en-gb/sections/20141680550044-Trustvoice). ## Contact endpoint timeout changes Timeout for requests made to the create contact endpoint (API v3) was made shorter. [Read in more detail here](/docs/api/timeout-changes#create-contact-endpoint "Timeout changes"). ## Interaction segmentation Segmentation based on individual properties in interactions [is now possible](/docs/loyalty/interactions#interaction-segmentation "Interaction segmentation"). ## Cow Hills becomes Orisha Cow Hills changes name to Orisha Commerce and docs were updated to reflect this. ## Centra back-in-stock New back-in-stock functionality for the Centra accelerator. # API integration Source: https://developer.voyado.com/docs/onsite/api-integration Voyado Onsite can be connected to different platforms and databases. Customer Engagement, of course, but also other CRMs / CXPs / CDPs as well as different e-com platforms. You can also use the generic API connection provided to build your own custom connections to and from any platform lacking a pre-made Onsite integration. This will be covered in this article. Voyado Onsite does not provide a generic API. When using the generic API solution, you will be setting up a server-side API for Voyado Onsite to use as a client based on the specifications below. ## Configuration Configuring a generic API integration is done in the Onsite manager. Follow these steps: 1. Log into the Onsite manager, then go to **Configuration** and **Sites**. 2. Pick the site you want to set up the generic API integration for and either click on its name or select "Edit" in the three-dots menu. 3. You should now be in **General settings** for that site. Scroll down to **Integrations**, locate the option **Generic** and click to expand it. 4. You'll now see the configuration for the generic API integration. The generic API integration There are three authentication methods to choose from: * Header * HMAC * API Key The configuration is very similar for all three methods. The values to fill in are: * **Get/Lookup Contact API Endpoint URL**: The URL for your API endpoint that will be used to check if a contact already exists. * **Create/Update Contact API Endpoint URL**: The URL for your API endpoint that will be used for creating new contacts and updating existing contacts with new data, consents, and so on. * **Email Key**: The variable name to be used for email address in API requests. * **Phone number Key**: The variable name to be used for mobile phone number in API requests. * **Name Key**: The variable name to be used for customer name (full name) in API requests. * **Personal Identity Key**: The variable name to be used for social security number (personal number in Sweden, for example) in requests to your API. * **Ignore HTTP response codes**: Here you list the HTTP error codes to ignore, which means no error will be logged if any of the listed response codes are received. Use this feature to ensure everything still functions in the case of the API responding with error codes. If this setting is *not* active, and an unexpected response code is returned from the API, the user flow will be blocked and/or aborted. ## The data The same JSON structure is used for sending and receiving data (via POST and GET respectively). The fields in the JSON are determined by what the campaign is configured to collect: ```json Sending and receiving Onsite data theme={null} { "name": "John Doe", "firstname": "John", "lastname": "Doe", "email": "john.doe@example.com", "phone": "+358123458394", "custom": "customvalue", "customoption": ["customoptionvalue1,customoptionvalue2"], "redeal": ["ExistingAmbassador", "NewFriend", "FriendConverted"] } ``` In this example, `customoption` might be "pet" and `customoptionvalue1` might be "Dog". You should configure the API to ignore anything that you don't recognize and/or can't handle. This is fully configurable and maps directly into what is collected from the user. There is one special field redeal to tag the type of contact, which is used for conversion tracking. You can choose to store this or just ignore it. The current tags in use are: * **NewAmbassador**: A new contact who acted as an ambassador by referring friends * **NewFriend**: A new contact from a friend referral * **NewSignup**: A new contact without a referral * **ExistingAmbassador**: An existing contact who acted as an ambassador * **ExistingFriend**: An existing contact who acted as a friend * **ConvertedAmbassador**: A contact acting as an ambassador who has converted * **ConvertedFriend**: A contact acting as a friend who has converted ## Receiving data The API call to fetch data is: ```http theme={null} GET {Get/Lookup Contact API Endpoint URL}?company={company}&email={email} ``` "Get/Lookup Contact API Endpoint URL" is the value you configured in Onsite Manager above. Expected return codes are: * *200 OK* * *404 Not found* The reason this GET is called is to avoid asking the user for information that is already known, and to set the correct redeal tags. You should make it optional to use the GET call. ## Sending data The API call used to send data is one of the following: ```http theme={null} POST {Create/Update Contact API Endpoint URL}?company={company} ``` ```http theme={null} POST {Create/Update Contact API Endpoint URL}?company={company}&email={email} ``` ```http theme={null} POST {Create/Update Contact API Endpoint URL}?company={company}&phone={phone} ``` "Create/Update Contact API Endpoint URL" is the value you configured in Onsite Manager above. Expected return codes here are: * *200 OK* * *201 Created* # Configuring onsite messaging Source: https://developer.voyado.com/docs/onsite/configuring-onsite Onsite uses pop-ups, banners, and referral widgets on your website to capture leads, to segment visitors, and to activate offers It taps into real-time behavior to make your site more dynamic and personal. Here is how you configure and work with onsite messaging in Customer Engagement. ## Prerequisites Before you start you will need these things to be in place: If your're using Google Tag Manager (GTM) you'll need to have it correctly installed on your website. You'll need access to your GTM container with publish permissions. You'll also need access to your website's dataLayer or variables for: * Order totals and revenue * Customer email addresses * Currency codes * Discount/coupon codes A test environment is needed to validate implementation And, optionally, the [Voyado Chrome extension](https://chromewebstore.google.com/detail/voyado/lccnimeooniolaajpndmaichmjkcopdp) installed for testing ## Implementation paths Here we will consider three ways to implement Onsite: 1. Via Google Tag Manager in SPAs (single page applications) 2. Via Google Tag Manager in traditional multi-page applications 3. Via direct implementation ## 1- Google Tag Manager - SPA Single Page Applications require special handling because page navigation doesn't trigger traditional page loads. For example, if your trigger uses custom pageload events, your exceptions must also use custom pageload events. Mixing event types (e.g., history change with pageview exceptions) will cause failures. Other aspects of SPAs: * Navigation happens without full page reloads * Scripts must trigger on virtual page views * Open widgets persist during navigation and must be explicitly closed * All triggers must use consistent event types You'll need to create four tags. Suggested names are: 1. **Voyado Onsite Cornerwidget** - Displays messaging widgets across the website 2. **Voyado Onsite Hide** - Closes any open widgets on checkout/cart pages 3. **Voyado Onsite Purchase** - Tracks conversions and displays messages on order confirmation page 4. **Voyado Onsite Login** - Identifies user and enables personalized messaging onsite ### Tag 1: Voyado Onsite Cornerwidget **Purpose:** This loads and displays messaging widgets on landing pages and virtual page views. **Trigger:** All pageviews EXCEPT checkout, cart, and order confirmation pages. Should also be excluded when the login script is called instead. ```javascript Voyado Onsite Cornerwidget Script theme={null} ``` The (typeof redeal !== 'function') check prevents the script from being loaded multiple times on SPAs. The script loader only runs once, but redeal('cornerwidget') can be called on each virtual page load. **Configuration in GTM:** * Tag Type: Custom HTML * Trigger: Custom Event on page load * Exceptions: Checkout page, Cart page, Order confirmation page and Login event **Alternative tag names:** Replace "cornerwidget" with any tag name configured in Onsite Manager. Use multiple tags with different conditions to trigger different onsite messages. ### Tag 2: Voyado Onsite Hide **Purpose:** Close open widgets when users navigate to pages where widgets shouldn't appear (checkout, cart). On SPAs, excluding pages from the "Voyado Onsite Cornerwidget" tag only prevents NEW widgets from loading; it doesn't close widgets that are already open. This tag explicitly closes them. **Trigger**: Pageview on checkout and cart pages. ```javascript Voyado Onsite Hide script theme={null} ``` **Configuration in GTM:** * Tag Type: Custom HTML * Trigger: Custom Event on page load when Page Path contains “/checkout” or “/cart” ### Tag 3: Voyado Onsite Purchase **Purpose**: Track conversions and optionally trigger post-purchase onsite messages. **Trigger**: Order confirmation page load (immediately after purchase). ```javascript Voyado Onsite Purchase script theme={null} ``` | Variable Name | Type | Example value | Description | | - | - | - | - | | sumtotal | Data Layer Variable | "113.75" | Total amount paid by customer (string) | | sumrevenue | Data Layer Variable | "93" | Revenue excluding tax and shipping (string) | | currencycode | Data Layer Variable | "SEK" | ISO currency code | | userEmail | Data Layer Variable | "[john@doe.com](mailto:john@doe.com)" | Customer email address | | discountcode | Data Layer Variable | "aJ7q54JU" | Coupon/discount code used (can be array) | Expected output: ```javascript theme={null} redeal('checkout', { total: "113.75", revenue: "93", currency: "SEK", email: "john@doe.com", coupons: ["aJ7q54JU"] }); ``` Important notes: * total = full amount paid by customer (including tax, shipping, fees) * revenue = amount excluding tax, shipping, and fees * coupons should be an array even if there is only one code * Missing or null values won't break functionality but will limit tracking and reporting * Optional fields: name, phone (useful for prefilling onsite messages and automatically identifying visitors) **Configuration in GTM:** * Tag Type: Custom HTML * Trigger: Custom Event associated with reaching the order confirmation page, for example “purchase”, or when Page Path contains “/order-confirmation” or “/thank-you” ### Tag 4: Voyado Onsite Login **Purpose**: Identifying unidentified visitors and triggering personalized onsite messages **When to trigger:** * When a user actively logs in (login button click) * On page load if user is already logged in (“soft login”) **Trigger**: Login event OR pageview when user is authenticated The login script should only be called *once per session*, and not on any subsequent page views after the login event occurs. On subsequent page views, calling the cornerwidget script instead is expected. ```javascript Voyado Onsite Login script theme={null} ``` **Required GTM Variables:** * userEmail - User's email address (unhashed string) **Optional fields**: * phone: userPhone – User's phone number * contactId: contactID – User's contact ID in Voyado Customer Engagement **Configuration in GTM**: * Tag Type: Custom HTML * Trigger: Custom Event **user\_login** OR first page view when User State = “logged\_in”, expected to fire only once per session **Benefits**: * Enables automated consent updates via Reach Ability feature * Triggers onsite messages to complete missing profile data * Improves contact data quality * Improves onsite scripts user identification ## 2 - Google Tag Manager (traditional) For traditional multi-page applications (non-SPAs), the implementation is simpler because each page navigation is a full page load. The main differences from the SPA implementation are: * No need for the if (typeof redeal !== 'function') check * No need for the Hide tag (widgets don't persist across page loads) ### Tag 1: Cornerwidget (Traditional Sites) ```javascript Cornerwidget (Traditional Sites) script theme={null} ``` **Trigger:** All Pages EXCEPT checkout and order confirmation. ### Tag 2: Purchase (Traditional Sites) ```javascript Purchase (Traditional Sites) script theme={null} ``` **Trigger**: Order Confirmation Page ### Tag 3: Login (Traditional Sites) ```javascript Login (Traditional Sites) script theme={null} ``` **Trigger**: Login success event OR first page view when user is authenticated ## 3 - Direct implementation For sites without GTM, or when you want full control, you can add the script directly to your HTML. Place this code immediately after the opening \ tag on all pages where messaging should appear: ```javascript Direct implementation script theme={null} ``` This loads the script asynchronously without blocking page rendering. The individual tag types are implemented like this: ### Cornerwidget (landing pages) ```javascript Cornerwidget (landing pages) script theme={null} ``` With user prefill (when user data is available) you will use: ```javascript Cornerwidget (landing pages) with refill script theme={null} ``` ### Checkout (order confirmation page) ```javascript Checkout (order confirmation page) script theme={null} ``` ### Login (login page or when user is authenticated) ```javascript Login script theme={null} ``` ### Hide function (SPAs only) ```javascript Hide function script theme={null} // Call this when navigating to pages where widgets should be hidden if(typeof redealHide === 'function') { redealHide(); } ``` ### Script reference **Standard tag names** | Tag name | When to use | Recommended data | | - | - | - | | cornerwidget | Landing pages, general browsing | None (optional/if available: email, name, phone, contact ID) | | checkout | Order confirmation/thank you page | total, revenue, currency, email, coupons | | login | After user login or when authenticated | email (optional/alternative: phone, contact ID) | | custom | Any custom trigger defined in Onsite Manager | Varies by onsite message | ### Data field specifications ```javascript Checkout fields theme={null} { total: "113.75", // String: Total amount paid (including tax, shipping) revenue: "93", // String: Revenue excluding tax and fees currency: "SEK", // String: ISO 4217 currency code email: "user@example.com", // String: Customer email coupons: ["CODE123"], // Array: Discount codes used name: "John Doe", // Optional String: Customer name phone: "+46701234567", // Optional String: Customer phone contactId: "b9a02dc6-52b8-4e11-b929-b37500f002f1" // Optional String: User's Contact ID in Engage } ``` ```javascript Cornerwidget fields (all optional) theme={null} { email: "user@example.com", name: "John Doe", phone: "+46701234567", contactId: "b9a02dc6-52b8-4e11-b929-b37500f002f1" } ``` ```javascript Login fields theme={null} { email: "user@example.com", // Optional: User email phone: "+46701234567", // Optional: User phone contactId: "b9a02dc6-52b8-4e11-b929-b37500f002f1" // Optional: User's Contact ID in Engage } ``` It is optional which of the above field(s) to pass in the script call, but at least one needs to be passed for script identification upon login to work. ### Custom trigger tags You can define custom tags in Onsite Manager and trigger them: ```javascript Trigger custom tag theme={null} redeal('custom-tag-name'); ``` Use case: Trigger different onsite messages based on user actions: ```javascript theme={null} ``` A/B Testing: If multiple onsite messages use the same script tag and fulfil any additional targeting criteria needed to be displayed, one will be randomly selected and displayed to the user. ### Hide function **Basic usage (hide all open widgets):** ```javascript Hide all open widgets theme={null} redealHide(); ``` ### Onsite message URL links Each onsite message in Onsite Manager generates an onsite message URL link as well as a QR code. Use these links in emails or social media to drive traffic to your web­site and trigger the appropriate onsite messages automatically. With personalization variables (for email marketing): ```http theme={null} https://yoursite.com/onsite-message-url?email=*|EMAIL|*&name=*|NAME|* ``` Replace *|EMAIL|* and *|NAME|* with the merge tag format from your email service provider. ### Domain matching Onsite messages only trigger on domains configured in Onsite Manager. **Use these:** Production domain: [www.example.com](http://www.example.com) Test subdomain: [test.example.com](http://test.example.com) **Do not use these:** Local development: [localhost](http://localhost) or 127.0.0.1 IP addresses: 192.168.1.100 For testing on non-production domains, add the test domain to your site configuration in Onsite Manager. # Testing and performance Source: https://developer.voyado.com/docs/onsite/testing-and-performance Below you'll find diverse information to help you with testing, valiadtion, touble-shooting and measureing performance of your Onsite set-up. ## Testing and validation Here is some useful information to help you with testing and validation. ### Using the Chrome Extension 1. Install the Voyado Chrome Extension 2. Navigate to your website 3. Wait for the page to fully load 4. Click the Voyado extension icon 5. Verify that the script is detected The extension only detects if the script is loaded on the current page. It doesn't validate triggers or data. ### Browser Console Checks Use this URL parameter to activate debug mode: ```http theme={null} https://yoursite.com/?voyado-debug ``` Open your browser's developer console (F12) and check: * The script loaded successfully: typeof redeal === 'function' — Should return: true * The widget is open: typeof IsRedealOpen === 'function' && IsRedealOpen('cornerwidget') — Should return: true if cornerwidget is open * There are script errors: Look for red errors in the console. Common issues: * Failed to load resource: [https://static.redeal.se/widget/js/redeal.js](https://static.redeal.se/widget/js/redeal.js) - Script blocked by firewall/CSP * redeal is not a function – Script not loaded or loaded after trigger call ### Clear testing session Use this URL parameter to reset your test session: ```http theme={null} https://yoursite.com/?voyado-clear ``` This clears local and session storage, allowing you to test onsite messages as an unidentified/new visitor. ### Testing checklist * Script loads on all intended pages * Only one script call loads per pageview * Cornerwidget appears on landing pages * Cornerwidget does NOT appear on checkout/cart pages * Open widgets close when navigating to checkout (SPAs only) * Checkout tag fires on order confirmation with correct data * Login tag fires when user logs in * Console shows no JavaScript errors * All GTM variables return expected values * Onsite message URL links work and trigger onsite messages * Onsite messages only trigger on configured domains * Test with ?voyado-clear to simulate new visitors For implementations using Google Tag Manager, using GTM's Preview mode to ensure the tags are correctly installed and that triggers are firing appropriately with complete and correctly formatted variables is highly recommended. ### Network tab verification In browser **DevTools > Network** tab: 1. Filter by “redeal” 2. Verify redeal.js loads successfully (Status: 200) 3. Check that POST requests to Voyado API return 200 status 4. Verify request payloads contain expected data ## Troubleshooting Here are some tips to help with trouble-shooting of onsite messaging. ### Script not loading **Symptoms:** * Chrome extension shows no detection * typeof redeal returns undefined * No network requests to static.redeal.se **Possible causes:** 1. Content Security Policy (CSP) blocking the script **Solution**: Add [https://static.redeal.se](https://static.redeal.se) to your CSP script-src directive 2. Ad blocker or privacy extension **Solution**: Disable extensions for testing or whitelist your domain 3. Firewall blocking external scripts **Solution**: Whitelist [static.redeal.se](http://static.redeal.se) in your firewall rules 4. Incorrect GTM trigger configuration **Solution**: Check trigger fires on page load using GTM Preview mode 5. Script tag syntax error **Solution**: Validate HTML, ensure no extra characters in script tag ### Widget not appearing **Symptoms:** * Script loads but no widget visible * Console shows no errors **Possible causes:** 1. Domain not configured in Onsite Manager **Solution**: Verify your domain is added to the onsite message configuration 2. Onsite message not active **Solution**: Check onsite message status in Onsite Manager 3. Onsite message already shown to this user **Solution**: Test with ?voyado-clear URL parameter 4. Wrong tag name **Solution**: Verify tag name in code matches Onsite Manager configuration 5. Z-index conflict **Solution**: Check if widget is rendering but hidden behind other elements ### Conversion not tracking **Symptoms:** * Checkout tag fires but conversions don't appear in reports **Possible causes:** 1. Variables returning null or undefined **Solution**: Use GTM Preview to verify variable values 2. Incorrect data types **Solution**: Ensure total, revenue are strings, coupons is array 3. Currency code format **Solution**: Use ISO 4217 codes (SEK, USD, EUR, etc.) 4. Race condition (script triggers before GTM variables populate) **Solution**: Add a small delay or ensure dataLayer pushes before tag fires ### GTM variables not populating **Symptoms:** * GTM Preview shows undefined or null variable values **Solutions:** 1. Verify dataLayer structure matches your variable configuration 2. Check that dataLayer.push() happens before page load events 3. Use “Data Layer Variable” type (not “JavaScript Variable”) in GTM 4. Test dataLayer in console: console.log(dataLayer) ### SPA issues **Widget persists on checkout page:** * Ensure Hide tag is implemented and triggering correctly * Verify Hide tag trigger uses same event type as other triggers * Check GTM Preview to confirm Hide tag fires **Widget not appearing after navigation:** * Confirm Cornerwidget tag triggers on virtual pageviews * Verify history change events are captured in GTM * Check that custom events fire consistently **Script loaded multiple times:** * Ensure if (typeof redeal !== 'function') wrapper is present * Check GTM Preview for duplicate tag fires ### Console error messages **Common errors and solutions:** | Error | Cause | Solution | | - | - | - | | Failed to load resource: [https://static.redeal.se/…](https://static.redeal.se/…) | Script blocked | Check CSP, firewall, ad blockers | | redeal is not a function | Script not loaded before call | Ensure script loader runs first | | IsRedealOpen is not defined | Script not fully initialized | Add conditional check: typeof IsRedealOpen === 'function' | | Cannot read property 'email' of undefined | Missing data object | Verify GTM variables are defined | ## Performance & security Performance and security can be improved by following these tips. ### Content Security Policy (CSP) If your site uses CSP headers, add these directives: ```yaml theme={null} Content-Security‐Policy: script-src 'self' https://static.redeal.se; connect-src 'self' https://*.redeal.se; ``` Without these directives, the script will be blocked by the browser. ### Script loading strategy The Voyado script uses async loading to minimise performance impact: ```javascript theme={null} a.async=1; // Script loads asynchronously ``` **Benefits:** * Non-blocking: Page rendering continues while script loads * Fast: Script hosted on CDN with global distribution * Cached: Browser caches script for subsequent visits **Performance Impact:** * Initial load: \~15-25 KB (minified and gzipped) * Subsequent loads: 0 KB (cached) * Page load delay: \<50 ms on average ### Best practices **Do:** * Load script on all relevant pages (cornerwidget is reusable) * Use async loading (included in implementation) * Implement proper CSP directives * Test on staging before production * Monitor console for errors after deployment **Don't:** * Block page rendering waiting for script * Load script multiple times on same page (use conditional checks for SPAs) * Hardcode customer data in JavaScript (security risk) * Skip testing on different browsers * Forget to update CSP when implementing ### Data privacy **Email handling:** * Emails are transmitted over HTTPS * Never expose emails in URLs (use POST data) * Comply with GDPR/privacy regulations in your region * Inform users about data collection in your privacy policy **Session data:** * Script uses local storage and session storage * Data is stored client-side only * Use ?voyado-clear to manually clear stored data ### Browser compatibility The script is compatible with: * Chrome (latest) * Firefox (latest) * Safari (latest) * Edge (latest) * Mobile browsers (iOS Safari, Chrome Mobile) Not supported: * Internet Explorer 11 and earlier ## Additional resources Onsite Manager: Configure onsite messages, sites/domains and view analytics Chrome Extension: [Download here](https://chromewebstore.google.com/detail/voyado/lccnimeooniolaajpndmaichmjkcopdp) GTM Documentation: [Google Tag Manager Help](https://support.google.com/tagmanager/answer/6103696) Voyado Support: Contact your Voyado representative for technical assistance If you have questions or encounter issues not covered in this guide, reach out to Voyado support or your Voyado representative. # Tracking cart changes Source: https://developer.voyado.com/docs/tracking/tracking-cart-changes How to send shopping cart updates to Customer Engagement with cart() and emptyCart(), and what abandoned cart automations require. Tracking cart changes is what makes abandoned cart automations possible. For Customer Engagement to trigger one, it needs to know **every state of every cart** and, eventually, **which contact the cart belongs to**. You can send cart data either with the [tracking script](/docs/tracking/tracking-script) or with the tracking API. ## How it works 1. Your site submits every cart change, via the tracking script or the tracking API. 2. A cart that still contains products and hasn't been touched for 30 minutes is treated as abandoned. If it is connected to a contact, it is sent to Customer Engagement. All other carts are filtered out. 3. Customer Engagement enriches the products from the product feed and triggers the automation matching the cart's locale. ## What an abandoned cart automation requires An abandoned cart automation triggers only when all of these are true: * The cart's `locale` matches a product feed in Customer Engagement, and an active automation is set to that language. * At least one SKU matches a product in that feed (usually via `g:id`). Products that don't match are left out of the email; if **no** SKUs match, the automation doesn't trigger at all. * No product in the cart has been purchased since the cart was marked abandoned - that is, no transaction has reached Customer Engagement. This can be overruled in the trigger configuration. ## Using the tracking script Two methods are involved. Both send an HTTP POST to the Collect endpoint. Call `cart()` every time the cart changes: an item added, an item removed, or a quantity updated. Call it whether or not the visitor has been identified. ```javascript Adding two products to the cart theme={null} va("cart", { "cartRef": "354354", "contactId": "afa7625d-2e97-4667-b4c1-ad3b01194bee", "cartUrl": "https://www.store.se/cart?id=354354", "locale": "sv-SE", "items": [ { "itemId": "456436", "quantity": 2 }, { "itemId": "456437", "quantity": 2 } ] }); ``` Your reference for this cart. Must be unique per cart and must never change for the life of that cart. The locale of the site the cart belongs to, as an IETF language tag such as `sv-SE`. Only locales with a product feed assigned in Customer Engagement can trigger an abandoned cart automation. The full current contents of the cart. The product identifier, matched against SKU in Customer Engagement. The number of that item in the cart. A link back to this cart on your site, for use in the abandoned cart email. The Customer Engagement contact ID. If omitted, the `_vaI` cookie is used as a fallback. Call `emptyCart()` when the cart has been emptied, either by the visitor or by a completed checkout. It is equivalent to calling `cart()` with no items. Calling it after every successful checkout is important — otherwise the cart stays populated in Customer Engagement and may trigger an abandoned cart email to someone who has already bought. ```javascript theme={null} va("emptyCart", { "cartRef": "354354", "contactId": "afa7625d-2e97-4667-b4c1-ad3b01194bee" }); ``` The same cart reference used for the cart's other events. Empty carts also need one. The Customer Engagement contact ID. Adding this is highly recommended, but if omitted, the `_vaI` cookie will be used as a fallback. ## Using the tracking API You can submit cart changes directly via the API instead of implementing the tracking script. The API always requires a `contactId`, so it can only track **identified** visitors. Anonymous carts can only be tracked with the script. You are also responsible for identifying visitors who arrive from Customer Engagement email links yourself, by reading the `vtid` query parameter and passing it as `contactId`. If you use abandoned browse as well as abandoned cart, you must also include a `sessionId` in every payload. See [sessions and sessionId](/docs/tracking/tracking-product-views#sessions-and-sessionid). You can test the calls from your OpenAPI (Swagger) page: ```http theme={null} [tenantname].voyado.com/api/v3/ui/index#/tracking ``` ## Rules to follow Send a cart event **only** when the cart actually changes. Firing events at other times — on page load, on navigation — pollutes your data in Customer Engagement. * **Track anonymous carts too.** A cart belonging to an anonymous visitor is still linked to their contact once they are identified. * **Every change means one event.** Customer Engagement always works from the cart's latest state, so the data you send must match what the site currently displays. Cart updates can happen in a product page, a popup, a mini-cart or the checkout — all of them need to fire an event. * **`cartRef` must be unique and stable.** Never share a `cartRef` between carts or visitors, and never change it for an existing cart. Getting this wrong on remove-product and empty-cart events is a common bug. * **Always send a `cartRef`, even for empty carts.** * **`locale` must match a product feed.** Customer Engagement needs a feed for that locale to enrich products with article data, and an active automation using that locale. * **Anything not emptied risks being marked abandoned.** If a visitor leaves or buys without your site calling `emptyCart()`, an active abandoned cart automation will trigger on that cart. # Tracking checkout changes Source: https://developer.voyado.com/docs/tracking/tracking-checkout-changes How to send checkout updates to Customer Engagement with checkout(), and how they drive Abandoned Checkout automations. Tracking checkout changes is what makes Abandoned Checkout automations possible. Each checkout event tells Customer Engagement *what a visitor is about to buy*: the products, prices, discounts, coupon, and whether they have selected a shipping and payment method. Every event is tied to a cart through `cartRef`, and to a contact through `contactId` or the visitor's identification cookie. You can send checkout data either with the [tracking script](/docs/tracking/tracking-script) or with the tracking API. ## How it works 1. Your site submits every checkout change, via the tracking script or the tracking API. 2. A checkout that still contains products and hasn't been touched for 30 minutes is treated as abandoned. If it is connected to a contact, it is sent to Customer Engagement. All other checkouts are filtered out. 3. Customer Engagement enriches the products from the product feed and triggers the **Abandoned Checkout** automation matching the checkout's locale. ### Checkout, cart and product views in one session Checkout works together with [cart changes](/docs/tracking/tracking-cart-changes) and [product views](/docs/tracking/tracking-product-views). Customer Engagement uses the `sessionId` to identify the winning "abandoned" signal for a session. If a single abandoned session contains all three event types (checkout, cart and product view), only one Abandoned Checkout signal is produced, not one per event type. To get this right, send the events from the same session with the same `sessionId`. The tracking script handles this for you. If you use the tracking API, you must include the `sessionId` yourself (see below). ## Using the tracking script Call `checkout()` every time the checkout state changes: an item is added, removed or updated, a coupon is applied, or the visitor selects a shipping or payment method. Call it whether or not the visitor has been identified. ```javascript Tracking a checkout theme={null} va("checkout", { "cartRef": "354354", "checkoutRef": "CHK-20931", "contactId": "afa7625d-2e97-4667-b4c1-ad3b01194bee", "cartUrl": "https://www.store.se/cart?id=354354", "checkoutUrl": "https://www.store.se/checkout?id=CHK-20931", "locale": "sv-SE", "currencyCode": "SEK", "coupon": "SUMMER10", "totalAmount": 1708.2, "totalDiscountAmount": 189.8, "shippingMethodSelected": true, "paymentMethodSelected": true, "items": [ { "itemId": "456436", "quantity": 2, "index": 0, "salesPrice": 499.0, "totalSalesPrice": 998.0, "totalDiscount": 99.8, "totalSalesPriceAfterDiscount": 898.2, "category": "Women", "category2": "Shoes", "brand": "Acme" }, { "itemId": "456437", "quantity": 1, "index": 1, "salesPrice": 899.0, "totalSalesPrice": 899.0, "totalDiscount": 89.9, "totalSalesPriceAfterDiscount": 809.1, "category": "Women", "category2": "Bags", "brand": "Acme" } ] }); ``` ### Checkout fields The reference of the cart this checkout belongs to. Must be the same `cartRef` you use in [cart events](/docs/tracking/tracking-cart-changes) for that cart. Your reference for this checkout. Must be unique per checkout and must never change for the life of that checkout. Maximum 255 characters. `checkoutReference` is accepted as an alias in the tracking script. The locale of the site the checkout belongs to, as an IETF language tag such as `sv-SE`. The full current contents of the checkout. A link back to the cart on your site. A link back to this checkout on your site. The Customer Engagement contact ID. Must be a valid GUID or short GUID, with a maximum of 36 characters. If omitted, the `_vaI` cookie is used as a fallback. The coupon or discount code applied to the checkout. Maximum 100 characters. The total amount of the checkout. The total discount applied to the checkout. The currency of all amounts, as a three-letter ISO 4217 code such as `SEK`. If provided, it must consist of exactly 3 letters. Whether the visitor has selected a shipping method (`true` or `false`). Whether the visitor has selected a payment method (`true` or `false`). ### Item fields The product identifier, matched against SKU in Customer Engagement. Maximum 255 characters. The number of that item in the checkout. Must be an integer of 1 or greater. The position of the item in the checkout. Must be 0 or greater. If omitted, the item's position in the `items` array is used. The unit price of the item. The price of the item multiplied by its quantity, before discount. The total discount applied to the item. The total price of the item after discount. The item's main category. Use `category2` to `category5` for deeper levels of the category hierarchy. Maximum 100 characters per category field. The item's brand. ## Using the tracking API You can submit checkout changes directly via the API instead of implementing the tracking script. The API always requires a `contactId`, so it can only track **identified** visitors. Anonymous checkouts can only be tracked with the script. You are also responsible for identifying visitors who arrive from Customer Engagement email links yourself, by reading the `vtid` query parameter and passing it as `contactId`. You must also include a `sessionId` in every payload. It is required to identify the winning abandoned signal when a session contains checkout, cart and product view events. Use the same `sessionId` across all three event types. See [sessions and sessionId](/docs/tracking/tracking-product-views#sessions-and-sessionid). Send checkout events to the `tracking/checkouts` endpoint. You can test the calls from your OpenAPI (Swagger) page: ```http theme={null} [tenantname].voyado.com/api/v3/ui/index#/tracking ``` ## Rules to follow Send a checkout event **only** when the checkout actually changes. Firing events at other times, such as on page load or navigation, pollutes your data in Customer Engagement. * **Every change means one event.** Customer Engagement always works from the latest state, so the data you send must match what the checkout currently displays. Send the full contents every time, not just what changed. * **`checkoutRef` must be unique and stable.** Never share a `checkoutRef` between checkouts or visitors, and never change it for an existing checkout. * **Use the same `cartRef` as your cart events.** This is what links the checkout to the cart. * **Always send `locale`, `checkoutRef`, `cartRef` and `items`.** The script does not send the event if any of them is missing, and logs an error to the browser console. * **Send valid items.** Every item needs an `itemId` (255 characters or fewer) and an integer `quantity` of 1 or greater. * **Use consistent currency and amounts.** Send all amounts in the same currency, and set `currencyCode` to a 3-letter ISO 4217 code. * **Use one `sessionId` per session across checkout, cart and product view events.** This ensures a single Abandoned Checkout signal per abandoned session. With the tracking script this is handled for you; with the API you must send it yourself. * **Call `emptyCart()` after a completed purchase.** A checkout event does not clear the cart. If a visitor buys without your site calling `emptyCart()`, an active abandoned automation may still trigger for someone who has already bought. See [tracking cart changes](/docs/tracking/tracking-cart-changes). # Tracking product views Source: https://developer.voyado.com/docs/tracking/tracking-product-views How to send product views to Customer Engagement with productview(), how sessions work, and what abandoned browse requires. Tracking product views feeds three features in Customer Engagement: abandoned browse, products of interest, and product recommendations. You can send product views either with the [tracking script](/docs/tracking/tracking-script) or with the tracking API. ## What product views are used for 1. Your site submits product views, which belong to a session. 2. A session with no activity for 30 minutes is treated as abandoned. If it contains **no cart event** and the product views are connected to a contact, it is sent to Customer Engagement. All other sessions are filtered out. A session can only be sent to Customer Engagement once. 3. The product views are enriched with data from the product feed. 4. Customer Engagement triggers the automation matching the trigger configuration. Email activities in that automation can include dynamic product data from the feed. From the recency, frequency and pattern of product views over the last 90 days, Customer Engagement calculates up to **25 products** each contact is most likely interested in, based purely on their onsite behavior. The list is refreshed by a scheduled job and enriched from the article register — note that products of interest are connected to the **article register**, not the product feed. It can then be used in segmentation and as part of a scheduled selection in an automation. If a contact has products of interest, that data feeds the calculation of their personal recommendations. Customer Engagement combines their onsite behavior with their full purchase and return history to produce a personalized set of recommendations for use in emails. ## What an abandoned browse automation requires * The `locale` must match a product feed in Customer Engagement. Events without a matching feed are filtered out. * At least one SKU must match a product in that feed (usually via `g:id`). Products that don't match are left out of the email; if **no** SKUs match, the automation doesn't trigger at all. * The session must contain no cart events. See [sessions and sessionId](#sessions-and-sessionid) below. ## Sessions and sessionId Every session has a unique `sessionId`. Customer Engagement uses it to make sure abandoned cart and abandoned browse never both trigger for the same session: * If `cart` and `productView` events share a `sessionId`, only the **abandoned cart** automation triggers. * **Abandoned browse** triggers only when no cart event was recorded for that `sessionId`. The tracking script adds `sessionId` automatically. When you use the tracking API, you must add it to every payload yourself. The `sessionId` is not strictly mandatory for cart events, but it is required if you use abandoned browse and abandoned cart together. Not using it can result in both automations firing for the same session. ## Using the tracking script Call `productview()` every time a product is shown to the visitor, whether or not they have been identified. It sends an HTTP POST to the Collect endpoint. ```javascript theme={null} va("productview", { "categoryName": "Women / Armour / Greaves", "itemId": "123XYZ", "contactId": "afa7625d-2e97-4667-b4c1-ad3b01194bbb", "locale": "sv-SE" }); ``` The product's category, for example `Men / Sweaters / Cardigan`. The product identifier, matched against SKU in Customer Engagement. The locale of the site, as an IETF language tag such as `sv-SE`. Must match a product feed locale in Customer Engagement, otherwise abandoned browse cannot trigger. The Customer Engagement contact ID. If omitted, the `_vaI` cookie is used as a fallback. ## Using the tracking API You can submit product views directly via the API instead of implementing the tracking script. Be aware that a lot of what the script handles out of the box then becomes your responsibility: * **Identification.** The API requires a `contactId`, so only identified product views can be registered. You must identify visitors arriving from Customer Engagement email links yourself, by reading the `vtid` query parameter and passing it as `contactId`. * **Sessions.** You must include a `sessionId` in every payload. Abandoned browse will not work without it. ## Rules to follow * **Send product views for anonymous visitors too.** A view recorded anonymously is linked to the contact once they are identified. Never restrict `productview()` to logged-in visitors. * **A "product view" is any time a product is shown**, not just a product page visit. A quick-look popup counts. * **`itemId` must match a SKU in Customer Engagement**, so the view can be enriched with product and transaction data. * **One event per unique SKU.** A page may fire `productview()` several times if it represents several SKUs, but never twice for the same SKU. ## Common scenarios The link from the Customer Engagement email carries `vtid`, so the script identifies the visitor before the product view is sent. Populate the event like this: * `categoryName` — the current product category, e.g. `Computer accessories > Printers > Toners` * `itemId` — the current product SKU, e.g. `549852` * `locale` — the site's current language/market, e.g. `sv-SE` * `contactId` — the visitor's Customer Engagement contact ID (`vtid` is used as a fallback) Send the event exactly as above, with one difference: * Do **not** populate `contactId` The view is recorded against the browser's client ID and connected to a contact later, if and when the visitor is identified. A product display page covering several sizes, colours or variants needs a decision about what counts as a view. * **Preferred:** send one `productview()` when a specific variant is selected, rather than one per size. * If you want to record the view regardless of variant, send a single representative SKU from the page. * As a last resort, send all variants — but be aware this risks abandoned browse emails filled with many variants of the same product. # Verifying and troubleshooting web tracking Source: https://developer.voyado.com/docs/tracking/verifying-web-tracking Step-by-step checks for the tracking script, cart events and product views, plus the most common implementation and identification errors. Work through these checks before going live, and return to them when tracking data looks wrong in Customer Engagement. ## Verify the script and cookies 1. Go to your e-com site. 2. Open the developer console and go to **Sources**. 3. Search for `analytics` and confirm you are loading the expected script version, and the right one for your environment. 4. Go to **Application → Cookies** and select the site running the tracking. 5. If you are identified or logged in, look for the `_vaI` cookie. 6. If you are not identified, look for the `_va` cookie. The _vaI cookie Tracking script used ## Verify product views 1. Go to your e-com site. 2. Open the developer console, go to **Network** and select the **Fetch/XHR** tab. 3. Visit any product on the site. 4. You should see a call to the Collect endpoint. 5. In the payload, `te` should be your tenant/account name. 6. `t` (type) should be `productview`. 7. If you are identified, check that `ci` holds the same ID as the `_vaI` cookie — and nothing else. The `locale` in a product view payload does not have to match the locale in your cart payloads, and it is not used to identify anything. It does, however, need to match a product feed for abandoned browse to trigger. Finding the payload ## Verify cart changes 1. Go to your e-com site. 2. Open the developer console, go to **Network** and select the **Fetch/XHR** tab. 3. Place an item in the cart. 4. You should see a call to the Collect endpoint. 5. In the payload, `t` (type) should be `cart`. Check the `items[]` array against what's actually in your cart. 6. If possible, complete the purchase. 7. A new Collect event should fire with an empty cart, confirming the purchase was registered. Verify tracking cart changes 1 Verify tracking cart changes 2 ## Checklist: script and identification The script must load on landing pages, category pages and any other non-product page. If an email link lands on a page without the script, identification fails. Confirm that simply loading or navigating between pages sends no events. Events should be sent when no `_vaI` cookie exists. Customer Engagement connects anonymous events to a contact once they are identified. Arrive via an email link and confirm events are sent and `_vaI` is set correctly. A visitor can be identified without being logged in. Log in and confirm that `setContactId()` sets `_vaI` correctly and that events are sent with the identification in place. `_vaI` should be set whether or not the URL has a trailing slash. * With: `www.site.se/?vtid=LTq-P1eRKU2s8CWc5KPe5w` * Without: `www.site.se?vtid=LTq-P1eRKU2s8CWc5KPe5w` Then navigate further into the site and confirm `_vaI` is not cleared. `_vaI` should update when a different `vtid` is used. Set `vtid` to `a4fa7a46-23b0-4523-b09c-5462053e7627`, then change it to `3fbe3a2d-9157-4d29-acf0-259ce4a3dee7` and confirm the cookie follows. Abandoned browse will not trigger without it. The script adds it automatically; if you use the tracking API, you must add it yourself. Running the production script in staging, or the staging script in production, means no tracking at all. ## Checklist: cart tracking The cart's locale must match both a product feed in Customer Engagement and the language version of the site, if you run a multimarket site. Try every way of changing the cart: add an item, remove an item, change quantity, empty it. Does the quantity update when the same item is added a second time? What happens when you remove the last item? Carts can usually be updated from the product page and a popup as well — and sites built as SPAs tend to behave inconsistently. Checkout usually behaves differently from the rest of the site. Is there a difference between setting quantity to zero and removing the item? What happens when all items are removed? Can you add new items from checkout? Does any of it differ with and without `vtid`? Update the cart on both the checkout page and elsewhere, moving back and forth. Confirm no incorrect or duplicate events are sent. Without this, the cart stays populated in Customer Engagement and can trigger an abandoned cart email to someone who already bought. ## Checklist: product views and products of interest To detect whether a purchase followed a product view, Customer Engagement matches the `itemId` from the view against the SKU in the article register on the receipt. A product page should fire one `productview` per unique `itemId`/SKU. Several events on one page are fine only if they are for different SKUs. Product pages can be reached from the landing page, checkout, another product page, the menu or the cart. Test each route. Sites built as SPAs don't do full page loads, which frequently causes events to be missed. ## Common implementation errors * **A cart event isn't sent for every update.** Every change — add, remove, quantity change, empty — must generate a cart event to the Collect endpoint. These changes happen in different contexts (product page, cart popup or sidebar, checkout) that often behave differently. * **The `cartRef` changes for the same cart.** `cartRef` must be unique per cart and identical across all of that cart's events. Invalid or changed `cartRef` values on remove-product and empty-cart events are a frequent cause of missing abandoned cart emails. * **Duplicate cart or product view events.** One cart update should produce exactly one cart event, and actions that aren't cart updates (such as page loads) should produce none. A product page should produce no more than one `productview` per unique SKU. ## Common identification errors * **The `_vaI` cookie is cleared after being set by `vtid`.** A visitor identified by an email link is identified without being signed in. Calling `setContactId()` with an empty or null value in that state wipes the identification. Only call it when your site has actually identified someone. * **No cart or product view events before checkout or sign-in.** Don't limit events to the checkout page, or to signed-in visitors. Anonymous events are connected retroactively. * **`_vaI` isn't set when the email link ends in a slash.** Some websites strip trailing slashes without preserving the query string, so `vtid` is lost. For example `www.mysite.com/myproduct/?vtid=uaz2QuT9NEez_eAsr8hEeQ` becomes `www.mysite.com/myproduct`.