Payout Bid Modification Tables API

View as Markdown

A Payout Bid Modification (PBM) table is a CSV of rules, uploaded to a Campaign, that adjusts the payout percentage of an RTB ping based on its attributes — caller state, revenue, a deterministic bucket assignment, or any other tag key. Use one to run a Control/Treatment payout experiment, or to apply a rate card from a rules provider.

Note

This is a detailed, rules-engine-shaped API. If you are setting up Payout Bid Modification for the first time, start with the Payout Bid Modification guide before working through this reference.

A table is write-once: csv_data, salt and table_version can only be set on create. The only thing you can change afterwards is active — flipping it is how you roll a campaign forward to a new table, or back to a previous one. A campaign keeps every table uploaded to it (its upload history), but only one may be active at a time; activating one deactivates whichever table was active before.

The rule CSV format

rule_id,caller_state,pbm_bucket,pbm_bucket,payout_pct
1,=~^(CO|NY|TX)$,*,*,70
2,*,>=5000,<7500,60
3,*,*,*,current
Swipe horizontally for full code

Each row is one rule, evaluated top to bottom; the first matching row wins. Two columns are reserved:

Swipe horizontally to view full table
Column Meaning
rule_id Carried through unchanged, for logging and reporting.
payout_pct The percentage of revenue to pay out when the rule matches (e.g. 70), or the literal current to leave the payout unmodified.

Every other column header is a real Retreaver tag key, used as-is — caller_state, sub_id, revenue, system_affiliate_id, or pbm_bucket (the ping’s deterministic bucket assignment, 0–9999, used for percentage-based traffic splits). Each cell uses the same key:operator:value shorthand Retreaver uses for tag conditions elsewhere, minus the key: part, since the column header names the key:

  • A plain value means equality, e.g. CO.
  • Prefix an operator for anything else, e.g. !=CA, >=5000, or =~^(CO|NY|TX)$ to match a set of values in one row.
  • *, or leaving the column out of a row, means no constraint.

The same key can appear as more than one column to AND comparators together. Two pbm_bucket columns, >=5000 and <7500, express a bucket range — there is no dedicated bucket_from/bucket_to.

Limits: at most 1,000 rules per table, and 1 MB of CSV per upload.

Warning

Affiliates, Campaigns and Targets each have two keys, matching the Our IDs vs Customer IDs distinction elsewhere in this API. Picking the wrong one does not error — it silently matches against the wrong ID.

Swipe horizontally to view full table
Key Matches on
system_affiliate_id, system_campaign_id, system_target_id Retreaver’s own internal ID (the id in every JSON response in this API).
affiliate_id, campaign_id, target_id The client-supplied external ID (afid / cid / tid) set by whoever integrated that object — not Retreaver’s ID.

If a rules provider hands you rule CSVs keyed on Retreaver’s own IDs — the normal case, and what every other endpoint in this API calls affiliate_id / campaign_id / target_id — use the system_* columns.

Payout Bid Modification table object

{
  "payout_bid_modification_table": {
    "id": 9001,
    "campaign_id": 16728,
    "table_version": 9001,
    "active": true,
    "salt": "acme_inc_2026-09-15",
    "csv_data": "rule_id,caller_state,pbm_bucket,pbm_bucket,payout_pct\n1,=~^(CO|NY|TX)$,*,*,70\n2,*,>=5000,<7500,60\n3,*,*,*,current\n",
    "created_at": "2026-09-15T18:04:22Z"
  }
}
Swipe horizontally for full code

Responses are wrapped in a payout_bid_modification_table key — id, active and the rest are nested one level in, not top-level. Only a validation-error response is flat.

Create a Payout Bid Modification table

curl -s -X POST "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables.json?api_key=[api_key]" \
  -H "Content-Type: application/json" \
  -d '{
        "payout_bid_modification_table": {
          "csv_data": "rule_id,caller_state,pbm_bucket,pbm_bucket,payout_pct\n1,=~^(CO|NY|TX)$,*,*,70\n2,*,>=5000,<7500,60\n3,*,*,*,current\n",
          "active": true
        }
      }'
Swipe horizontally for full code

The above command returns the new table — see Payout Bid Modification table object.

HTTP Request

curl -X POST "https://api.retreaver.com/api/v5/campaigns/:campaign_id/payout_bid_modification_tables.json?api_key=[api_key]" \
  -H "Content-Type: application/json"
Swipe horizontally for full code

Body Parameters

All parameters must be nested under a payout_bid_modification_table key.

