Get placement content

In this article...

We'll show you how to use the Content API to get placement content for API implementations of the Coveo Merchandising Hub.

Glossary

  • Solution - high-level personalization type, and one of Personalized content, Product recommendations, or Product badging
  • Tag - way to group placements within Qubit's UI. A placement can have multiple tags and be named in any way to make it more understandable for the business user. Typically, tags are 1-to-1 with a page type, for example, "Homepage", "Product details page (PDP)", or "Product listing page (PLP)"
  • Triggers - rules attached to a placement to determine when and where the placement is rendered. These only affect placements delivered client-side
  • Placement - specific area on a site designated to render content served by Qubit's Content API with a set schema. The placement can map to one of our three solutions
  • Campaign - means of deploying content into a placement
  • Experience - when deploying Personalized content campaigns, a pairing of an audience with content—"who sees what"
  • Audience - group of people, as defined in the Merchandising Hub
  • Qubit Visitor Id - Qubit's device-level identifier for a visitor, stored in a cookie called _qubitTracker
  • Smartserve - Qubit's JavaScript bundle, containing event tracking, segmentation, and client-side experimentation code

Endpoint

All content is served from a single GraphQL API:

TIP: A GraphQL playground interface is available at https://api.qubit.com/placements.

Content types

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.

Body format

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.

Getting content for a single placement

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.

Handling the Qubit Visitor Id

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.

Browser-side

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

Server-side

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)

  • Secure - maybe, depending on the website (the only reason not to would be if there are any HTTP-only sections of the site)

Using a self-generated Visitor Id

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.

Browser-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)

  • Secure - maybe, depending on the website (the only reason not to would be if there are any HTTP-only sections of the site)

Server-side

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)

  • Secure - maybe, depending on the website (the only reason not to would be if there are any HTTP-only sections of the site)

Executing callback URLs

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

    • debug=ga returns the GA 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

  • HTTP code 400 and a plain text error message in the body for requests that failed to parse. Example error messages are "invalid data" or "invalid type"

Impressions

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.

Clickthroughs

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.

Reporting on individual products

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

  • The Ids must match those in your product feed and QProtocol events (product.productId)

"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.

Handling no content for a placement

There are several scenarios where the Content API may not return content to be rendered into the page:

  1. If the business user has not published a campaign

  2. If the business user has paused an existing campaign

  3. If the business user has published a campaign with a 50% or 95% traffic split and the visitor is in the control group

  4. If the business user has published a campaign and the visitor does not match any audience in it

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:

    • Impression and clickthrough callbacks should still be called in this scenario to compute A/B testing metrics. They should be called under conditions as similar as possible to the "content returned" case to ensure the fairness of A/B tests
  • If the placement is inserting an entirely new piece of content, then it is best to show nothing and to call the impression callback:

    • The impression should be fired under conditions as similar as possible to the "content returned" case to ensure the fairness of A/B tests.

GraphQL argument reference

  • 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

  • experienceId (string) - in PREVIEW mode, identifies the campaign experience to preview

When 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)

Full attribute reference

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 basket, category, checkout, confirmation, home, product, search

view.subtypes (String array)

Always

[“womens”, “dresses”]

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.

If unavailable, it can be resolved if resolveVisitorState is set to true

1

visitor.sessionNumber (Int)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

1

visitor.lifetimeValue (Float)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

1000.00

visitor.firstViewTs (Int)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

1613681906471

visitor.lastViewTs (Int)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

1613681906471

visitor.firstConversionTs (Int)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

1613681906471

visitor.lastConversionTs (Int)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

1613681906471

location.areaCode (String)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

124462

location.cityCode (String)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

12997

location.regionCode (String)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

65

location.countryCode (String)

Always, if available.

If unavailable, it can be resolved if resolveVisitorState is set to true

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

[“Women’s > Dresses”]

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

[“Women’s > Dresses”]

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

[“Women’s > Dresses”]

Response schema reference

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.

Personalized content

For personalized content, the placement’s schema is set in our placement builder and is fully customizable:

pc schema

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"
    }
  }
}

Product recommendations

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.

Product Badging

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"
    }
  }
}
Last updated: April 2022
Did you find this article useful?