Pular para o conteúdo

Receita: gasto por campanha de um mês

Objetivo: listar o gasto, as conversões e o CPA de cada campanha de um mês, com totais por moeda, sem erro de unidade.

  • Uma chave de API em OWIEW_API_KEY (veja Início rápido).
  • OWIEW_API_URL apontando para https://dash.owiew.com.
  • Para o curl: jq. Para JavaScript: Node.js 18 ou superior. Para Python: o pacote requests.
  1. Troca a chave por um token em POST /v1/token.
  2. Lê GET /v1/t/{tenantID}/campaigns com since e until do mês.
  3. Converte micros com inteiros, soma por moeda e calcula o CPA do total a partir dos totais somados, nunca pela média dos CPAs.
gasto-por-campanha.sh
set -euo pipefail
SINCE="${1:-2026-09-01}"
UNTIL="${2:-2026-09-30}"
TOKEN_JSON=$(curl -sS -X POST "$OWIEW_API_URL/v1/token" \
-H "Authorization: Bearer $OWIEW_API_KEY")
TOKEN=$(echo "$TOKEN_JSON" | jq -r .access_token)
TENANT=$(echo "$TOKEN_JSON" | jq -r .tenant_id)
curl -sS "$OWIEW_API_URL/v1/t/$TENANT/campaigns?since=$SINCE&until=$UNTIL" \
-H "Authorization: Bearer $TOKEN" > campanhas.json
echo "Por campanha (gasto_micros, conversoes_micros):"
jq -r '.campaigns[] | [.campaign_name, .currency, .spend_micros, .conversions_micros] | @tsv' campanhas.json
echo "Totais por moeda:"
jq -r '.campaigns | group_by(.currency)[]
| {moeda: .[0].currency, gasto: (map(.spend_micros) | add), conv: (map(.conversions_micros) | add)}
| "\(.moeda) gasto_micros=\(.gasto) conversoes_micros=\(.conv)"' campanhas.json

Rode com bash gasto-por-campanha.sh 2026-09-01 2026-09-30. O jq usa números de ponto flutuante, exatos até 2^53; para valores maiores use JavaScript ou Python.

gasto-por-campanha.mjs
const BASE = process.env.OWIEW_API_URL ?? "https://dash.owiew.com";
const CHAVE = process.env.OWIEW_API_KEY;
if (!CHAVE) throw new Error("Defina OWIEW_API_KEY");
const [since = "2026-09-01", until = "2026-09-30"] = process.argv.slice(2);
async function pegarJson(url, opcoes) {
const resposta = await fetch(url, opcoes);
const corpo = await resposta.json().catch(() => null);
if (!resposta.ok) {
const e = corpo?.error ?? {};
throw new Error(`HTTP ${resposta.status} ${e.code ?? ""} (trace_id ${e.trace_id ?? "n/d"})`);
}
return corpo;
}
/** Micros em texto "1500,00", arredondando para centavos com inteiros. */
function moeda(micros) {
const centavos = (BigInt(micros) + 5_000n) / 10_000n;
return `${centavos / 100n},${(centavos % 100n).toString().padStart(2, "0")}`;
}
/** Conversões em micros para texto com 2 casas. */
function conversoes(micros) {
return moeda(micros);
}
const token = await pegarJson(`${BASE}/v1/token`, {
method: "POST",
headers: { Authorization: `Bearer ${CHAVE}` },
});
const url = `${BASE}/v1/t/${token.tenant_id}/campaigns?since=${since}&until=${until}`;
const { campaigns } = await pegarJson(url, {
headers: { Authorization: `Bearer ${token.access_token}` },
});
const porMoeda = new Map();
for (const c of campaigns) {
const t = porMoeda.get(c.currency) ?? { gasto: 0n, conv: 0n };
t.gasto += BigInt(c.spend_micros);
t.conv += BigInt(c.conversions_micros);
porMoeda.set(c.currency, t);
const cpa = c.conversions_micros > 0
? moeda((BigInt(c.spend_micros) * 1_000_000n) / BigInt(c.conversions_micros))
: "sem conversão";
console.log(`${c.campaign_name}\t${c.currency} ${moeda(c.spend_micros)}\t${conversoes(c.conversions_micros)} conv\tCPA ${cpa}`);
}
for (const [codigo, t] of porMoeda) {
const cpa = t.conv > 0n ? moeda((t.gasto * 1_000_000n) / t.conv) : "sem conversão";
console.log(`TOTAL ${codigo}\t${moeda(t.gasto)}\t${conversoes(t.conv)} conv\tCPA ${cpa}`);
}

