We'll show you how to use the Content API to get placement content for API implementations of the Coveo Merchandising Hub.
_qubitTracker
All content is served from a single GraphQL API:
HTTP method - POST
Content type - application/json or text/plain - see below
TIP: A GraphQL playground interface is available at https://api.qubit.com/placements.
You will usually send GraphQL POST requests as "application/json". However, if the request is being made cross-origin, this will cause the browser to make a preflight CORS request (with the HTTP OPTIONS method) and will increase overall latency for the query.
To mitigate this, the Qubit Content API also supports content being sent as "text/plain", which will not cause the browser to send a preflight request.
You should send GraphQL requests as a JSON object with two fields, a query
string, and a variables
object.
Example request:
{
"query": "<GraphQL query>",
"variables": { ... }
}
See GraphQL argument reference for more information on the "variables" object.
The following example shows how you can get content for a simple placement with three fields—an image, a URL, and some text.
Example GraphQL query:
query PlacementContent (
$mode: Mode!
$placementId: String!
$attributes: Attributes!
$resolveVisitorState: Boolean!
) {
placementContent(
mode: $mode
placementId: $placementId
attributes: $attributes
resolveVisitorState: $resolveVisitorState
) {
content
callbackData
visitorId
}
}
Variables:
{
"mode": "LIVE",
"placementId": "IMAmsWP4T827kUWUPEQEqA",
"attributes": {
"visitor": {
"id": "qs9rlhhzets-0k1g9i70n-dzq3rto",
"url": "https://example.com",
},
"user": {},
"product": {},
"view": {
"currency": "EUR",
"type": "home",
"subtypes": [],
"language": "en-gb"
}
}
}
Example response:
{
"data": {
"placementContent": {
"content": {
"image": "https://cdn.example.com/foo.jpg",
"link": "https://example.com/foo",
"message": "Foo bar!"
},
"callbackData": "aaa-111-bbb-222-ccc-333"
}
}
}
See Response schema reference for more details on the response object.
The Qubit Visitor Id is a device-specific identifier, usually stored in a first-party cookie. Qubit relies on the Visitor Id to track user activity around a site and measure campaign performance.
Therefore, it is essential that the Visitor Id is persisted and passed correctly to the Content API.
Qubit's Smartserve JavaScript bundle will create the Visitor Id if it does not exist; this is typically the case for new visitors to the site or users that have cleared their cookies.
How you work with the visitor Id depends on where you are calling the Content API from. To use a self-generated identifier, see Using a self-generated Visitor Id.
When calling the Content API from the browser, smartserve exposes the value of visitor Id via the getVisitorState
method. This method will be provided to your placement code or can also be reached using _qubit.jolt.getVisitorState()
.
Calling the Content API from the server is more complicated because you also need to manage site visitors that don't yet have a visitor Id.
When this happens, you will need to pass an empty string for the visitor.id
attribute into your Content API request. The Content API will then generate a new Visitor Id and return it to you in the response body.
You can see this in the following example response.
Example GraphQL query:
query PlacementContent (
$mode: Mode!
$placementId: String!
$attributes: Attributes!
$resolveVisitorState: Boolean!
) {
placementContent(
mode: $mode
placementId: $placementId
attributes: $attributes
resolveVisitorState: $resolveVisitorState
) {
content
callbackData
visitorId
}
}
Variables:
{
"mode": "LIVE",
"placementId": "IMAmsWP4T827kUWUPEQEqA",
"attributes": {
"visitor": {
"id": "",
"url": "https://example.com",
},
"user": {},
"product": {},
"view": {
"currency": "EUR",
"type": "home",
"subtypes": [],
"language": "en-gb"
}
}
}
Example response:
{
"data": {
"placementContent": {
"content": {
"image": "https://cdn.example.com/foo.jpg",
"link": "https://example.com/foo",
"message": "Foo bar!"
},
"callbackData": "aaa-111-bbb-222-ccc-333",
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}
You will then need to persist this new Visitor Id by using the Set-Cookie
HTTP header in the response to the user's browser. You should set the cookie using the following configuration:
Cookie name - _qubitTracker
Path - /
Domain - the website domain, with a leading .
e.g. .example.com
Expiry - one year from the time of the request
HttpOnly - no (we need to read it from JavaScript)
It is possible to use your own user identifier as the visitor Id instead of using the one generated by Qubit. You can do this both browser- and server-side.
When setting a self-generated Id from the browser, you'll need to set the _qubitTracker
cookie before the Smartserve script loads; otherwise, Smartserve will generate its own Id.
You can also edit your prescript to delay smartserve from loading until you can set the cookie.
You'll only need to do this for visitors that do not have this cookie value already set.
The cookie should have the following configuration:
Cookie name - _qubitTracker
Path - /
Domain - the domain of the website, with a leading .
e.g. .testdomain.com
Expiry - one year from the time of the request
HttpOnly - no (we need to read it from JavaScript)
When setting a self-generated Id from the server, you'll need to set the _qubitTracker
cookie using the Set-Cookie HTTP header in the response to the visitor's browser.
You'll only need to do this for visitors that do not have this cookie value already set.
The cookie should have the following configuration:
Cookie name - _qubitTracker
Path - /
Domain - the website domain, with a leading .
e.g. .testdomain.com
Expiry - one year from the time of the request
HttpOnly - no (we need to read it from JavaScript)
The Content API response contains a callbackData
blob allowing you to form callback URLs. Calling these URLs will cause tracking events to be sent by the Content API, allowing Qubit to measure campaign performance.
To execute a callback, you need to make an HTTP GET request to the URL. The callbackData
blob is a compressed set of campaign attributes used for performance measurement that cannot be interpreted directly.
A typical Content API response will look like this:
{
"data": {
"placementContent": {
"content": { ... },
"callbackData": "aaa-111-bbb-222-ccc-333"
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}
A typical callback URL will look like this:
https://api.qubit.com/placements/cb?d={callbackData}&t={eventType}...
The following query parameters are used:
d - required, contains the callbackData
blob from the Content API response
t - optional, contains the event type. Supported event types are impression
and clickthrough
. If unspecified, an impression
event is recorded
ts - recommended, contains the actual timestamp of the event. The format is an integer number of milliseconds since January 1, 1970, equivalent to Date.now()
in JavaScript. If unspecified, the event is recorded with the timestamp of the callback URL request
gcid - optional, contains the Google Analytics client Id of the actual visitor. Only used when GA tracking of campaigns is enabled for the property.
imp - optional, contains the placement's unique implementation Id. This parameter is automatically appended to injected placements and not used for placements delivered via API.
debug - optional, causes the callback endpoint to return debugging information formatted as JSON. You should only use this for development and debugging purposes. Using this parameter doesn't stop the events from being emitted. There are three modes:
debug without any value returns decoded callback data
debug=qp returns the QP event being emitted
The response to the callback GET request could be one of the following:
HTTP code 200 and an empty body for requests that were parsed without error
HTTP code 200 and a JSON body when the debug mode is being invoked
The impression callback should be executed as soon as the placement content is in view for the visitor. If you are calling the Content API server-side, you should pass the callback data to the browser so that the callback URL request is executed via JavaScript when a visitor sees the placement.
The clickthrough callback should be executed when the user clicks the placement's primary CTA. If clicking the CTA will cause the browser to navigate, it is important to execute the callback before navigation occurs; otherwise, it will cancel the callback HTTP request.
If there are many clickable elements, for example, in a product recommendations carousel, the clickthrough callback should be executed for a click on any of the elements.
You can report on individul products seen and clicked on by adding product Ids to the basic callback URL structure. This can be done in addition to the basic callbacks to supply more granular analytics on recommendations placements.
Add product IDs to the query string as query parameters - &p=id
Multiple product Ids are supported - &p=id&p=id
The Ids must be URL-encoded
"URL-encoded" means serializing the Ids according to the URL Standard, Section 5.2, step 3.4 on encoding values. For example, that means that spaces, single and double quotes, among other characters have to be percent-encoded.
This is typically done for product recommendations placements but can also be done in personalized content placements containing a "product" element in their schema.
A callback URL with added product Ids will look like this:
https://api.qubit.com/placements/cb?d={callbackData}&t={eventType}&p={product-id}
WARNING: Calling a callback URL with product IDs should be done in addition to the basic URL and not treated as a replacement. In practice, this means that an impressio or click would mandate at least two callback calls.
There are several scenarios where the Content API may not return content to be rendered into the page:
If the business user has not published a campaign
If the business user has paused an existing campaign
If the business user has published a campaign with a 50% or 95% traffic split and the visitor is in the control group
Example response:
{
"data": {
"placementContent": {
"content": null,
"callbackData": "aaa-111-bbb-222-ccc-333"
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}
You must handle this scenario. There are a couple of options:
If the placement is replacing an existing piece of content on the page, then the original content should be shown:
If the placement is inserting an entirely new piece of content, then it is best to show nothing and to call the impression callback:
placementId (string) - unique Id of the implemented placement
mode (enum) - can be either LIVE
or PREVIEW
campaignId (string) - in PREVIEW
mode, identifies the campaign to preview
PREVIEW
mode, identifies the campaign experience to previewWhen mode is set to LIVE
, the request will return the current live content for a placement.
When mode is set to PREVIEW
, previewOptions
(see below) is also required. The request will return the latest draft of a specific campaign or experience. When a business user uses the preview functionality in the Qubit application, the URL they will land on will have the following format:
https://<baseUrl>?qb_opts=preview,remember&qb_placement_id=<placementId>&qb_campaign_id=<campaignId>&qb_experience_id=<experienceId>&qb_remember=1&qb_mode=PREVIEW
previewOptions.campaignId (string)
When provided in PREVIEW
mode by itself (i.e. previewOptions.experienceId
is not specified), a campaign level preview will be requested. This means that the latest draft of the campaign will be returned. When previewing through the Qubit platform, this will be passed through the url search params as qb_campaign_id
.
previewOptions.experienceId (string)
When provided in PREVIEW
mode along with previewOptions.campaignId
, an experience level preview will be requested. This means that the latest draft of the experience will be returned. When previewing through the Qubit platform, this will be passed through the url search params as qb_experience_id.
visitorId (string)
The unique Qubit visitor Id. See Handling the Qubit Visitor Id for details of how to handle this argument.
resolveVisitorState (boolean)
Attribute (JS Data Type) | Details |
---|---|
attributes.visitor.conversionNumber (int) | Inferred from attributes.visitor.id based on stored history |
attributes.visitor.sessionNumber (int) | Inferred from attributes.visitor.id based on stored history |
attributes.visitor.lifetimeValue (float) | Inferred from attributes.visitor.id based on stored history |
attributes.visitor.firstViewTs (int) | Inferred from attributes.visitor.id based on stored history |
attributes.visitor.lastViewTs (int) | Inferred from attributes.visitor.id based on stored history |
attributes.visitor.firstConversionTs (int) | Inferred from attributes.visitor.id based on stored history |
attributes.visitor.lastConversionTs (int) | Inferred from attributes.visitor.id based on stored history |
attributes.location.areaCode (string) | Inferred from attributes.visitor.ipAddress |
attributes.location.cityCode (string) | Inferred from attributes.visitor.ipAddress |
attributes.location.regionCode (string) | Inferred from attributes.visitor.ipAddress |
attributes.location.countryCode (string) | Inferred from attributes.visitor.ipAddress |
Values for the following attributes must be set on all pages:
Attribute (JS Data Type) |
---|
attributes.visitor.ipAddress (String) |
attributes.visitor.url (String) |
attributes.visitor.userAgent.userAgentString (String) |
attributes.view.type (String) |
attributes.view.subtypes (String array) |
attributes.view.currency (String) |
attributes.view.language (String) |
attributes.user.id (String) |
attributes.user.email (String) |
Values for the following attributes must be set on product detail pages:
Attribute (JS Data Type) |
---|
attributes.product.id (String) |
attributes.product.name (String) |
attributes.product.categories (String array) |
Values for the following attributes must be set on basket and checkout pages:
Attribute (JS Data Type) |
---|
attributes.basketProducts.[].id (string) |
attributes.basketProducts.[].name (string) |
attributes.basketProducts.[].categories (string array) |
Values for the following attribute must be set on confirmation pages:
Attribute (JS Data Type) |
---|
attributes.transactionProducts.[].id (String) |
attributes.transactionProducts.[].name (String) |
attributes.transactionProducts.[].categories (String array) |
All attributes are assumed to be children of the attributes
argument.
Attribute (JS data type) | When to set | Example |
---|---|---|
visitor.id (String) | Always | aaaa-bbbb-cccc-dddd |
visitor.ipAddress (String) | Always | 0.0.0.0 |
visitor.url (String) | Always | |
visitor.userAgent.userAgent (String) | Always | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 |
view.type (String) | Always | Can be one of |
view.subtypes (String array) | Always |
|
view.currency (String) | Always | USD |
view.language (String) | Always | en-us |
user.id (String) | Always, if available | eabc123 |
user.email (String) | Always, if available | user@qubit.com |
user.loyalty.membershipType (String) | Always, if available | gold |
visitor.conversionNumber (Int) | Always, if available. | 1 |
visitor.sessionNumber (Int) | Always, if available. | 1 |
visitor.lifetimeValue (Float) | Always, if available. | 1000.00 |
visitor.firstViewTs (Int) | Always, if available. | 1613681906471 |
visitor.lastViewTs (Int) | Always, if available. | 1613681906471 |
visitor.firstConversionTs (Int) | Always, if available. | 1613681906471 |
visitor.lastConversionTs (Int) | Always, if available. | 1613681906471 |
location.areaCode (String) | Always, if available. | 124462 |
location.cityCode (String) | Always, if available. | 12997 |
location.regionCode (String) | Always, if available. | 65 |
location.countryCode (String) | Always, if available. | CA |
product.id (String) | On product detail pages | abc123 |
product.name (String) | On product detail pages | Red Dress |
product.categories (String array) | On product detail pages |
|
basketProducts.[].id (String) | On any page, for each product in a visitor’s basket | abc123 |
basketProducts.[].name (String) | On any page, for each product in a visitor’s basket | Red Dress |
basketProducts.[].categories (String array) | On any page, for each product in a visitor’s basket |
|
transactionProducts.[].id (String) | On the confirmation page, for each product in a visitor’s transaction | abc123 |
transactionProducts.[].name (String) | On the confirmation page, for each product in a visitor’s transaction | Red Dress |
transactionProducts.[].categories (String array) | On the confirmation page, for each product in a visitor’s transaction |
|
The response schema will depend on the GraphQL query provided.
Example GraphQL query:
query PlacementContent (
$mode: Mode!
$placementId: String!
$attributes: Attributes!
) {
placementContent(
mode: $mode
placementId: $placementId
attributes: $attributes
) {
content
callbackData
visitorId
}
}
Example response:
{
"data": {
"placementContent": {
"content": {...},
"callbackData": "aaa-111-bbb-222-ccc-333",
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}
The data.placementContent.content
object will have a different schema depending on the placement's solution type.
For personalized content, the placement's schema is set in our placement builder and is fully customizable:
Example placement query response:
{
"data": {
"placementContent": {
"content": {
"message": "Hello there",
"link": "https://example.com/cta",
"image": "https://www.instantprint.co.uk/umbraco-media/6627/indoorevents-category-banner-web.jpg"
},
"callbackData": "aaa-111-bbb-222-ccc-333",
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}
For Product recommendations, the placement schema is mostly fixed; you can define the minimum and maximum number of products to return when configuring the placement in the placement builder UI.
Example placement query response:
{
"data": {
"placementContent": {
"content": {
"headline": "Recommended for you",
"recs": [
{
"details": {
"categories": [
"Dresses"
],
"images": [ "https://cdn.shopify.com/s/files/1/0525/2343/4176/products/premium-black-floral-embroidered-maxi-dress_400x.jpg?v=1610720134"
],
"language": "en",
"id": "6152963096768",
"views": 7059,
"name": "Black floral summer dress",
"stock": 9641,
"size": null,
"unit_sale_price": 300,
"currency": "gbp",
"base_currency": "GBP",
"unit_price": 300,
"url": "https://qubit-demo.myshopify.com/products/short-sleeve-t-shirt",
"description": "Casual summer dress, cotton",
"category": null,
"subcategory": null,
"locale": "en-gbp",
"image_url": "https://cdn.shopify.com/s/files/1/0525/2343/4176/products/premium-black-floral-embroidered-maxi-dress_400x.jpg?v=1610720134"
}
},
{
"details": {
"categories": [
"Dresses"
],
"images": [
"https://cdn.shopify.com/s/files/1/0525/2343/4176/products/Midi_Wrap_Dress_049aed2e-601d-4338-b7ef-debc0d74f2a1_400x.jpg?v=1610720082"
],
"language": "en",
"id": "6134818603200",
"views": 7071,
"name": "Astrid Wool-Cashmere Midi Wrap Dress",
"stock": 9644,
"size": null,
"unit_sale_price": 448,
"currency": "gbp",
"base_currency": "GBP",
"unit_price": 448,
"url": "https://qubit-demo.myshopify.com/products/astrid-wool-cashmere-midi-wrap-dress",
"description": "This is a product",
"category": null,
"subcategory": null,
"locale": "en-gbp",
"image_url": "https://cdn.shopify.com/s/files/1/0525/2343/4176/products/Midi_Wrap_Dress_049aed2e-601d-4338-b7ef-debc0d74f2a1_400x.jpg?v=1610720082"
}
}
]
},
"callbackData": "aaa-111-bbb-222-ccc-333",
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}
Each element in the data.placementContent.content.recs.[]
array contains metadata that is defined in the product feed.
For Product badging, the placement schema is entirely fixed.
Example placement query response:
{
"data": {
"placementContent": {
"content": {
"message": "Selling Fast",
"imageUrl": "https://dd6zx4ibq538k.cloudfront.net/static/images/5797/89912f668090747c00863e3fe21f1ff3_256_256.png"
},
"callbackData": "aaa-111-bbb-222-ccc-333",
"visitorId": "qs9rlhhzets-0k1g9i70n-dzq3rto"
}
}
}