Getting Started with Kevel Forecast

How to check inventory availability and forecast campaign delivery with Kevel

Kevel Forecast provides the insights you need to see what inventory is available and how a campaign will deliver. You have the ability to predict future ad traffic levels and campaign inventory availability using an unlimited number of targeting variables, including geo, keyword, key-value, user segment, or frequency capping.

For Forecast use cases and types, check out the Forecast Overview.

Getting Started

Asynchronous API

It’s important to note that the Kevel Forecast API is asynchronous. You can request a forecast to be performed via a POST to https://api.kevel.co/v1/forecaster/ with the appropriate API key and you always get back a response such as the following:

{
  "id": "8a6f6bf5-f823-409f-8746-9ae047190cc3",
  "status": "enqueued",
  "progress": 0.0,
  "desc": "Forecast is queued"
}

This object contains an id that you can use in a GET request to https://api.kevel.co/v1/forecaster/{id} to get the forecast result back. Forecasts can take a few seconds to some minutes to complete, depending on the amount of ads involved, how far into the future we’re forecasting and the sampling level. A complete forecast result is one whose response has the status β€œfinished”. For example, the following is a response to a finished forecast:

{
  "resultStatus": "success",
  "result": {
    "total": {
      "329797406": {
        "impressions": 0,
        "uniqueUsers": 0
      },
      "336416380": {
        "impressions": 76792,
        "uniqueUsers": 31718
      }
    }
  },
  "id": "8a6f6bf5-f823-409f-8746-9ae047190cc3",
  "status": "finished",
  "progress": 100.0,
  "desc": "Forecast is finished"
}

Forecast dates

Forecast requests require an end date and, optionally, in some forecast types, the start date as parameters. These dates can suffer minor adjustments to reflect the availability of future inventory and existing campaigns' information.
The dates effectively used by the forecast are returned in its results as forecastStartDate and forecastEndDate.

Grouping-by and time zone considerations

In GroupBy you can define any key you can use in custom targeting, as well as some custom ones like $datetime.date. For example, the following payload triggers a forecast request for existing ads using sampling level 3 and grouping results by site and date:

{
  "Type": "existing",
  "EndDate": "2023-03-20",
  "Params": {
    "Sampling": 3,
    "GroupBy": ["$site", "$datetime.date"],
    "TimeZone": "UTC"
  }
}

Forecast results for requests with a GroupBy parameter have an additional field in the result object, called grouped, where you can see results for each ad grouped by the possible values of each tuple of grouping keys. The TimeZone parameter in the Params field specifies in which time zone should time-related fields in the GroupBy be considered in, defaulting to UTC if no TimeZone is set.

{
  "resultStatus": "success",
  "result": {
    "total": {
      "1234567891": {
        "impressions": 0,
        "uniqueUsers": 0
      },
      "1234567890": {
        "impressions": 76136,
        "uniqueUsers": 31680
      },
      "1234567899": {
        "impressions": 294776,
        "uniqueUsers": 97152
      },
      (...)
    },
    "grouped": [
      {
        "key": {
          "$site": 1234567,
          "$datetime.date": "2023-03-14"
        },
        "values": {
          "1234567891": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          "1234567890": {
            "impressions": 38488,
            "uniqueUsers": 17464
          },
          "1234567899": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          (...)
        }
      },
      {
        "key": {
          "$site": 1234567,
          "$datetime.date": "2023-03-14"
        },
        "values": {
          "1234567891": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          "1234567890": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          "1234567899": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          (...)
        }
      },
      {
        "key": {
          "$site": 1234568,
          "$datetime.date": "2023-03-14"
        },
        "values": {
          "1234567891": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          "1234567890": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          "1234567899": {
            "impressions": 0,
            "uniqueUsers": 0
          },
          (...)
        }
      },
      (...)
    ]
  },
  "id": "33665016-a0e6-4bc0-98a2-ad14aca86053",
  "status": "finished",
  "progress": 100.0,
  "desc": "Forecast is finished"
}

🚧

Large result set warning

Note that large combinations of grouping keys can result in very large forecast results. Settings within the Params object can be used with any forecast type.
When no value is available for the specified GroupBy key in the predicted impression, the results will be grouped by the value null for that key.

Group-by array fields

When the group-by field is an array, and not individual values, the default behavior is for results to be shown for the set of values and not for each individual value. For example, it is common for a single ad opportunity to have multiple AdTypes or Keywords. A group-by $keywords results in a group for each set of keywords, e.g. forecasted metrics for ad opportunities with all three keywords: ["bunny","business","tacocat"]will be presented as a single group-by dimension. This behavior ensures that you can sum all the individual group-by ad opportunities forecasted and they will add up to the total of ad opportunities.