Rode com node gasto-por-campanha.mjs 2026-09-01 2026-09-30.

gasto_por_campanha.py
import os
import sys
from collections import defaultdict
import requests
BASE = os.environ.get("OWIEW_API_URL", "https://dash.owiew.com")
CHAVE = os.environ["OWIEW_API_KEY"]
since = sys.argv[1] if len(sys.argv) > 1 else "2026-09-01"
until = sys.argv[2] if len(sys.argv) > 2 else "2026-09-30"
def checar(r):
if r.ok:
return r.json()
try:
e = r.json().get("error", {})
except ValueError:
e = {}
raise SystemExit(f"HTTP {r.status_code} {e.get('code', '')} (trace_id {e.get('trace_id', 'n/d')})")
def moeda(micros):
"""Micros em texto '1500,00', arredondando para centavos com inteiros."""
centavos = (micros + 5_000) // 10_000
return f"{centavos // 100},{centavos % 100:02d}"
token = checar(requests.post(f"{BASE}/v1/token", headers={"Authorization": f"Bearer {CHAVE}"}, timeout=30))
r = requests.get(
f"{BASE}/v1/t/{token['tenant_id']}/campaigns",
params={"since": since, "until": until},
headers={"Authorization": f"Bearer {token['access_token']}"},
timeout=60,
)
campanhas = checar(r)["campaigns"]
totais = defaultdict(lambda: [0, 0])
for c in campanhas:
totais[c["currency"]][0] += c["spend_micros"]
totais[c["currency"]][1] += c["conversions_micros"]
cpa = moeda(c["spend_micros"] * 1_000_000 // c["conversions_micros"]) if c["conversions_micros"] else "sem conversão"
print(f"{c['campaign_name']}\t{c['currency']} {moeda(c['spend_micros'])}\t{moeda(c['conversions_micros'])} conv\tCPA {cpa}")
for codigo, (gasto, conv) in totais.items():
cpa = moeda(gasto * 1_000_000 // conv) if conv else "sem conversão"
print(f"TOTAL {codigo}\t{moeda(gasto)}\t{moeda(conv)} conv\tCPA {cpa}")

Rode com python gasto_por_campanha.py 2026-09-01 2026-09-30.

Uma linha por campanha e uma linha de total por moeda, no formato (valores de exemplo):

Marca BRL 1500,00 75,00 conv CPA 20,00
Remarketing BRL 2500,33 20,50 conv CPA 121,97
TOTAL BRL 4000,33 95,50 conv CPA 41,89

O CPA do total (41,89) não é a média dos CPAs das campanhas. Ele é o gasto somado dividido pelas conversões somadas.

  • HTTP 401: a chave não vale ou venceu. Veja Autenticação.
  • HTTP 400 INVALID_WINDOW: datas inválidas. Veja Janelas de data e paginação.
  • Lista vazia: confira o período e o estado das contas em /accounts (last_date e last_observed_at). Veja Unidades e tipos.
  • Valores 10 mil vezes maiores que o esperado: você tratou micros como centavos.
  • Para guardar o resultado, releia pelo menos os últimos 28 dias em cada execução, porque as plataformas revisam números já reportados.