# Exports API

Exports generate a CSV of a large historical range in the background, so you don't have to page through thousands of rows yourself. The flow is always three steps: create the export, poll it until it is finished, then download the file from the URL the export gives you.

The only export type available today is `rtb_inbounds` — see [RTB Inbounds](/api/rtb-inbounds). Calls have no async export yet; page through the [Calls API](/api/calls) directly, as the [API Code Example](/api/introduction#api-code-example) does.

## Export object

```json
{
  "id": 501,
  "export_count": 168,
  "export_progress": 168,
  "export_percentage": 100.0,
  "format": "csv",
  "file_upload_url": "/v2/reports/rtb_inbounds/501/download",
  "in_progress": false,
  "errors": {}
}
```

### Fields

| Field               | Type              | Description                                                                                                                                  |
| ------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                | integer           | The export's ID. Use it to poll the export.                                                                                                  |
| `format`            | string            | `csv`.                                                                                                                                       |
| `in_progress`       | boolean           | `true` while generating; `false` once finished — or failed, so check `errors`.                                                               |
| `export_count`      | integer \| null   | `null` while generating. Once finished, the number of rows in the export.                                                                    |
| `export_progress`   | integer           | `0` while generating. Once finished, the same as `export_count`.                                                                             |
| `export_percentage` | number            | `0.0` while generating; `100.0` once finished.                                                                                               |
| `file_upload_url`   | string \| null    | Path to download the finished file from, relative to `https://api.retreaver.com` — not a full URL. `null` until the export is done.          |
| `errors`            | object            | Populated if the export request itself was invalid, e.g. a bad `params` filter.                                                              |

## Create an export

```shell
curl -X POST "https://api.retreaver.com/api/v5/exports/rtb_inbounds.json?api_key=[api_key]" \
  -H "Content-Type: application/json" \
  -d '{
        "export": {
          "format": "csv",
          "params": { "campaign_id": [123], "created_at": "last_7d" }
        }
      }'
```

> The above command returns JSON structured like this:

```json
{
  "id": 501,
  "export_count": null,
  "export_progress": 0,
  "export_percentage": 0.0,
  "format": "csv",
  "file_upload_url": null,
  "in_progress": true,
  "errors": {}
}
```

### HTTP Request

```shell
curl -X POST "https://api.retreaver.com/api/v5/exports/rtb_inbounds.json?api_key=[api_key]" \
  -H "Content-Type: application/json"
```

### Body Parameters

All parameters must be nested under an `export` key.

| Parameter | Type   | Required | Description                                                                                                                                                                                                                                                                                                   |
| --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `format`  | string |          | Only `csv` is supported, and it is the default.                                                                                                                                                                                                                                                               |
| `params`  | object | required | The filter to export. Takes the same keys as the [RTB Inbounds query parameters](/api/rtb-inbounds#query-parameters) (`campaign_id`, `affiliate_id`, `target_id`, `caller_state`, `status`, and so on, as plain values or arrays), plus either `created_at` (a relative window like `last_7d`) or `created_at_start` / `created_at_end`. The window is capped at 2 months, as on the live log. |

## Check an export's status

```shell
curl "https://api.retreaver.com/api/v5/exports/rtb_inbounds/501.json?api_key=[api_key]"
```

> The above command returns an [Export object](#export-object).

Poll until `in_progress` is `false`. While an export is generating, `export_count` stays `null` and `export_progress` / `export_percentage` stay at `0` — there is no partial progress to report.

### HTTP Request

```shell
curl "https://api.retreaver.com/api/v5/exports/rtb_inbounds/:id.json?api_key=[api_key]"
```

## Download the export

```shell
curl -L -o rtb_inbounds.csv.gz \
  "https://api.retreaver.com/v2/reports/rtb_inbounds/501/download?api_key=[api_key]"
gunzip rtb_inbounds.csv.gz
```

Once `in_progress` is `false`, prepend `https://api.retreaver.com` to the export's `file_upload_url`. Don't build the path yourself — only prepend the host, since the path is not under `/api/v5/`. It authenticates the same way (`api_key`) and redirects to the actual file.

> [!NOTE]
> The file is always gzip-compressed, whatever the export's `format`. Decompress it before reading — `gunzip`, or `Zlib::GzipReader` or your language's equivalent, as in the scripts below.

## Script Example: Downloading an RTB Inbounds export

Creates an export, polls it, downloads it, and decompresses it to a date-ranged CSV in the current directory, named `rtb_inbounds_<start>-<end>.csv` (e.g. `rtb_inbounds_20260908-20260915.csv`). It is the scripted version of the three steps above.

```ruby
# How to run:
#   1. Save this script to a file, e.g. download_rtb_inbounds.rb
#   2. Edit API_KEY below
#   3. Run: ruby download_rtb_inbounds.rb
#   4. You'll end up with one result file in the current directory, named
#      rtb_inbounds_<start>-<end>.csv (e.g. rtb_inbounds_20260908-20260915.csv)

require 'net/http'
require 'uri'
require 'json'
require 'time'
require 'zlib'
require 'stringio'

API_KEY = "your-api-key" # <- Change to your api key https://retreaver.com/user/edit/api_access
BASE_URL = "https://api.retreaver.com"

# The download itself is a redirect (302) to the actual file, not the file itself —
# Net::HTTP doesn't follow redirects on its own.
def http_get_following_redirects(uri, limit = 5)
  raise "too many redirects" if limit == 0

  response = Net::HTTP.get_response(uri)
  response.is_a?(Net::HTTPRedirection) ? http_get_following_redirects(URI.parse(response['location']), limit - 1) : response
end

# Create + poll + download an RTB Inbounds export, saving the decompressed CSV to `path`.
def fetch_rtb_inbounds_csv(created_at_start:, created_at_end:, path:)
  puts "Creating RTB Inbounds export..."
  create_uri = URI.parse("#{BASE_URL}/api/v5/exports/rtb_inbounds.json?api_key=#{API_KEY}")
  create_body = { export: { format: "csv", params: { created_at_start: created_at_start, created_at_end: created_at_end } } }
  export = JSON.parse(Net::HTTP.post(create_uri, create_body.to_json, "Content-Type" => "application/json").body)
  puts "  export id=#{export['id']} created, polling until it's done..."

  show_uri = URI.parse("#{BASE_URL}/api/v5/exports/rtb_inbounds/#{export['id']}.json?api_key=#{API_KEY}")
  loop do
    sleep 2
    export = JSON.parse(Net::HTTP.get_response(show_uri).body)
    # export_progress/export_count don't report partial progress — poll on in_progress itself.
    puts "  in_progress=#{export['in_progress']}..."
    break unless export['in_progress']
  end

  # file_upload_url is a path (e.g. "/v2/reports/rtb_inbounds/501/download"), not a full URL — prepend BASE_URL.
  puts "Downloading export from #{export['file_upload_url']}..."
  download_uri = URI.parse("#{BASE_URL}#{export['file_upload_url']}?api_key=#{API_KEY}")
  response = http_get_following_redirects(download_uri)
  puts "  downloaded #{response.body.bytesize} gzip-compressed bytes."

  # The export is always written gzip-compressed — decompress before use.
  csv_text = Zlib::GzipReader.new(StringIO.new(response.body)).read
  File.write(path, csv_text)
  puts "  saved #{csv_text.bytesize} bytes (decompressed) to #{path}"

  path
end

created_at_start = (Time.now - 7 * 24 * 3600).utc
created_at_end = Time.now.utc
date_range = "#{created_at_start.strftime('%Y%m%d')}-#{created_at_end.strftime('%Y%m%d')}"

puts "About to export RTB Inbounds created between #{created_at_start.iso8601} and #{created_at_end.iso8601}"
puts "  using api_key=#{API_KEY}"
print "Press Enter to continue, or Ctrl+C to cancel... "
STDIN.gets

rtb_inbounds_csv_path = fetch_rtb_inbounds_csv(
  created_at_start: created_at_start.iso8601,
  created_at_end: created_at_end.iso8601,
  path: "rtb_inbounds_#{date_range}.csv",
)

puts "\nDone! RTB Inbounds saved to #{File.expand_path(rtb_inbounds_csv_path)}"
```

## Example: merging Calls and RTB Inbounds by call_uuid

RTB Inbounds rows carry the `call_uuid` of the Call each reservation belongs to, so you can enrich an export with Call data, or the reverse, by joining on it. This script fetches neither dataset; it joins two files you already have:

- a Calls JSON file, e.g. `calls_20260915_180831.json` from the Ruby [API Code Example](/api/introduction#api-code-example).
- an RTB Inbounds CSV, e.g. `rtb_inbounds_20260908-20260915.csv` from [Downloading an RTB Inbounds export](#script-example-downloading-an-rtb-inbounds-export) above.

Fetch both for the same time window, or the unmatched rows on each side will mostly be an artifact of the windows not lining up.

The join is a **full outer join** on `call_uuid`. Every RTB Inbounds row is kept even when it has no matching Call (a `rejected` or `no-target` ping never creates one), and every Call is kept even when no RTB Inbounds row references it (it wasn't reserved through RTB). The unmatched side is left blank rather than the row dropped. Columns are ordered RTB Inbounds first, then Call.

```ruby
# How to run:
#   ruby merge_calls_with_rtb_inbounds.rb <calls.json> <rtb_inbounds.csv>
#
# calls.json       — from the API Code Example in the Introduction
# rtb_inbounds.csv — from the Script Example: Downloading an RTB Inbounds export, above

require 'json'
require 'csv'

calls_path, rtb_inbounds_csv_path = ARGV
if calls_path.nil? || rtb_inbounds_csv_path.nil?
  abort "Usage: ruby #{$PROGRAM_NAME} <calls.json> <rtb_inbounds.csv>"
end

# Each element of the Calls API response is root-wrapped ({"call": {...}}) — unwrap it
# so every entry is a plain call hash.
all_calls = JSON.parse(File.read(calls_path)).map { |entry| entry["call"] || entry }
rtb_inbounds = CSV.read(rtb_inbounds_csv_path, headers: true)
calls_by_uuid = all_calls.each_with_object({}) { |call, h| h[call["uuid"]] = call }

# A Call field can be an array/hash (downstream_call_uuids, target_group, ...) — flatten those to a
# JSON string so they round-trip through a single CSV cell.
def csv_value(value)
  value.is_a?(Array) || value.is_a?(Hash) ? value.to_json : value
end

# RTB Inbounds columns first, then one column per key found across all_calls' own JSON keys.
rtb_headers = rtb_inbounds.headers || []
call_headers = all_calls.flat_map(&:keys).uniq
matched_call_uuids = {}
written = 0

puts "Merging #{rtb_inbounds.length} RTB Inbounds rows with #{all_calls.length} calls by call_uuid..."

CSV.open("calls_with_rtb_inbounds.csv", "w") do |out|
  out << rtb_headers + call_headers

  rtb_inbounds.each do |row|
    call = calls_by_uuid[row["Call UUID"]]
    matched_call_uuids[row["Call UUID"]] = true if call

    out << rtb_headers.map { |h| row[h] } + call_headers.map { |h| call && csv_value(call[h]) }
    written += 1
  end

  all_calls.each do |call|
    next if matched_call_uuids[call["uuid"]]

    out << rtb_headers.map { nil } + call_headers.map { |h| csv_value(call[h]) }
    written += 1
  end
end

puts "Wrote #{written} rows (#{rtb_inbounds.length} from RTB Inbounds, #{written - rtb_inbounds.length} Calls with no RTB Inbounds row) to calls_with_rtb_inbounds.csv."
```

> [!NOTE]
> The RTB Inbounds CSV headers are human-readable labels (`Call UUID`, `Status`), not the JSON field names (`call_uuid`, `status`) the [RTB Inbounds](/api/rtb-inbounds) endpoint returns. Calls have no such relabeling: each Call's own JSON keys (`uuid`, `status`, …) become its columns as-is.

The merged `calls_with_rtb_inbounds.csv` is the input to the [Control/Treatment payout reduction](/api/payout-bid-modification-tables#script-example-a-controltreatment-payout-reduction) script.