But sometimes it's desirable to break down by individual keyword or ad type. Forecast supports this by having the ability to parse out any array field by appending the suffix []to the field name, e.g. $keywords[] or $adTypes[]. To make it simpler, it also supports certain aliases for known standard fields, namely $keywordin place of $keywords[] and $adType in place of $adTypes[]. See the details in the table for all available aliases in Forecast.

🚧

You cannot sum individual results of a group-by individual array elements

The sum of all individual results in a group by when using array fields individual elements will not add up to the total results. For example, there is 1 ad impression available for keywords ["bunny","business","tacocat"]and when broken down by $keyword they will show 1 impression each but the total impressions is still just 1, and if one ad is booked against any of the three keywords it will remove availability for the other two.

Group-by fields

The following keys are available for use in the GroupBy parameter:

NameDescriptionExamples
$acceptLanguageLanguage set by a user's browser (also known as "locale")."en-US,en;q=0.5"
$adTypeAlias for $adTypes[]1234
$adTypesSet of Ad Type Ids present in the requests.[1234,5678]
$content.customContentDB targeting against a Schema/ContentKey. Group by schema using:$content.custom.<schema>.Schema: Song
Content: {"title": "Somebody To Love"}
$datacenterSet to true if the request originates from a known datacenter.true
$datetime.dateCalendar day, considering the Timezone set in the TimeZone parameter, in the YYYY-MM-DD format."2024-01-22"
$datetime.dayofweekDay of week, considering the Timezone set in the TimeZone parameter. Starts on Sunday, range is 1-7.2
$datetime.hourHour of day, considering the Timezone set in the TimeZone parameter.14
$datetime.weekWeek of year, according to ISO 8601, considering the Timezone set in the TimeZone parameter.8
$datetime.monthMonth of year, considering the Timezone set in the TimeZone parameter.2
$device.brandNameThe device brand."Nokia"
$device.modelNameThe device model."N95"
$device.marketingNameIn addition to Brand and Model, some devices have a marketing name."Nokia 8800 Scirocco"
$device.osOperating system name."iOS"
$device.osVersionOperating system version.{"string":"17.0","major":17,"minor":0}
$device.osVersion.stringFull representation of the version string."17.0"
$device.osVersion.majorMajor version number (group of digits before dot) if available. Defaults to 0 if no number represented.17
$device.osVersion.minorMinor version number (group of digits after dot) if available. Defaults to 0 if no number represented.0
$device.browserThe device browser."Safari"
$device.browserVersionThe device browser's version."17.0"
$device.resolutionWidthScreen width in pixels.640
$device.resolutionHeightScreen height in pixels.1136
$device.maxImageWidthImage's maximum viewable width in pixels.320
$device.maxImageHeightImage's maximum viewable height in pixels.568
$device.physicalScreenWidthScreen width in millimeters.50
$device.physicalScreenHeightScreen height in millimeters.89
$device.formFactorEither "desktop", "phone", or "tablet"."phone"
$device.deviceIdentifiedWhether WURFL matches the user agent with a known devicetrue
$divNameDiv name (DOM identifier) present in the ad request."div0"
$ipUser's IP address."127.0.0.1"
$keywordAlias for $keywords[]"bunny"
$keywordsThe set of Keywords present in the ad requests.["bunny","business","tacocat"]
$location.countryCodeTwo-character country code."US"
$location.contryNameCountry name."United States"
$location.dmaCodeNumeric Nielsen designated market area code (US only).560
$location.metroCodeMetro Code (US only).20500
$location.cityCity name."Durham"
$location.regionTwo or three-character code for the state, region, or province."NC"
$location.subRegionA two or three-character code for a subregion. Certain countries only."CAM"
$referralUrlThe referrer of the URL that makes an ad request."https://kevel.com"
$regs.gdprTrue if the request is GDPR-regulated.false
$consent.gdprTrue if the request has GDPR consent.true
$site.idID of the Kevel site.1234567
$urlThe URL of the page that makes an ad request."https://kevel.com"
$user.blockedItems.advertiserAlias for $user.blockedItems.advertisers[]
$user.blockedItems.campaignAlias for $user.blockedItems.campaigns[]
$user.blockedItems.creativeAlias for $user.blockedItems.creatives[]
$user.blockedItems.flightAlias for $user.blockedItems.flights[]
$user.customThe set of user custom fields present at the time of the ad requests.
Group by inner fields using: $user.custom.<field>.
{"age":"20","gender":"female","categories":["food","fashion"]}
$user.custom{}Expands each value of key value pairs of $user.custom{"gender": "Female"}
$user.custom.<field>[]If the content element of a user custom property is an array field, you can break by individual element this format, e.g.$user.custom.categories[]"food"
$user.interestAlias for $user.interests[]"fashion"
$user.interestsThe set of user interests present at the time of the ad requests.["fashion", "food"]
$user.retargetingSegmentsSegments used for Retargeting. Group by segment using: $user.retargetingSegments.<segment>.{"b422":1}
$userAgent.textThe actual text of the user agent."Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
$userAgent.browser.nameThe name of the browser."Chrome"
$userAgent.browser.versionAn object containing the segments (major, minor, and patch) of the version number of the browser.{"major":125,"minor":0,"patch":0}
$userAgent.os.nameThe name of the operating system."Android"
$userAgent.os.versionAn object containing the segments (major, minor, and patch) of the version number of the OS.{"major":6,"minor":0,"patch":1}
$userAgent.deviceInformation about the user's mobile device, if any.{"name":"iPhone"}
$placement.<field>The that is used as part of the ad request. For example, to group by a custom property used in ADRs, $placement.properties.loggedStatus"logged-in"
$zoneAlias for $zones[]8675309
$zonesThe set of IDs of the Kevel Zones.[8675309,6318425]

