Receita: conciliar com pedidos
Objetivo: cruzar as conversões e o gasto por campanha da API com os pedidos pagos da sua própria loja, e calcular o ROAS real do período.
O que você precisa
Seção intitulada “O que você precisa”-
O mesmo preparo da receita anterior: chave,
OWIEW_API_URLe as ferramentas da sua linguagem (veja Receita: gasto por campanha de um mês). -
Um arquivo
pedidos.csv, exportado da sua loja ou do seu sistema, com os pedidos pagos do mesmo período, já somados por campanha. Colunas:campaign_id,pedidos_pagos,receita_centavosc1,60,720000c2,10,150000O
campaign_idprecisa ser o mesmo identificador de campanha da plataforma de anúncio. Para isso, o anúncio deve levar o identificador da campanha nos parâmetros de URL, e a sua loja deve gravá-lo no pedido. Sem esse elo, não há como conciliar por campanha.
Por que os dois números divergem
Seção intitulada “Por que os dois números divergem”A plataforma de anúncio conta a conversão no dia do clique e pode incluir pedido cancelado, estornado ou nunca pago. O seu sistema conta o dinheiro que entrou. Os dois respondem perguntas diferentes: “quantas compras o anúncio originou” e “quanto entrou no caixa”. A diferença entre eles não é erro. Ela pode ser negativa: quem clicou há dez dias e comprou hoje aparece no seu sistema e não no período da plataforma.
Por isso a receita calcula o ROAS real a partir da receita confirmada pelo seu sistema e do gasto da API, ambos somados:
ROAS real = receita paga / gasto
No código, receita_centavos * 10000 / spend_micros dá o mesmo resultado, porque 1 centavo vale 10.000 micros.
O curl busca os dados da API. O cruzamento com o CSV é feito com jq.
set -euo pipefailSINCE="${1:-2026-09-01}"UNTIL="${2:-2026-09-30}"PEDIDOS="${3:-pedidos.csv}"
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
# CSV vira JSON: {"c1": {"pedidos": 60, "receita": 720000}, ...}PEDIDOS_JSON=$(tail -n +2 "$PEDIDOS" | jq -R -s ' split("\n") | map(select(length > 0) | split(",")) | map({key: .[0], value: {pedidos: (.[1] | tonumber), receita: (.[2] | tonumber)}}) | from_entries')
jq -r --argjson pedidos "$PEDIDOS_JSON" ' .campaigns[] as $c | ($pedidos[$c.campaign_id] // {pedidos: 0, receita: 0}) as $p | [$c.campaign_name, $c.spend_micros, ($c.conversions_micros / 1000000), $p.pedidos, (($c.conversions_micros / 1000000) - $p.pedidos), (if $c.spend_micros > 0 then (($p.receita * 10000 * 100 / $c.spend_micros) | floor / 100) else 0 end)] | @tsv' campanhas.jsonColunas da saída: campanha, gasto em micros, conversões reportadas, pedidos pagos, diferença e ROAS real.
JavaScript (fetch)
Seção intitulada “JavaScript (fetch)”import { readFileSync } from "node:fs";
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", arquivo = "pedidos.csv"] = 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;}
/** Lê o CSV simples (sem aspas) para um mapa campaign_id -> { pedidos, receita }. */function lerPedidos(caminho) { const mapa = new Map(); for (const linha of readFileSync(caminho, "utf8").split("\n").slice(1)) { if (!linha.trim()) continue; const [id, pedidos, receita] = linha.split(","); mapa.set(id, { pedidos: Number(pedidos), receita: BigInt(receita) }); } return mapa;}
/** ROAS com 2 casas, só com inteiros: receita em centavos x 10.000 / gasto em micros. */function roas(receitaCentavos, gastoMicros) { if (gastoMicros === 0n) return "0.00"; const x100 = (receitaCentavos * 10_000n * 100n) / gastoMicros; return `${x100 / 100n}.${(x100 % 100n).toString().padStart(2, "0")}`;}
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 pedidos = lerPedidos(arquivo);let gastoTotal = 0n, receitaTotal = 0n, convTotal = 0, pedidosTotal = 0;
for (const c of campaigns) { const p = pedidos.get(c.campaign_id) ?? { pedidos: 0, receita: 0n }; const conv = c.conversions_micros / 1_000_000; gastoTotal += BigInt(c.spend_micros); receitaTotal += p.receita; convTotal += conv; pedidosTotal += p.pedidos; console.log([c.campaign_name, c.spend_micros, conv, p.pedidos, conv - p.pedidos, roas(p.receita, BigInt(c.spend_micros))].join("\t"));}console.log(["TOTAL", gastoTotal, convTotal, pedidosTotal, convTotal - pedidosTotal, roas(receitaTotal, gastoTotal)].join("\t"));Python (requests)
Seção intitulada “Python (requests)”import csvimport osimport sys
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"arquivo = sys.argv[3] if len(sys.argv) > 3 else "pedidos.csv"
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 roas(receita_centavos, gasto_micros): """ROAS com 2 casas, só com inteiros: centavos x 10.000 / micros.""" if gasto_micros == 0: return "0.00" x100 = receita_centavos * 10_000 * 100 // gasto_micros return f"{x100 // 100}.{x100 % 100:02d}"
with open(arquivo, newline="", encoding="utf-8") as f: pedidos = { linha["campaign_id"]: (int(linha["pedidos_pagos"]), int(linha["receita_centavos"])) for linha in csv.DictReader(f) }
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"]
gasto_total = receita_total = pedidos_total = 0conv_total = 0.0for c in campanhas: n_pedidos, receita = pedidos.get(c["campaign_id"], (0, 0)) conv = c["conversions_micros"] / 1_000_000 gasto_total += c["spend_micros"] receita_total += receita pedidos_total += n_pedidos conv_total += conv print(c["campaign_name"], c["spend_micros"], conv, n_pedidos, conv - n_pedidos, roas(receita, c["spend_micros"]), sep="\t")
print("TOTAL", gasto_total, conv_total, pedidos_total, conv_total - pedidos_total, roas(receita_total, gasto_total), sep="\t")Resultado esperado
Seção intitulada “Resultado esperado”Com os dados de exemplo, uma linha por campanha e uma de total. A coluna de diferença mostra quantas conversões a plataforma reportou a mais (ou a menos) do que os pedidos pagos (valores de exemplo):
Marca 1500000000 75 60 15 4.80Remarketing 2500333333 20.5 10 10.5 0.59TOTAL 4000333333 95.5 70 25.5 2.17O ROAS é truncado em duas casas. A versão em Python pode mostrar 75.0 onde as outras mostram 75, e a versão em curl não imprime a linha de total. O ROAS do total é a receita somada dividida pelo gasto somado, e não a média dos ROAS das linhas.
Se algo der errado
Seção intitulada “Se algo der errado”- Todos os pedidos aparecem como zero: o
campaign_iddo CSV não é o da plataforma. Confira se a sua loja grava o identificador da campanha do anúncio. - Diferença grande e constante: normal para quem vende com boleto, Pix ou parcelado, porque o pedido paga depois do clique. Concilie por períodos fechados.
- Receita de balcão ou de WhatsApp fora do CSV: o ROAS real fica subestimado. Inclua essas vendas se a origem for conhecida.
- Moedas diferentes na mesma saída: filtre por
currencyantes de somar. Veja Unidades e tipos.