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

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

🚧

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 fields

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

NameDescriptionExamples
$adTypesSet of Ad Type Ids present in the requests.[1234,5678]
$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
$divNameDiv name (DOM identifier) present in the ad request."div0"
$keywordsThe set of Keywords present in the ad requests.["bunny", "business", "tacocat"]
$location.countryCodeTwo-character country code."US"
$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"
$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
$zonesThe set of IDs of the Kevel Zones.[8675309,6318425]
$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.interestsThe set of user interests present at the time of the ad requests.["fashion", "food"]

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 a single ad will be used from each list of multi-winner ads, but you can adjust that behavior with:

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.