Multi-winner scenarios

In the Kevel Ad Platform you can make a single request and get multiple ads back (multi-winner placements). The actual availability and deliverability of ads depend on what you do with each individual ad selected in a multi-winner ad request. By default, the Forecast assumes all ads in will be used/delivered from each list of multi-winner ads, but you can adjust that default behavior at the network level by contacting your Customer Success manager, and experiment with different settings at each forecast request by using the other options below:

🚧

Caution: forecasts that use a non-default multi-winner setting will take longer to run

Dynamic winner ratio

Setting the DynamicMultiWinnerImpressionRatio to true uses the previously observed winners to delivered impressions ratio for each different ad request. So if the number of delivered impressions is different for each of your placements, sites or other dimensions, this setting is likely what you'd like to use as it will simulate the delivery of a variable number of ads for each future ad request - even if you're always requesting the same number of winners. When used, it takes precedence over the static MultiWinnerImpressionRatio if both are set.

Static winner ratio

If you want to simulate a single winner ratio across all ad requests, use the MultiWinnerImpressionRatio parameter in the Forecast request which represents the percentage (from 0 to 1) of ads you expect to deliver from the list of returned winners. The ratio is applied to the Placement's count and rounded up to the nearest value. The resulting value is clamped between 1 and the placement's count. e.g., for a ratio of 0, the maximum number of opportunities to consider for that placement will be 1. If you always deliver all ads sent back in a multi-winner response, you would use the ratio of 1. If you tend to select for delivery 5 out of 10 you would set a ratio of 0.5.

πŸ“˜

Additional Display Rules

It's common for Additional Display Rules to be used alongside multi-winner placements, these are also taken into account when forecasting multi-winner scenarios (and other multi-request scenarios such as multiple placements on the same page).

Click forecasts

Forecast responses also return the estimated number of Clicks, alongside the Impression and Unique Users. This enables CTR can be easily derived (Clicks / Impressions) for any resulting forecast at any grouping level (e.g. CTR by Site, Geo and Placement):

{
    "requestDateTime": "2023-09-22T14:14:08.540Z",
    "lastUpdateDateTime": "2023-09-22T14:14:15.070Z",
    "startDateTime": "2023-09-22T14:14:09.176Z",
    "endDateTime": "2023-09-22T14:14:15.070Z",
    "resultStatus": "success",
    "result": {
        "total": {
            "clicks": 880,
            "impressions": 135392,
            "uniqueUsers": 43952
        },
        "grouped": [
            {
                "key": {
                    "$site": 12345679
                },
                "value": {
                    "clicks": 0,
                    "impressions": 168,
                    "uniqueUsers": 24
                }
            },
...
        ]
    },
    "id": "45dff65f-e4a0-4f8a-98b9-960a9f5b7240",
    "status": "finished",
    "progress": 100.0,
    "desc": "Forecast is finished"
}

Adjusting click CTR predictions

To enable some fine-tuning over the predicted CTR in the forecast, there's two API options that live inside the optional parameter CampaignCTROverrides:
MinimumCTR: Sets the minimum clickthrough rate percentage for all ads
ZeroImpressionsCTR: The default clickthrough rate for campaigns without historical impressions

E.g. for specifying a minimum of 0.15% CTR and that new campaigns that don’t have enough historical impressions should have a 0.5% CTR:

…  
"Params": {  
…  
    "CampaignCTROverrides": {  
      "MinimumCTR": 0.0015,  
      "ZeroImpressionsCTR": 0.005  
    }

🚧

Lag between impression and click event tracking

A drift/delay in sending Click events may result in missed and inaccurate click predictions, so it is advised to minimize the delay between a click happening and sending it to Kevel.