Adjust / Cost Integration

Modified on Wed, 4 Nov at 6:18 AM

bideaseSETUP GUIDE
How to receive Bidease spend data in Adjust: a DataWorks connection, a daily email report or Google Sheets.
⚠
SpendWorks package
Ad Spend Data Import requires the SpendWorks Enterprise or Custom package.
⚿
API token from your account manager
The DataWorks connection needs an API token provided by your Bidease account manager.
◷
Daily at 05:00 UTC
Email reports for the previous day are sent daily at 05:00 UTC.

Bidease currently uses the Ad Spend Data Import integration as the main source of spend data. More information about this integration: https://help.adjust.com/en/article/ad-spend-data-import.

To use this integration, you must be on the SpendWorks Enterprise or Custom package. Contact your Adjust CSM for detailed information about the available packages and pricing.

INTEGRATION METHODS
Sharing Spend Data
1
DataWorks connection

In the Adjust dashboard, open the sidebar and go to DataWorks → Connections.

DataWorks Connections

On the page that opens, click the New connection button and configure the integration:

FieldValue
PartnerSearch for the Bidease partner
ServicesSelect the Ad Spend option
API TokenEnter your API token, which you can get from your Bidease account manager
Custom Campaign Name FieldThe dimension to use as the data point at the campaign level. Ask your Bidease account manager for this value; if you leave the field empty, the default value is used.
Custom Creative Name FieldThe dimension to use as the data point at the ad group level. Ask your Bidease account manager for this value; if you leave the field empty, the default value is used.

After you finish configuring the integration, click Connect.

New connection settings
2
Email data sharing method

To use this method:

■
Contact your Adjust CSM and request the email address where Bidease should send daily reports
■
After receiving the email address, contact your Bidease account manager
■
The integration will be completed within 24 hours
ⓘ
Schedule: Bidease sends the data for the previous day daily at 05:00 UTC (UTC+0).
3
Google Sheets sharing method

There are a few ways to start using this method:

■
Ask your Bidease account manager to set up the integration. In this case, Bidease will create a Google Spreadsheet and share it with you
■
Send Bidease a link to your Google Spreadsheet and grant editor access to the entire bidease.com domain
■
Set up the integration yourself using Google Apps Script, e.g. with the script below
ⓘ
Issues with the script? Contact your Bidease account manager and grant editor access to the entire bidease.com domain.
APPS SCRIPT
const CONFIG = {
  apiToken: "YOUR_API_KEY",
  sheetNameSheet1: "Sheet1",
  baseUrl: "https://ui-api.bidease.com/api/reporting/v1/stats",
  groups: ["day", "operationsystem", "adbundle", "country", "campaignid"],
  filters: {},
  extras: { "group-by-param": "CustomCampaign" },

  // ===== DATES MODE =====
  // 1) Default: useManualDates = false (yesterday)
  // 2) Manual: useManualDates = true
  dateMode: {
    useManualDates: false,
    //useManualDates: true,
    //fromdate: "2026-01-01",
    //todate:   "2026-01-02"
  }
};

function run() {
  console.log("run(): START");

  try {
    const { url } = _buildBideaseUrl_();

    console.log("===== Bidease API REQUEST =====");
    console.log("Method: GET");
    console.log("URL: " + url);
    console.log("================================");

    const response = UrlFetchApp.fetch(url, { method: "GET", muteHttpExceptions: true });
    const status = response.getResponseCode();
    console.log("Response status: " + status);

    if (status !== 200) {
      console.log("Response body (non-200): " + response.getContentText());
      return;
    }

    const text = response.getContentText();
    if (!text) {
      console.log("Empty API response");
      return;
    }

    const table = Utilities.parseCsv(text, ",");
    console.log("Rows parsed: " + (table ? table.length : 0));
    if (!table || table.length < 2) {
      console.log("No data rows");
      return;
    }

    _appendToSheet1_(table);
  } catch (e) {
    console.log("Execution error: " + (e && e.stack ? e.stack : e));
  } finally {
    console.log("run(): END");
  }
}

