Political Planner
Plan, activate, and convert a Political proposal into an insertion order (IO) and campaigns with this in-depth tutorial using IQM's Planner API.
The following endpoints are used to create a plan, activate a proposal, and generate an IO with campaigns:
POSTPOST
GET
POST
GET
POST
About Political proposals
A Political proposal is a forecast-backed media plan that IQM can turn into a live IO and campaigns. Proposals are built in stages, and each stage produces the ID that the next one needs:
Plan → Strategy → Proposal → Insertion order and campaigns
- A plan holds the budget, flight dates, and targeting. A plan is saved as either a draft or ready plan. Only ready plans produce strategies.
- When a ready plan is created, IQM's forecasting service generates up to three strategies for it: Maximum Reach, Balanced, and Most Economical. Each strategy is a different trade-off between reach, impressions, and cost.
- Activating a strategy turns it into a proposal: an editable set of proposed campaigns with its own status.
- Generating an IO from a ready proposal creates a real IO and campaigns in your Workspace. This is a one-way step.
More resources
- Planner API guidelines
- Audience API guidelines
- Healthcare Professional (HCP) Planner tutorial
- Political Planner overview Help Center article
Before you begin
To create a Political proposal and convert it into an IO, the following are required:
- An account on the IQM platform in a Political Workspace
- See the Getting started section to create an account and request a Client ID and Client Secret.
- Planner API requests from a Workspace outside the Political vertical are rejected.
- A Political Advertiser profile
- See Get list of Advertiser profile and details to find an existing profile ID, or Add Advertiser profile to create one.
- Required in Step 5 to generate an IO. Use a real profile ID; it is validated when the IO is created.
Log in
POSTTo log in, send the Authorization: Basic header with your Base64-encoded Client ID and Client Secret, and send the request body as form data (application/x-www-form-urlencoded). This endpoint returns an OAuth-compliant response with an access token and an Organization Workspace ID (owId), a unique identifier for each Organization. This ID is used for any further API communications.
For further information, see OAuth token.
| Headers | |
|---|---|
Authorization string required | Basic followed by your Base64-encoded client_id:client_secret. See Sign up and authenticate. |
| Request schema | |
|---|---|
grant_type string required | password to log in. See OAuth grant types. |
username string required | User account email. |
password string required | User account password. |
- JSON
curl -i --location 'https://api.iqm.com/api/v3/ua/oauth/token' \
--header 'accept: application/json, text/plain, */*' \
--header 'Authorization: Basic <Client ID/Client Secret>' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username=user@iqm.com' \
--data-urlencode 'password=<your password>'
{
"success": true,
"data":
{
"access_token": "106adb25-37b0-4cab-8381-d682fe7cc3c8",
"refresh_token": "eac4c1f6-781e-4b04-baff-9c2e415d1f64",
"scope": "read write",
"token_type": "bearer",
"expires_in": 35999,
"owId": 200001
}
}
More responses
{
"success": false,
"data":
{
"status": "On Hold",
"reason": "The particular account is kept on hold due to missed payment dates for last 3 months.",
"supportEmail": "support@iqm.com"
},
"errorObjects":
[
{
"error": "User is not allowed to access provided customer",
"reason": "User is not associated with any active organization."
}
]
}
{
"success": false,
"errorObjects":
[
{
"error": "User doesn't exist or user is not allowed to provided workspace."
}
]
}
Authentication
Use the following header parameters for all requests after logging in:
| Headers | |
|---|---|
Authorization string required | Bearer followed by the access_token from the login response. See Sign up and authenticate. |
X-IAA-OW-ID integer required | Organization Workspace ID header. |
Create a Political proposal
This tutorial covers the standard path for Reach forecasting and Budget estimation plans:
- Create a ready plan
- Get the plan's strategies
- Activate a strategy into a proposal
- Check proposal details
- Generate an IO and campaigns
Voter TV+ (cross-screen) plans follow a different path. See Voter TV+ proposals after completing the standard path. To target a Custom Voter audience (CVA), see Target a Custom Voter audience before Step 1.
Step 1: Create a ready plan
POSTPOST
Create a ready (or draft) plan with a budget, flight dates, and targeting. When a ready plan is created, IQM sends its details to the forecasting service, which generates the plan's strategies. Save the planId from the response for Step 2.
See Political Planner resource properties for the full list of fields and their descriptions.
The forecasting service decides which targeting and budget combinations can be planned, and there is no fixed list of valid combinations. A plausible request can still be rejected (response 422). If this happens, nothing is created: widen the targeting or change the budget and try again.
| Request schema | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name string required | Plan name, up to 255 characters. | ||||||||||||||||||||||||
objectiveId integer required | Plan objective ID. Use 1 or 2 for this tutorial. Voter TV+ (3) follows the Voter TV+ path. | ||||||||||||||||||||||||
totalBudget number required | Total plan budget. | ||||||||||||||||||||||||
details object required | Flight dates, frequency cap, and margin. | ||||||||||||||||||||||||
| |||||||||||||||||||||||||
startDate integer required | Unix epoch start date in milliseconds; can't be in the past. |
endDate integer required | Unix epoch end date in milliseconds; must be at least 30 minutes after startDate. |
frequencyCap integer | Frequency cap, 1 to 999. |
marginValue integer | Margin applied on top of the base CPM, as a percentage from 0 to 100 (not a currency amount). Defaults to the Customer's margin. |
targeting object required
targeting object properties
whiteListedLocation object | Locations to target. | |||||
| ||||||
country integer | Country ID to target. |
device object
device object properties
deviceTypeIds array of integers | Device type IDs to target. |
creative object
creative object properties
creativeTypeIds array of integers | Creative type IDs to target. |
isTvAutoBuying boolean | Whether TV auto buying is enabled. |
cva object
cva object properties
id integer required | CVA ID from Create CVA details. |
equation string required | Segment equation sent when the CVA was created. |
stateIds array of integers required | State IDs sent when the CVA was created. |
segmentIds array of integers required | Numeric segment IDs of the segments in the equation, from Find segment IDs. |
reach integer required | Audience reach, from Get the audience reach. |
Response properties
planId integer | ID of the created plan. |
- JSON
- TypeScript
{
"name": "Ohio GOTV Reach Plan",
"objectiveId": 1,
"totalBudget": 50000,
"details": {
"startDate": 1793595600000,
"endDate": 1796187600000,
"frequencyCap": 3
},
"targeting": {
"whiteListedLocation": {
"country": 30100001
},
"device": {
"deviceTypeIds": [20300001, 20300004]
},
"creative": {
"creativeTypeIds": [11, 14],
"isTvAutoBuying": false
}
}
}
{
"success": true,
"data": {
"planId": 123
}
}
{
"success": false,
"errorObjects": [
{
"error": "No strategies meet your criteria. Edit the plan to explore more options."
}
]
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
planId: number;
};
};
};
};
422: {
content: {
"application/json": {
success: boolean;
errorObjects: {
error: string;
}[];
};
};
};
}
function createReadyPlan(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/plans/ready',
requestBody: {
content: {
"application/json": {
name: `string`,
objectiveId: `number`,
totalBudget: `number`,
details: {
startDate: `number`,
endDate: `number`,
frequencyCap?: `number`,
marginValue?: `number`,
},
targeting: {
whiteListedLocation?: {
country?: `number`,
},
device?: {
deviceTypeIds?: `array of numbers`,
},
creative?: {
creativeTypeIds?: `array of numbers`,
isTvAutoBuying?: `boolean`,
},
},
cva?: {
id: `number`,
},
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 2: Get the plan's strategies
GETUse the planId from Step 1 to list the strategies generated for the plan. Compare the forecasted reach, impressions, budget, and cpm of each strategy and save the id of the one you want to activate in Step 3.
A strategy's budget, frequency, creative types, and campaigns are the forecasting service's recommendation, not a copy of the plan. They can differ from what you sent in Step 1: for example, a lower budget, a lower frequency cap, or only some of the plan's creative types. The forecasting service can also return fewer than three strategies.
If you create extra strategies with Generate custom strategies, the id in that endpoint's response is the forecasting service's own ID, not an IQM strategy ID, and can't be activated. Always get strategy IDs from this endpoint.
| Query parameters | |
|---|---|
planId integer required | Plan ID from Step 1. |
Response properties
id integer | Strategy ID; use this to activate the strategy. |
name string | Strategy name. |
strategyTypeId integer | Strategy type ID: 1 Maximum Reach, 3 Balanced, 4 Most Economical, 5 Custom. |
proposalId integer | ID of the proposal created from this strategy, or null if it hasn't been activated. |
startDate integer | Unix epoch start date in milliseconds. |
endDate integer | Unix epoch end date in milliseconds. |
duration integer | Flight duration in days. |
campaignsCount integer | Number of proposed campaigns in the strategy. |
budget number | Forecasted budget. |
reach integer | Forecasted reach. |
impressions integer | Forecasted impressions. |
cpm number | Forecasted cost per mille (CPM). |
frequency integer | Frequency cap. |
creatives array of integers | Creative type IDs. |
devices array of integers | Device type IDs. |
channels array of integers | Channel type IDs. |
- JSON
- TypeScript
{
"success": true,
"data": [
{
"id": 3001,
"name": "Maximum Reach",
"startDate": 1793595600000,
"endDate": 1796187600000,
"duration": 30,
"strategyTypeId": 1,
"campaignsCount": 4,
"proposalId": null,
"budget": 50000,
"frequency": 3,
"reach": 1250000,
"impressions": 2220000,
"cpm": 22.52,
"creatives": [11, 14],
"devices": [20300001, 20300004],
"channels": [11, 12, 13, 14],
"isTvAutoBuying": false
},
{
"id": 3002,
"name": "Most Economical",
"startDate": 1793595600000,
"endDate": 1796187600000,
"duration": 30,
"strategyTypeId": 4,
"campaignsCount": 2,
"proposalId": null,
"budget": 38500,
"frequency": 3,
"reach": 980000,
"impressions": 2100000,
"cpm": 18.33,
"creatives": [11, 14],
"devices": [20300001, 20300004],
"channels": [11, 12],
"isTvAutoBuying": false
}
]
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
id: number;
name: string;
startDate: number;
endDate: number;
duration: number;
strategyTypeId: number;
campaignsCount: number;
proposalId: number | null;
budget: number;
frequency: number;
reach: number;
impressions: number;
cpm: number;
creatives: number[];
devices: number[];
channels: number[];
isTvAutoBuying: boolean;
}[];
};
};
};
}
function getPoliticalStrategyListing(): Promise<Responses> {
const options = {
method: 'GET',
url: 'https://api.iqm.com/api/v3/plan/political/strategies',
params: {
query: {
planId: `number`,
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 3: Activate a strategy into a proposal
POSTPass the strategy id from Step 2 as strategyId to activate it. Activation creates a proposal with its proposed campaigns, in Ready (6) status. Save the proposal id from the response for the next steps.
Voter TV+ plans have no strategies, so they are activated with planId instead of strategyId. See Voter TV+ proposals.
| Request schema | |
|---|---|
strategyId integer required | Strategy ID from Step 2. |
Response properties
id integer | Proposal ID. |
message string | Success message. |
- JSON
- TypeScript
{
"strategyId": 3001
}
{
"success": true,
"data": {
"id": 7,
"message": "Ohio GOTV Reach Plan saved successfully."
}
}
More responses
{
"success": false,
"errorObjects": [
{
"error": "Strategy with strategy id 42 does not exist",
"field": "strategyId"
}
]
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
id: number;
message: string;
};
};
};
};
422: {
content: {
"application/json": {
success: boolean;
errorObjects: {
error: string;
field: string;
}[];
};
};
};
}
function activateProposal(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/proposals',
requestBody: {
content: {
"application/json": {
strategyId?: `number`,
planId?: `number`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 4: Check proposal details
GETUse the proposal id from Step 3 to review the proposal and its proposed campaigns before generating an IO. The statusId must be Ready (6) or Failed (4) to continue to Step 5.
To change the proposal's budget, dates, or campaigns first, see Edit proposal.
| Path parameters | |
|---|---|
proposalId integer required | Proposal ID from Step 3. |
Response properties
id integer | Proposal ID. | |||||||||||||||||||||||||||||||||||||||||||||
name string | Proposal name. | |||||||||||||||||||||||||||||||||||||||||||||
planId integer | ID of the plan the proposal was created from. | |||||||||||||||||||||||||||||||||||||||||||||
planName string | Name of the plan the proposal was created from. | |||||||||||||||||||||||||||||||||||||||||||||
objectiveId integer | Plan objective ID. | |||||||||||||||||||||||||||||||||||||||||||||
statusId integer | Proposal status ID. | |||||||||||||||||||||||||||||||||||||||||||||
ioId integer | IO ID generated from the proposal, once it's converted. | |||||||||||||||||||||||||||||||||||||||||||||
owId integer | Organization Workspace ID. | |||||||||||||||||||||||||||||||||||||||||||||
startTime integer | Unix epoch start time in milliseconds. | |||||||||||||||||||||||||||||||||||||||||||||
endTime integer | Unix epoch end time in milliseconds. | |||||||||||||||||||||||||||||||||||||||||||||
timeZoneId integer | Timezone ID. | |||||||||||||||||||||||||||||||||||||||||||||
timeZoneName string | Timezone name. | |||||||||||||||||||||||||||||||||||||||||||||
frequencyCap integer | Frequency cap. | |||||||||||||||||||||||||||||||||||||||||||||
minReach integer | Minimum forecasted reach. | |||||||||||||||||||||||||||||||||||||||||||||
reach integer | Forecasted reach. | |||||||||||||||||||||||||||||||||||||||||||||
maxReach integer | Maximum forecasted reach. | |||||||||||||||||||||||||||||||||||||||||||||
minImpressions integer | Minimum forecasted impressions. | |||||||||||||||||||||||||||||||||||||||||||||
impressions integer | Forecasted impressions. | |||||||||||||||||||||||||||||||||||||||||||||
maxImpressions integer | Maximum forecasted impressions. | |||||||||||||||||||||||||||||||||||||||||||||
minBudget number | Minimum budget. | |||||||||||||||||||||||||||||||||||||||||||||
budget number | Proposal budget. | |||||||||||||||||||||||||||||||||||||||||||||
maxBudget number | Maximum budget. | |||||||||||||||||||||||||||||||||||||||||||||
minCpm number | Minimum cost per mille (CPM); can be null at the proposal level. | |||||||||||||||||||||||||||||||||||||||||||||
cpm number | CPM; can be null at the proposal level. Each proposed campaign has its own CPM. | |||||||||||||||||||||||||||||||||||||||||||||
maxCpm number | Maximum CPM; can be null at the proposal level. | |||||||||||||||||||||||||||||||||||||||||||||
campaignGeneratable boolean | Whether an IO and campaigns can be generated from the proposal. Only meaningful while the proposal is Ready or Failed; it isn't reset after the proposal is converted. | |||||||||||||||||||||||||||||||||||||||||||||
proposedCampaigns array of objects | Campaigns that are created when the IO is generated. | |||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||
id integer | Proposed campaign ID. |
name string | Campaign name. |
type string | Voter TV+ proposals only: CTV or Linear TV. Only CTV campaigns are created when the IO is generated. null for other proposals. |
strategy string | Targeting dimensions the campaign was split by. |
creativeTypeId integer | Creative type ID. |
startTime integer | Unix epoch start time in milliseconds. |
endTime integer | Unix epoch end time in milliseconds. |
duration integer | Flight duration in days. |
modelType string | Pricing model; always CPM. |
frequencyCap integer | Frequency cap. |
minTargetedReach integer | Minimum targeted reach. |
targetedReach integer | Targeted reach. |
maxTargetedReach integer | Maximum targeted reach. |
minTargetedImpressions integer | Minimum targeted impressions. |
targetedImpressions integer | Targeted impressions. |
maxTargetedImpressions integer | Maximum targeted impressions. |
minCpm number | Minimum CPM. |
cpm number | CPM. |
maxCpm number | Maximum CPM. |
minBudget number | Minimum budget. |
budget number | Campaign budget. |
maxBudget number | Maximum budget. |
- JSON
- TypeScript
{
"success": true,
"data": {
"id": 7,
"name": "Ohio GOTV Reach Plan",
"planId": 123,
"planName": "Ohio GOTV Reach Plan",
"objectiveId": 1,
"startTime": 1793595600000,
"endTime": 1796187600000,
"owId": 202500,
"statusId": 6,
"ioId": null,
"reach": 1250000,
"impressions": 2220000,
"budget": 50000,
"frequencyCap": 3,
"timeZoneId": 29,
"timeZoneName": "US/Eastern",
"campaignGeneratable": true,
"proposedCampaigns": [
{
"id": 101,
"name": "Ohio GOTV Reach Plan - CTV",
"startTime": 1793595600000,
"endTime": 1796187600000,
"creativeTypeId": 14,
"duration": 30,
"modelType": "CPM",
"targetedReach": 500000,
"frequencyCap": 3,
"targetedImpressions": 1500000,
"cpm": 25.5,
"budget": 38250,
"strategy": "Device + Channel"
}
]
}
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
id: number;
name: string;
planId: number;
planName: string;
objectiveId: number;
startTime: number;
endTime: number;
owId: number;
statusId: number;
ioId: number | null;
reach: number;
impressions: number;
budget: number;
frequencyCap: number;
timeZoneId: number;
timeZoneName: string;
campaignGeneratable: boolean;
proposedCampaigns: {
id: number;
name: string;
startTime: number;
endTime: number;
creativeTypeId: number;
duration: number;
modelType: string;
targetedReach: number;
frequencyCap: number;
targetedImpressions: number;
cpm: number;
budget: number;
strategy: string;
}[];
};
};
};
};
}
function getProposalDetailsById(): Promise<Responses> {
const options = {
method: 'GET',
url: 'https://api.iqm.com/api/v3/plan/political/proposal/{proposalId}',
params: {
path: {
proposalId: `number`,
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 5: Generate an IO and campaigns
POSTConvert the proposal into an IO and campaigns. The proposal must be in Ready (6) or Failed (4) status.
The request returns immediately and the proposal moves to Processing (5) while the IO and campaigns are created in the background. When generation finishes, the proposal moves to Converted (1) and its ioId is set. If generation fails, the proposal moves to Failed (4) and you can retry this step. Use Step 4 to check the result.
The converted proposal doesn't link its proposed campaigns to the campaigns that were created. To find the new campaigns, call Get basic details of campaigns (POST /api/v3/cmp/basic/list) with the proposal's ioId in ioIdsList, for example {"ioIdsList": [29046]}. The campaigns are created as drafts.
Manage the generated IO and campaigns with the Campaign API.
Generating an IO is a one-way step. A converted proposal can't be returned to Ready, and calls that require a Ready, Failed, or Expired proposal, such as editing, downloading, or restoring, no longer work for it.
| Path parameters | |
|---|---|
proposalId integer required | Proposal ID from Step 3. |
| Request schema | |
|---|---|
politicalAdvertiserId integer required | Political Advertiser profile ID. |
startTime integer required | Unix epoch start time for the campaigns, in milliseconds; can't be in the past. Always required. |
endTime integer | Unix epoch end time in milliseconds. If omitted, the proposal's current duration is kept from the new startTime. If provided, it must keep the same duration in days. |
- JSON
- TypeScript
{
"politicalAdvertiserId": 789,
"startTime": 1793595600000
}
{
"success": true,
"data": {
"message": "Campaign generation is in progress"
}
}
More responses
{
"success": false,
"errorObjects": [
{
"error": "proposal start and end date changes must preserve the existing duration"
}
]
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
message: string;
};
};
};
};
422: {
content: {
"application/json": {
success: boolean;
errorObjects: {
error: string;
}[];
};
};
};
}
function generateIOCampaigns(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/proposals/{proposalId}/io',
params: {
path: {
proposalId: `number`,
}
},
requestBody: {
content: {
"application/json": {
politicalAdvertiserId: `number`,
startTime: `number`,
endTime?: `number`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Voter TV+ proposals
Voter TV+ (objective 3) plans forecast reach across linear TV and connected TV (CTV). They follow a different path from the standard steps above:
- Get a reach saturation graph
- Create a ready plan with the graph
- Activate the plan into a proposal
- Split the proposal's CTV campaigns (optional)
- Generate an IO and campaigns, as in Step 5 of the standard path
When the IO is generated, only the CTV proposed campaigns become campaigns; linear TV campaigns aren't created.
Eligibility requirements
Voter TV+ plans:
- Must have a flight of 7 to 31 days
- Must have a budget over 100
- Can't target Power Segment audiences
Step 1: Get a reach saturation graph
POSTSend the plan's budget, dates, and targeting with objectiveId 3 to get a reach forecast for linear TV and CTV. Save the graphId and overview from the response for the next step.
Check audienceOverlap.totalReach in the response before continuing. If it's 0, the targeting doesn't match any voters: a plan created from this graph produces a proposal with 0 budgets, and splitting it fails. Narrow the location targeting, for example to states or DMAs, and request a new graph. The sample targets Ohio (30200036) and Pennsylvania (30200039).
To adjust the forecast, call this endpoint again with the same graphId and new overview values.
See Get reach saturation graph for all request and response fields.
The first request for a new graph calls an external data service and can take more than 60 seconds. Set your client timeout to at least 60 seconds so the request isn't mistaken for a failure. Later requests with the same graphId are calculated without the external call.
| Request schema | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name string required | Plan name. | ||||||||||||||||||||||
objectiveId integer required | Plan objective ID: 3 (Voter TV+). | ||||||||||||||||||||||
totalBudget number required | Total plan budget; must be at least 100. | ||||||||||||||||||||||
details object required | Flight dates and frequency cap. | ||||||||||||||||||||||
| |||||||||||||||||||||||
startDate integer required | Unix epoch start date in milliseconds; can't be in the past. |
endDate integer required | Unix epoch end date in milliseconds. The flight must be 7 to 31 days. |
frequencyCap integer | Frequency cap, 1 to 999 (default: 3). |
targeting object
targeting object properties
whiteListedLocation object | Locations to target. | |||||
| ||||||
country integer | Country ID to target. |
states array of integers | State IDs to target. |
ageGroupSegments array of integers
genderSegments array of integers
incomeRangeSegments array of integers
ethnicitySegments array of integers
partyAffiliationSegments array of integers
whitelistedAudiences array of integers
graphId string
overview object
overview object properties
linearTvCpm number | Linear TV cost per mille (CPM); 0 or more. |
ctvCpm number | CTV CPM; 0 or more. |
grp number | Linear TV gross rating points (GRP); 0 or more. |
linearTvPercentage integer | Percentage of the budget allocated to linear TV, 0 to 100. The rest goes to CTV. |
ctvFrequency integer | Average CTV frequency, 1 to 99. |
- JSON
- TypeScript
{
"name": "Voter TV+ Plan",
"objectiveId": 3,
"totalBudget": 100000,
"details": {
"startDate": 1793595600000,
"endDate": 1794805200000,
"frequencyCap": 3
},
"targeting": {
"whiteListedLocation": {
"country": 30100001,
"states": [30200036, 30200039]
},
"ageGroupSegments": [6073147, 6073145],
"genderSegments": [6189394, 6189395]
}
}
{
"success": true,
"data": {
"graphId": "0f6c7400-a8f5-4a1b-ab65-af15372cc0ff",
"overview": {
"linearTvCpm": 20,
"ctvCpm": 7,
"grp": 180,
"linearTvPercentage": 50,
"ctvFrequency": 3
},
"audienceOverlap": {
"population": 16100000,
"totalReach": 1470000,
"linearTvReach": 1020000,
"ctvReach": 778000,
"overlapReach": 328000,
"ctvIncrementalReach": 450000
},
"maximumAudienceOverlap": {
"population": 16100000,
"totalReach": 2150000,
"linearTvReach": 1480000,
"ctvReach": 1150000,
"overlapReach": 480000,
"ctvIncrementalReach": 670000
},
"reachSaturation": [
{
"day": 1,
"ctvReach": 12,
"linearTvReach": 14,
"overallReach": 20
}
]
}
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface AudienceOverlap {
population: number;
totalReach: number;
linearTvReach: number;
ctvReach: number;
overlapReach: number;
ctvIncrementalReach: number;
}
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
graphId: string;
overview: {
linearTvCpm: number;
ctvCpm: number;
grp: number;
linearTvPercentage: number;
ctvFrequency: number;
};
audienceOverlap: AudienceOverlap;
maximumAudienceOverlap: AudienceOverlap;
reachSaturation: {
day: number;
ctvReach: number;
linearTvReach: number;
overallReach: number;
}[];
};
};
};
};
}
function getCrossScreenGraph(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/reach-saturation-graph',
timeout: 60000,
requestBody: {
content: {
"application/json": {
name: `string`,
objectiveId: `number`,
totalBudget: `number`,
details: {
startDate: `number`,
endDate: `number`,
frequencyCap?: `number`,
},
targeting?: `object`,
graphId?: `string`,
overview?: `object`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 2: Create a Voter TV+ plan
POSTCreate a ready plan with objectiveId 3, and copy the graphId and overview from your Step 1 response. Use the same budget, dates, and targeting you sent in Step 1 so the plan matches the graph. Save the planId from the response.
Voter TV+ plans always target video creatives on CTV devices. Any targeting.creative or targeting.device values in the request are replaced.
Voter TV+ plans don't generate strategies, so Get the plan's strategies returns an empty list. Activate the plan directly in the next step.
| Request schema | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name string required | Plan name, up to 255 characters. | ||||||||||||||||||||||
objectiveId integer required | Plan objective ID: 3 (Voter TV+). | ||||||||||||||||||||||
totalBudget number required | Total plan budget; must be more than 100. | ||||||||||||||||||||||
graphId string required | graphId from Step 1. | ||||||||||||||||||||||
overview object | overview from Step 1. Defaults are used if omitted. | ||||||||||||||||||||||
| |||||||||||||||||||||||
linearTvCpm number | Linear TV cost per mille (CPM); 0 or more. |
ctvCpm number | CTV CPM; 0 or more. |
grp number | Linear TV gross rating points (GRP); 0 or more. |
linearTvPercentage integer | Percentage of the budget allocated to linear TV, 0 to 100. The rest goes to CTV. |
ctvFrequency integer | Average CTV frequency, 1 to 99. |
details object required
details object properties
startDate integer required | Unix epoch start date in milliseconds; can't be in the past. |
endDate integer required | Unix epoch end date in milliseconds. The flight must be 7 to 31 days. |
frequencyCap integer | Frequency cap, 1 to 999 (default: 3). |
marginValue integer | Margin applied on top of the base CPM, as a percentage from 0 to 100 (not a currency amount). Defaults to the Customer's margin. |
targeting object required
targeting object properties
whiteListedLocation object | Locations to target. | |||||
| ||||||
country integer | Country ID to target. |
states array of integers | State IDs to target. |
ageGroupSegments array of integers
genderSegments array of integers
incomeRangeSegments array of integers
ethnicitySegments array of integers
partyAffiliationSegments array of integers
whitelistedAudiences array of integers
- JSON
- TypeScript
{
"name": "Voter TV+ Plan",
"objectiveId": 3,
"totalBudget": 100000,
"graphId": "0f6c7400-a8f5-4a1b-ab65-af15372cc0ff",
"overview": {
"linearTvCpm": 20,
"ctvCpm": 7,
"grp": 180,
"linearTvPercentage": 50,
"ctvFrequency": 3
},
"details": {
"startDate": 1793595600000,
"endDate": 1794805200000,
"frequencyCap": 3
},
"targeting": {
"whiteListedLocation": {
"country": 30100001,
"states": [30200036, 30200039]
},
"ageGroupSegments": [6073147, 6073145],
"genderSegments": [6189394, 6189395]
}
}
{
"success": true,
"data": {
"planId": 124
}
}
More responses
{
"success": false,
"errorObjects": [
{
"error": "Duration in days must be between 7 and 31"
}
]
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
planId: number;
};
};
};
};
}
function createVoterTvPlan(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/plans/ready',
requestBody: {
content: {
"application/json": {
name: `string`,
objectiveId: `number`,
totalBudget: `number`,
graphId: `string`,
overview?: {
linearTvCpm: `number`,
ctvCpm: `number`,
grp: `number`,
linearTvPercentage: `number`,
ctvFrequency: `number`,
},
details: {
startDate: `number`,
endDate: `number`,
frequencyCap?: `number`,
},
targeting: `object`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 3: Activate a Voter TV+ plan
POSTPass the planId from the previous step to create a proposal from the plan's graph. The budget is split between linear TV and CTV campaigns using the linearTvPercentage from the overview. The proposal is created in Ready (6) status.
| Request schema | |
|---|---|
planId integer required | Voter TV+ plan ID. Don't send strategyId in the same request. |
- JSON
- TypeScript
{
"planId": 124
}
{
"success": true,
"data": {
"id": 8,
"message": "Voter TV+ Plan_Mix_01 saved successfully."
}
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
id: number;
message: string;
};
};
};
};
}
function activateVoterTvProposal(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/proposals',
requestBody: {
content: {
"application/json": {
planId: `number`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Step 4: Split a Voter TV+ proposal
POSTSplit a Voter TV+ proposal's CTV campaigns into one campaign per value of a targeting dimension, such as one campaign per state. The existing CTV campaigns are replaced. Send an empty locations list to merge them back into a single CTV campaign. This step is optional.
A split only has an effect for dimensions that the plan targets with more than one value. The sample splits by state (8), because the sample plan targets two states; splitting it by ZIP code or device gives a single CTV campaign. Split campaigns are named with a numbered suffix, such as _Video + Loc_1 and _Loc_2, not with the state name.
The response shows the proposal as it was before the split. To see the new campaigns, get the proposal again with Check proposal details.
This endpoint only works on Voter TV+ proposals. On a proposal with 0 budgets, such as one created from a graph with no reach, it returns a generic 500 error.
Splitting can take more than 60 seconds for large proposals. Set your client timeout to at least 60 seconds.
| Path parameters | |
|---|---|
proposalId integer required | Voter TV+ proposal ID. |
| Request schema | |
|---|---|
locations array of integers | Targeting type IDs to split by. Despite the field name, these are targeting dimension IDs, not location IDs. For example, 8 (state), 12 (DMA), 15 (ZIP code), 29 (device). |
- JSON
- TypeScript
{
"locations": [8]
}
{
"success": true,
"data": {
"id": 8,
"name": "Voter TV+ Plan_Mix_01",
"startTime": 1793595600000,
"endTime": 1794805200000,
"owId": 202500,
"statusId": 6,
"reach": 778000,
"impressions": 7140000,
"budget": 100000,
"frequencyCap": 3,
"campaignGeneratable": true,
"proposedCampaigns": [
{
"id": 501,
"name": "Voter TV+ Plan_Mix_01_VTV + Video",
"type": "Linear TV",
"budget": 50000
},
{
"id": 502,
"name": "Voter TV+ Plan_Mix_01_Video",
"type": "CTV",
"budget": 50000
}
]
}
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
id: number;
name: string;
startTime: number;
endTime: number;
owId: number;
statusId: number;
reach: number;
impressions: number;
budget: number;
frequencyCap: number;
campaignGeneratable: boolean;
proposedCampaigns: object[];
};
};
};
};
}
function splitProposal(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/proposals/{proposalId}/split',
timeout: 60000,
params: {
path: {
proposalId: `number`,
}
},
requestBody: {
content: {
"application/json": {
locations?: `array of numbers`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Target a Custom Voter audience
A Custom Voter audience (CVA) combines voter segments into a single audience for a plan. To target a CVA:
- Find the segment IDs
- Get the audience reach
- Create CVA details
- Add the CVA to a plan
When you add the CVA to a plan, send the full cva object: id, equation, stateIds, segmentIds, and reach. A plan request with only cva.id returns a generic 500 error, even though the other fields are stored with the CVA ID.
Step 1: Find segment IDs
POSTLook up the segments you want to combine. Each leaf segment has a taxonomyId, such as IQMCVACA069112, which is the token used in the CVA equation, and a numeric segmentId, which goes in the plan's cva.segmentIds. Save both.
To browse the segment catalog instead of looking up known taxonomy IDs, see Get Custom Voter audience segment details.
| Request schema | |
|---|---|
taxonomyIds array of strings | Taxonomy IDs of the segments to look up. |
validateTaxonomyIds boolean | Set to true to return an error if any taxonomy ID doesn't exist (default: false). |
Response properties
cvaSegmentData array of objects | Matching segments. | |||||||||
| ||||||||||
segmentId integer | Segment ID; use it in cva.segmentIds. |
taxonomyId string | Taxonomy ID; use it in the CVA equation. |
taxonomyValue string | Segment name. |
isLeaf boolean | Whether the segment is a leaf segment. Only leaf segments can be used in an equation. |
{
"taxonomyIds": ["IQMCVACA069112", "IQMCVACA080323", "IQMCVACA101616"],
"validateTaxonomyIds": true
}
{
"success": true,
"data": {
"totalRecords": 3,
"filteredRecords": 3,
"cvaSegmentData": [
{ "segmentId": 4211, "taxonomyId": "IQMCVACA069112", "isLeaf": true },
{ "segmentId": 4215, "taxonomyId": "IQMCVACA080323", "isLeaf": true },
{ "segmentId": 4230, "taxonomyId": "IQMCVACA101616", "isLeaf": true }
]
}
}
Step 2: Get the audience reach
POSTGet the number of voters that match the equation. Use segment_num_voters from the response as the CVA's reach in the next step; IQM stores the reach you send and passes it to the forecasting service without recalculating it.
This endpoint uses snake_case state_ids, unlike the camelCase stateIds in the next step. If state_ids is empty, the response counts no voters, so send the states your segments belong to.
| Request schema | |
|---|---|
equation string required | CVA equation; see Step 3 for the syntax. |
state_ids array of integers required | State IDs of the segments. |
Response properties
segment_num_voters integer | Number of voters that match the equation; use it as the CVA's reach. |
total_num_voters integer | Total number of voters in the selected states. |
map_metrics object | Voter counts by party, keyed by location ID. Can include keys that aren't state IDs. |
{
"equation": "((IQMCVACA069112|(IQMCVACA080323))&(IQMCVACA101616))",
"state_ids": [30200005]
}
{
"success": true,
"data": {
"segment_num_voters": 1629051,
"total_num_voters": 23165810,
"map_metrics": {
"30200005": {
"Republican": 412870,
"Democrat": 761304,
"Other": 454877
}
}
}
}
Step 3: Create CVA details
POSTDefine the CVA with an equation of the taxonomy IDs from Step 1, and the reach from Step 2.
Equation syntax:
- Wrap each segment ID in parentheses
- Separate segments with | to combine them (OR)
- Separate groups with & to intersect them (AND)
For example, ((IQMCVACA069112|(IQMCVACA080323))&(IQMCVACA101616)) targets voters in either of the first two segments who are also in the third.
An equation with an unknown taxonomy ID or unbalanced parentheses returns a 500 error with the message Invalid CVA details provided. This is a validation error, not an outage: fix the equation and try again.
| Request schema | |
|---|---|
equation string required | Segment equation. |
stateIds array of integers | State IDs to limit the audience to. |
reach integer | Audience reach: segment_num_voters from Step 2. This value is stored and passed to the forecasting service as provided; it isn't calculated. |
Response properties
id integer | CVA ID; pass it as cva.id when creating a plan. |
- JSON
- TypeScript
{
"equation": "((IQMCVACA069112|(IQMCVACA080323))&(IQMCVACA101616))",
"stateIds": [30200005],
"reach": 1629051
}
{
"success": true,
"data": {
"id": 12345
}
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
id: number;
};
};
};
};
}
function createCvaDetails(): Promise<Responses> {
const options = {
method: 'POST',
url: 'https://api.iqm.com/api/v3/plan/political/cva-details',
requestBody: {
content: {
"application/json": {
equation: `string`,
stateIds?: `array of numbers`,
reach?: `number`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}
Add the CVA to a plan
Pass the full CVA in the cva field when you create a ready plan: the id from Step 3, the equation and stateIds from Step 3, the segmentIds from Step 1, and the reach from Step 2. To keep the forecast consistent, target the CVA's states in the plan's whiteListedLocation as well.
"cva": {
"id": 12345,
"equation": "((IQMCVACA069112|(IQMCVACA080323))&(IQMCVACA101616))",
"stateIds": [30200005],
"segmentIds": [4211, 4215, 4230],
"reach": 1629051
}
Restore an expired proposal
PATCHA proposal that isn't converted becomes Expired (3) after its end date passes. To use an expired proposal again, restore it with new flight dates. Restored proposals return to Ready (6) status and can then be converted as in Step 5.
The new flight can be a different length from the original; unlike generating an IO, restoring doesn't require the duration to stay the same. Restoring only works on expired proposals. It isn't an undo for Delete proposals.
| Request schema | |
|---|---|
proposalIds array of integers required | IDs of expired proposals to restore. |
startTime integer required | New Unix epoch start time in milliseconds; can't be in the past. |
endTime integer required | New Unix epoch end time in milliseconds; must be at least 30 minutes after startTime. |
- JSON
- TypeScript
{
"proposalIds": [7],
"startTime": 1793595600000,
"endTime": 1796187600000
}
{
"success": true,
"data": {
"message": "Proposals restored successfully"
}
}
More responses
{
"success": false,
"errorObjects": [
{
"error": "One or more proposals are not in expired status",
"field": "proposalIds"
}
]
}
See TypeScript prerequisites page for usage.
import {
getInstance
} from "prerequisites"
const axios = getInstance();
interface Responses {
200: {
content: {
"application/json": {
success: boolean;
data: {
message: string;
};
};
};
};
422: {
content: {
"application/json": {
success: boolean;
errorObjects: {
error: string;
field: string;
}[];
};
};
};
}
function restoreProposals(): Promise<Responses> {
const options = {
method: 'PATCH',
url: 'https://api.iqm.com/api/v3/plan/political/proposals/restore',
requestBody: {
content: {
"application/json": {
proposalIds: `array of numbers`,
startTime: `number`,
endTime: `number`,
}
}
}
};
return axios.request(options).then(({ data }: { data: Responses }) => data);
}