Swipe horizontally to view full table
Parameter Type Required Description
csv_data string required The rule CSV — see The rule CSV format.
active boolean Defaults to false. true activates the table immediately, deactivating whichever table was active on the campaign.
salt string Makes bucket assignment independently verifiable: the same (salt, call_uuid) pair always produces the same pbm_bucket, so whoever supplied the CSV can recompute it and confirm the assignment. Derived from your company name and the upload date if omitted. To get each call’s actual pbm_bucket alongside its Call data, see Merging Calls and RTB Inbounds.
table_version string An optional label for this upload, unique per campaign — e.g. to match a rules provider’s own revision numbers. Defaults to the table’s id. It is a label only; every route identifies a table by id.

If csv_data doesn’t parse (bad CSV, unknown operator, too many rules, over the size limit), the response is:

{ "errors": ["Csv data could not be parsed into rules (...)"] }
Swipe horizontally for full code

List a campaign’s Payout Bid Modification tables

curl "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables.json?api_key=[api_key]"
Swipe horizontally for full code

The above command returns an array of Payout Bid Modification table objects, each wrapped in its own payout_bid_modification_table key.

Returned most recently uploaded first.

HTTP Request

curl "https://api.retreaver.com/api/v5/campaigns/:campaign_id/payout_bid_modification_tables.json?api_key=[api_key]"
Swipe horizontally for full code

Get a specific Payout Bid Modification table

curl "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables/9001.json?api_key=[api_key]"
Swipe horizontally for full code

The above command returns a single Payout Bid Modification table object.

HTTP Request

curl "https://api.retreaver.com/api/v5/campaigns/:campaign_id/payout_bid_modification_tables/:id.json?api_key=[api_key]"
Swipe horizontally for full code

Activate or roll back a table

curl -s -X PUT "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables/9001.json?api_key=[api_key]" \
  -H "Content-Type: application/json" \
  -d '{ "payout_bid_modification_table": { "active": false } }'
Swipe horizontally for full code

The only field an update can change is active. To roll a campaign back to a previous upload, activate that older table’s id — this deactivates the currently active table, the same way creating with active: true does.

HTTP Request

curl -X PUT "https://api.retreaver.com/api/v5/campaigns/:campaign_id/payout_bid_modification_tables/:id.json?api_key=[api_key]" \
  -H "Content-Type: application/json"
Swipe horizontally for full code

Body Parameters

Swipe horizontally to view full table
Parameter Type Required Description
active boolean required true activates this table (deactivating the campaign’s current one); false deactivates it without activating anything else.

Delete a Payout Bid Modification table

curl -X DELETE "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables/9001.json?api_key=[api_key]"
Swipe horizontally for full code

Returns an empty 200 OK body on success.

Warning

Deleting a table is permanent and removes it from the campaign’s upload history. If you might want to roll back to it later, deactivate it instead.

HTTP Request

curl -X DELETE "https://api.retreaver.com/api/v5/campaigns/:campaign_id/payout_bid_modification_tables/:id.json?api_key=[api_key]"
Swipe horizontally for full code

Script Example: a Control/Treatment payout reduction

A common use of Payout Bid Modification is a per-publisher payout experiment: pay less on part of a publisher’s traffic and leave the rest as a control group, so the impact is measured against a baseline rather than applied blind.

This script builds one from real data. It reads the merged Calls + RTB Inbounds file from Merging Calls and RTB Inbounds, computes each publisher’s current payout rate from their claimed traffic, and generates a table that pays 10% less than that rate on half of each publisher’s traffic. The split is by bucket, so it is stable and deterministic per call. The control half gets no rule at all: it falls through to the trailing catch-all row (payout_pct = current), rather than needing an explicit current row per publisher.

Important

This is a demo, not a recommendation. A flat 10% cut, a straight sum(Payout) / sum(Revenue) rate and an even 50/50 split are one arbitrary, simple choice. The real work in a payout experiment is what this script glosses over: what “current rate” should mean for a publisher whose traffic mix varies (by hour, geography, conversion type), how big a change is worth testing and for how long, what split gives a statistically meaningful read, and how to pull fresh data and recompute as the experiment runs. Treat the shape — receive data, calculate rules, upload — as scaffolding for your own math.

Two details about the merged file’s publisher column:

  • It holds RTB Inbounds’ own affiliate_id — Retreaver’s internal ID, not a client-supplied one — so the generated rules key on system_affiliate_id, not affiliate_id. See the warning above.
  • Its header depends on your account’s terminology: Source ID on the default (Enterprise) nomenclature, Publisher ID on a Performance Marketing account. Same field underneath; the script accepts either.
