Pular para o conteúdo

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 mesmo preparo da receita anterior: chave, OWIEW_API_URL e 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_centavos
    c1,60,720000
    c2,10,150000

    O campaign_id precisa 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.

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.

conciliar.sh
set -euo pipefail
SINCE="${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.json

Colunas da saída: campanha, gasto em micros, conversões reportadas, pedidos pagos, diferença e ROAS real.

conciliar.mjs
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"));
conciliar.py
import csv
import os
import 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 = 0
conv_total = 0.0
for 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")

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.80
Remarketing 2500333333 20.5 10 10.5 0.59
TOTAL 4000333333 95.5 70 25.5 2.17

O 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.

  • Todos os pedidos aparecem como zero: o campaign_id do 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 currency antes de somar. Veja Unidades e tipos.