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 specifiedGroupBy
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 $keyword
in 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:
Name | Description | Examples |
---|---|---|
$acceptLanguage | Language set by a user's browser (also known as "locale"). | "en-US,en;q=0.5" |
$adType | Alias for $adTypes[] | 1234 |
$adTypes | Set of Ad Type Ids present in the requests. | [1234,5678] |
$content.custom | ContentDB targeting against a Schema/ContentKey. Group by schema using:$content.custom.<schema> . | Schema: Song Content: {"title": "Somebody To Love"} |
$datacenter | Set to true if the request originates from a known datacenter. | true |
$datetime.date | Calendar day, considering the Timezone set in the TimeZone parameter, in the YYYY-MM-DD format. | "2024-01-22" |
$datetime.dayofweek | Day of week, considering the Timezone set in the TimeZone parameter. Starts on Sunday, range is 1-7. | 2 |
$datetime.hour | Hour of day, considering the Timezone set in the TimeZone parameter. | 14 |
$datetime.week | Week of year, according to ISO 8601, considering the Timezone set in the TimeZone parameter. | 8 |
$datetime.month | Month of year, considering the Timezone set in the TimeZone parameter. | 2 |
$device.brandName | The device brand. | "Nokia" |
$device.modelName | The device model. | "N95" |
$device.marketingName | In addition to Brand and Model, some devices have a marketing name. | "Nokia 8800 Scirocco" |
$device.os | Operating system name. | "iOS" |
$device.osVersion | Operating system version. | {"string":"17.0","major":17,"minor":0} |
$device.osVersion.string | Full representation of the version string. | "17.0" |
$device.osVersion.major | Major version number (group of digits before dot) if available. Defaults to 0 if no number represented. | 17 |
$device.osVersion.minor | Minor version number (group of digits after dot) if available. Defaults to 0 if no number represented. | 0 |
$device.browser | The device browser. | "Safari" |
$device.browserVersion | The device browser's version. | "17.0" |
$device.resolutionWidth | Screen width in pixels. | 640 |
$device.resolutionHeight | Screen height in pixels. | 1136 |
$device.maxImageWidth | Image's maximum viewable width in pixels. | 320 |
$device.maxImageHeight | Image's maximum viewable height in pixels. | 568 |
$device.physicalScreenWidth | Screen width in millimeters. | 50 |
$device.physicalScreenHeight | Screen height in millimeters. | 89 |
$device.formFactor | Either "desktop", "phone", or "tablet". | "phone" |
$device.deviceIdentified | Whether WURFL matches the user agent with a known device | true |
$divName | Div name (DOM identifier) present in the ad request. | "div0" |
$ip | User's IP address. | "127.0.0.1" |
$keyword | Alias for $keywords[] | "bunny" |
$keywords | The set of Keywords present in the ad requests. | ["bunny","business","tacocat"] |
$location.countryCode | Two-character country code. | "US" |
$location.contryName | Country name. | "United States" |
$location.dmaCode | Numeric Nielsen designated market area code (US only). | 560 |
$location.metroCode | Metro Code (US only). | 20500 |
$location.city | City name. | "Durham" |
$location.region | Two or three-character code for the state, region, or province. | "NC" |
$location.subRegion | A two or three-character code for a subregion. Certain countries only. | "CAM" |
$referralUrl | The referrer of the URL that makes an ad request. | "https://kevel.com" |
$regs.gdpr | True if the request is GDPR-regulated. | false |
$consent.gdpr | True if the request has GDPR consent. | true |
$site.id | ID of the Kevel site. | 1234567 |
$url | The URL of the page that makes an ad request. | "https://kevel.com" |
$user.blockedItems.advertiser | Alias for $user.blockedItems.advertisers[] | |
$user.blockedItems.campaign | Alias for $user.blockedItems.campaigns[] | |
$user.blockedItems.creative | Alias for $user.blockedItems.creatives[] | |
$user.blockedItems.flight | Alias for $user.blockedItems.flights[] | |
$user.custom | The 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.interest | Alias for $user.interests[] | "fashion" |
$user.interests | The set of user interests present at the time of the ad requests. | ["fashion", "food"] |
$user.retargetingSegments | Segments used for Retargeting. Group by segment using: $user.retargetingSegments.<segment> . | {"b422":1} |
$userAgent.text | The 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.name | The name of the browser. | "Chrome" |
$userAgent.browser.version | An object containing the segments (major, minor, and patch) of the version number of the browser. | {"major":125,"minor":0,"patch":0} |
$userAgent.os.name | The name of the operating system. | "Android" |
$userAgent.os.version | An object containing the segments (major, minor, and patch) of the version number of the OS. | {"major":6,"minor":0,"patch":1} |
$userAgent.device | Information 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" |
$zone | Alias for $zones[] | 8675309 |
$zones | The 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.