function _getDateParams_() {
  const fmt = (d) => Utilities.formatDate(d, "UTC", "yyyy-MM-dd");

  const dm = CONFIG.dateMode || {};
  if (dm.useManualDates) {
    const fromdate = String(dm.fromdate || "").trim();
    const todate = String(dm.todate || "").trim();

    if (!fromdate || !todate) {
      throw new Error('Manual dates are enabled, but fromdate/todate are not set in CONFIG.dateMode');
    }

    const re = /^\d{4}-\d{2}-\d{2}$/;
    if (!re.test(fromdate) || !re.test(todate)) {
      throw new Error('fromdate/todate must be in format "YYYY-MM-DD"');
    }

    return { fromdate, todate };
  }

  const now = new Date();

  const todayUTC = new Date(Date.UTC(
    now.getUTCFullYear(),
    now.getUTCMonth(),
    now.getUTCDate()
  ));

  const yesterdayUTC = new Date(todayUTC);
  yesterdayUTC.setUTCDate(todayUTC.getUTCDate() - 1);

  return { fromdate: fmt(yesterdayUTC), todate: fmt(todayUTC) };
}

function _buildBideaseUrl_() {
  const dates = _getDateParams_();

  const params = {
    "api-token": CONFIG.apiToken,
    fromdate: dates.fromdate,
    todate: dates.todate
  };

  const pairs = [];
  _pushParamPairs_(pairs, params);

  (CONFIG.groups || []).forEach((g) => pairs.push(["group", g]));
  _pushParamPairs_(pairs, CONFIG.filters || {});
  _pushParamPairs_(pairs, CONFIG.extras || {});

  const query = pairs
    .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
    .join("&");

  return { url: `${CONFIG.baseUrl}?${query}` };
}

function _pushParamPairs_(out, obj) {
  Object.keys(obj || {}).forEach((k) => {
    const v = obj[k];
    if (v === null || v === undefined) return;

    if (Array.isArray(v)) {
      v.forEach((item) => {
        if (item === null || item === undefined || item === "") return;
        out.push([k, item]);
      });
      return;
    }

    if (v === "") return;
    out.push([k, v]);
  });
}

function _appendToSheet1_(table) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss) throw new Error("Active spreadsheet not found");

  const sheet = ss.getSheetByName(CONFIG.sheetNameSheet1);
  if (!sheet) throw new Error(`Sheet "${CONFIG.sheetNameSheet1}" not found`);

  const header = table[0];
  const idx = _indexMap_(header);

  const required = ["day", "CustomCampaign", "campaignid", "country", "adbundle", "operationsystem", "spend"];
  const missing = required.filter((k) => idx[k] === undefined);
  if (missing.length) {
    console.log("Header row: " + JSON.stringify(header));
    throw new Error("Missing column(s): " + missing.join(", "));
  }

  if (sheet.getLastRow() === 0) {
    sheet.getRange(1, 1, 1, 7).setValues([[
      "date", "campaign_name", "campaign_id", "country", "store_id", "store_type", "ad_spend"
    ]]);
  }

  let total = 0;
  let skippedZero = 0;
  let skippedBadDate = 0;

  const badDateSamples = [];
  const out = [];
  const iso3List = table.slice(1).map(r => r[idx["country"]]);
  const isoMap = _iso3ToIso2ViaRestCountries_(iso3List);

  for (const row of table.slice(1)) {
    total++;

    const spend = _parseNumber_(row[idx["spend"]]);
    if (spend === 0) {
      skippedZero++;
      continue;
    }

    const os = String(row[idx["operationsystem"]] || "").toLowerCase().trim();

    const rawDay = row[idx["day"]];
    const dateObj = _parseDayToDate_(rawDay);
    if (!dateObj) {
      skippedBadDate++;
      if (badDateSamples.length < 5) badDateSamples.push(String(rawDay));
      continue;
    }

    out.push([
      dateObj,
      row[idx["CustomCampaign"]],
      row[idx["campaignid"]],
      _iso3ToIso2_(row[idx["country"]], isoMap),
      row[idx["adbundle"]],
      os === "android" ? "google_play" : os === "ios" ? "app_store" : "",
      spend
    ]);
  }

  console.log("Rows total: " + total);
  console.log("Skipped spend=0: " + skippedZero);
  console.log("Skipped bad date: " + skippedBadDate);
  if (badDateSamples.length) console.log("Bad day samples: " + JSON.stringify(badDateSamples));
  console.log("Rows to append: " + out.length);

  if (!out.length) return;

  const startRow = sheet.getLastRow() + 1;
  sheet.getRange(startRow, 1, out.length, out[0].length).setValues(out);
  sheet.getRange(startRow, 1, out.length, 1).setNumberFormat("yyyy-mm-dd");

  console.log("Appended: " + out.length);
}