# How to run:
#   ruby build_payout_reduction.rb <campaign_id> <calls_with_rtb_inbounds.csv>
#
# calls_with_rtb_inbounds.csv — from the merge example in Exports.
#
# This computes real numbers from your data and then activates a table immediately on
# real traffic — review the printed per-publisher rates before confirming.

require 'csv'
require 'json'
require 'net/http'
require 'uri'

API_KEY = "your-api-key" # <- Change to your api key https://retreaver.com/user/edit/api_access
BASE_URL = "https://api.retreaver.com"
PAYOUT_REDUCTION = 0.10  # pay 10% less than the current rate
TREATMENT_BUCKETS = 0...5000  # half the 0-9999 bucket space; the rest is the control group

campaign_id, merged_csv_path = ARGV
if campaign_id.nil? || merged_csv_path.nil?
  abort "Usage: ruby #{$PROGRAM_NAME} <campaign_id> <calls_with_rtb_inbounds.csv>"
end

rows = CSV.read(merged_csv_path, headers: true)

# The publisher-id column is "Source ID" on the default (old/Enterprise) nomenclature, or
# "Publisher ID" on a Performance Marketing account — same affiliate_id field, different
# label per company. Accept either rather than assuming one.
publisher_id_header = (rows.headers || []).find { |h| ["Source ID", "Publisher ID"].include?(h) }
unless publisher_id_header
  abort "Could not find a 'Source ID' or 'Publisher ID' column in #{merged_csv_path}"
end

claimed = rows.select { |row| row["Status"] == "claimed" && row["Campaign ID"] == campaign_id }
abort "No claimed RTB Inbounds found for campaign #{campaign_id} in #{merged_csv_path}" if claimed.empty?

other_campaigns = rows.map { |row| row["Campaign ID"] }.uniq - [campaign_id]
puts "Note: ignoring other campaign(s) in this file: #{other_campaigns.join(', ')}" if other_campaigns.any?

# Sum Revenue/Payout per publisher across their claimed rows — this becomes
# system_affiliate_id, not affiliate_id, in the rules.
totals = claimed.each_with_object(Hash.new { |h, k| h[k] = { revenue: 0.0, payout: 0.0 } }) do |row, sums|
  publisher_totals = sums[row[publisher_id_header]]
  publisher_totals[:revenue] += row["Revenue"].to_f
  publisher_totals[:payout] += row["Payout"].to_f
end

csv_rows = [["rule_id", "system_affiliate_id", "pbm_bucket", "pbm_bucket", "payout_pct"]]
rule_id = 0

puts "Payout changes for campaign #{campaign_id} (10% off on half of traffic, current rate on the other half):"

totals.each do |publisher_id, sums|
  next if sums[:revenue].zero? # no revenue, no rate to derive

  current_pct = (sums[:payout] / sums[:revenue] * 100).round(2)
  reduced_pct = (current_pct * (1 - PAYOUT_REDUCTION)).round(2)
  puts "  publisher #{publisher_id}: #{current_pct}% -> #{reduced_pct}% on bucket #{TREATMENT_BUCKETS}, unchanged on the rest"

  # Only the treatment half gets a rule. The control half is deliberately left OUT of the
  # rules entirely, rather than given its own "current" row — it falls through to the
  # catch-all below, same as any publisher/bucket we never wrote a rule for.
  rule_id += 1
  csv_rows << [rule_id, publisher_id, ">=#{TREATMENT_BUCKETS.begin}", "<#{TREATMENT_BUCKETS.end}", reduced_pct]
end

rule_id += 1
csv_rows << [rule_id, "*", "*", "*", "current"] # everything not matched above (control buckets, other publishers) stays unmodified

csv_data = csv_rows.map(&:to_csv).join
puts "\n#{csv_data}"

print "Press Enter to upload and activate this table now, or Ctrl+C to cancel... "
STDIN.gets

create_uri = URI.parse("#{BASE_URL}/api/v5/campaigns/#{campaign_id}/payout_bid_modification_tables.json?api_key=#{API_KEY}")
body = { payout_bid_modification_table: { csv_data: csv_data, active: true } }
response = JSON.parse(Net::HTTP.post(create_uri, body.to_json, "Content-Type" => "application/json").body)

if response["errors"]
  abort "Upload failed: #{response["errors"].join(", ")}"
end

# A successful create response is wrapped in a payout_bid_modification_table key —
# only a validation-error response is flat.
table = response["payout_bid_modification_table"]
puts "Uploaded and activated table id=#{table["id"]} (table_version=#{table["table_version"]}) on campaign #{campaign_id}."
Swipe horizontally for full code

Help us improve this article or request new support guides.