function _indexMap_(headerRow) {
  const map = {};
  (headerRow || []).forEach((name, i) => {
    const key = String(name).replace(/^\uFEFF/, "").trim();
    map[key] = i;
  });
  return map;
}

function _parseNumber_(value) {
  if (value === null || value === undefined) return 0;
  const s = String(value).trim();
  if (!s) return 0;
  const cleaned = s.replace(/\s|\u00A0/g, "").replace(",", ".");
  const n = Number(cleaned);
  return Number.isFinite(n) ? n : 0;
}

function _parseDayToDate_(dayValue) {
  if (Object.prototype.toString.call(dayValue) === "[object Date]" && !isNaN(dayValue.getTime())) {
    return new Date(dayValue.getFullYear(), dayValue.getMonth(), dayValue.getDate(), 12, 0, 0);
  }

  const s0 = String(dayValue || "").trim();
  if (!s0) return null;

  const s = s0.replace(/^["']+|["']+$/g, "").trim();

  let m = s.match(/(\d{4})-(\d{2})-(\d{2})/);
  if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12, 0, 0);

  m = s.match(/(\d{2})\/(\d{2})\/(\d{4})/);
  if (m) return new Date(Number(m[3]), Number(m[1]) - 1, Number(m[2]), 12, 0, 0);

  m = s.match(/(\d{4})\/(\d{2})\/(\d{2})/);
  if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12, 0, 0);

  m = s.match(/(\d{2})\.(\d{2})\.(\d{4})/);
  if (m) return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]), 12, 0, 0);

  return null;
}

function _iso3ToIso2ViaRestCountries_(iso3List) {
  const cache = CacheService.getScriptCache();

  const uniq = {};
  (iso3List || []).forEach((x) => {
    const k = String(x || "").trim().toUpperCase();
    if (k) uniq[k] = true;
  });
  const codes = Object.keys(uniq);
  if (!codes.length) return {};

  const cached = cache.getAll(codes);
  const map = {};
  const missing = [];

  codes.forEach((c) => {
    const v = cached[c];
    if (v) map[c] = v;
    else missing.push(c);
  });

  if (!missing.length) return map;

  const chunkSize = 50;
  for (let i = 0; i < missing.length; i += chunkSize) {
    const chunk = missing.slice(i, i + chunkSize);

    const url =
      "https://restcountries.com/v3.1/alpha?codes=" +
      encodeURIComponent(chunk.join(",")) +
      "&fields=cca3,cca2";

    try {
      const resp = UrlFetchApp.fetch(url, { method: "GET", muteHttpExceptions: true });
      const status = resp.getResponseCode();

      if (status !== 200) {
        console.log("REST Countries non-200: " + status + " body=" + resp.getContentText());
        continue;
      }

      const data = JSON.parse(resp.getContentText() || "[]");
      if (!Array.isArray(data)) continue;

      const toCache = {};
      data.forEach((item) => {
        const cca3 = String(item && item.cca3 || "").trim().toUpperCase();
        const cca2 = String(item && item.cca2 || "").trim().toUpperCase();
        if (cca3 && cca2) {
          map[cca3] = cca2;
          toCache[cca3] = cca2;
        }
      });

      cache.putAll(toCache, 21600);
    } catch (e) {
      console.log("REST Countries fetch error: " + (e && e.stack ? e.stack : e));
    }
  }

  return map;
}

function _iso3ToIso2_(iso3, isoMap) {
  const key = String(iso3 || "").trim().toUpperCase();
  if (!key) return "";
  return (isoMap && isoMap[key]) ? isoMap[key] : key;
}
bidease
Grow Your App with Ease
Questions? Contact your Bidease account manager
support@bidease.com

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article