Conciliar vendas e assinaturas

Baixe todas as vendas e assinaturas de um período e compare com o seu sistema, sem perder nem duplicar registros.

Use este guia para conferir, no fim de um período, se o seu sistema tem todas as vendas e assinaturas da PagPolar, com os valores e status certos.

Durante o período, os webhooks avisam cada mudança. A conciliação usa a API para achar o que ficou de fora.

Visão geral

O diagrama mostra as duas fontes de dados e a comparação no fim do período.

Antes de começar

Quais identificadores usar

CampoOnde apareceUse para
idVenda na API (data[].id) e no webhook (data.transaction.id)Chave principal da conciliação. Não muda e não se repete.
identifierVenda na API e no webhook, com o mesmo valorCódigo da venda: PPO seguido de 10 dígitos, como PPO9876543210. Mostrar o código ao cliente e filtrar GET /refunds por sale_identifier. Para juntar os registros, prefira o id.
external_referenceSó na venda da APILigar a venda ao código do seu pedido. Não chega no webhook.
cycleVenda na API e no webhookPosição da cobrança na assinatura: 1, 2, 3...
data.subscription.idSó no webhook de vendaLigar a venda à assinatura. A venda da API não traz a assinatura.

Passo a passo

Guarde o id e a sua referência na criação

Na cobrança, envie external_reference com o código do seu pedido, como PEDIDO-1234. Não use o formato do código da venda. Veja Como a venda é encontrada.

A resposta 201 de POST /payments/pix, POST /payments/boleto e POST /payments/credit-card traz data.transactions: a lista de id das vendas criadas. Grave esses id junto do seu pedido.

Resposta resumida:

{
  "data": {
    "offer_identifier": "PPP1234567890",
    "transactions": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]
  }
}

Na assinatura, a resposta 201 de POST /plans/offer/{id}/subscribe traz a assinatura em data. Grave o data.id.

A referência fica só na primeira venda da assinatura

Na assinatura, external_reference é gravada na venda do primeiro ciclo. As vendas das renovações não têm referência. Por isso, GET /sales?external_reference=PEDIDO-1234 traz só a primeira venda.

Grave o net_amount que chega no webhook

Cada evento de venda traz os valores da venda. net_amount é o valor da venda sem os juros do parcelamento: total_amount menos installment_tax. Ele só existe no webhook.

Os valores em dinheiro estão em reais. O tipo muda entre o webhook e a API (veja Valores nas respostas):

CampoNo webhookNa API
total_amountTexto: "197.0000"Número: 197
net_amountNúmero: 197Não existe
installment_taxTexto: "0.0000"Não existe

Converta o texto para número antes de gravar e de comparar: no JavaScript, "197.0000" === 197 é false. O identifier chega igual ao da API, com o prefixo, e pode ser gravado como chegou:

const buildSaleRecord = (payload) => {
  const { transaction, subscription, source } = payload.data;

  return {
    id: transaction.id,
    identifier: transaction.identifier,
    status: transaction.status,
    type: transaction.type,
    cycle: transaction.cycle,
    total_amount: Number(transaction.total_amount),
    installment_tax: Number(transaction.installment_tax),
    net_amount: transaction.net_amount,
    subscription_id: subscription?.id ?? null,
    channel: source?.channel ?? null,
  };
};

Grave um registro por id. Se o mesmo id chegar de novo, atualize o registro em vez de criar outro. Veja Processar eventos sem duplicar.

Baixe as vendas do período

Chame GET /sales página por página, com o período em created_from e created_to.

Siga as regras de Percorra todas as páginas sem perder registros e pare quando page passar de meta.total_pages. A listagem ordena só por created_at, sem critério de desempate: vendas com o mesmo created_at podem mudar de posição entre uma página e outra, por isso guarde as vendas num mapa pelo id.

curl "https://api.pagpolar.com/v1/sales?page=1&per_page=100&created_from=2026-09-01T00:00:00-03:00&created_to=2026-09-15T23:59:59-03:00" \
  -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>"

Repita a chamada com page=2, page=3 e assim por diante, até o valor de meta.total_pages.

const apiUrl = 'https://api.pagpolar.com/v1';
const accessToken = '<SEU_TOKEN_DE_ACESSO>';
const createdFrom = '2026-09-01T00:00:00-03:00';
const createdTo = '2026-09-15T23:59:59-03:00';

const wait = (seconds) =>
  new Promise((resolve) => setTimeout(resolve, seconds * 1000));

const fetchPage = async (path, query) => {
  const response = await fetch(`${apiUrl}${path}?${new URLSearchParams(query)}`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  if (response.status === 429) {
    await wait(Number(response.headers.get('Retry-After') ?? 60));
    return fetchPage(path, query);
  }

  if (!response.ok) {
    throw new Error(`Erro ${response.status}: ${await response.text()}`);
  }

  return response.json();
};

const salesById = new Map();
let page = 1;
let totalPages = 1;

do {
  const body = await fetchPage('/sales', {
    page: String(page),
    per_page: '100',
    created_from: createdFrom,
    created_to: createdTo,
  });

  for (const sale of body.data) {
    salesById.set(sale.id, sale);
  }

  totalPages = body.meta.total_pages;
  page += 1;
} while (page <= totalPages);

console.log(`${salesById.size} vendas no período`);

Resposta resumida de uma venda. Todos os campos estão na referência de GET /sales.

{
  "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "identifier": "PPO9876543210",
  "external_reference": "PEDIDO-1234",
  "status": "PAID",
  "type": "BILLING",
  "payment_method": "PIX",
  "installments": 1,
  "total_amount": 197,
  "cycle": 1,
  "paid_at": "2026-09-15T14:35:00.000Z",
  "created_at": "2026-09-15T14:00:00.000Z"
}

A listagem traz todas as vendas da sua conta: as criadas pela API e as do checkout da PagPolar. A venda da API não diz o canal. Para separar, use o data.source.channel que chegou no webhook. Veja Formato do evento.

Separe as vendas pelo type

A listagem traz mais de um tipo de venda. Não some os tipos como se fossem a mesma coisa.

typeO que é
BILLINGA cobrança feita ao cliente numa venda sua.
TRANSFERUm repasse: a sua parte numa venda de outra conta, que divide o valor com você. Não gera webhook.
const allSales = [...salesById.values()];
const billingSales = allSales.filter((sale) => sale.type === 'BILLING');
const transferSales = allSales.filter((sale) => sale.type === 'TRANSFER');

Compare com o seu sistema

Junte as vendas da API com os registros do seu sistema pelo id. Para cada diferença:

SituaçãoO que fazer
Uma venda TRANSFER ou FEE está na API e não está no seu sistema.É esperado: essas vendas não geram webhook. Grave a venda com os dados da API.
Uma venda BILLING está na API e não está no seu sistema.Um evento não chegou. Grave a venda com os dados da API. Para ter o net_amount, reenvie o evento pelo painel. Veja Entregas e retentativas.
A venda está no seu sistema e não está na API.Confira a data de criação: o filtro usa created_at. Depois consulte GET /sales/{identifier} com o id. 404 quer dizer que a venda não existe na sua conta.
O status é diferente.Vale o da API, que é o atual. Atualize o seu registro.
total_amount é maior que o net_amount gravado.São os juros do parcelamento (installment_tax). Não é erro.
Uma venda do seu pedido não tem id gravado.Procure pela sua referência: GET /sales?external_reference=PEDIDO-1234.
curl "https://api.pagpolar.com/v1/sales?external_reference=PEDIDO-1234" \
  -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>"
const apiUrl = 'https://api.pagpolar.com/v1';
const accessToken = '<SEU_TOKEN_DE_ACESSO>';

const query = new URLSearchParams({ external_reference: 'PEDIDO-1234' });

const response = await fetch(`${apiUrl}/sales?${query}`, {
  headers: { Authorization: `Bearer ${accessToken}` },
});

if (!response.ok) {
  throw new Error(`Erro ${response.status}: ${await response.text()}`);
}

const body = await response.json();

console.log(body.data.map((sale) => ({ id: sale.id, status: sale.status })));

A mesma external_reference pode estar em mais de uma venda. O filtro traz todas, da mais nova para a mais antiga.

O período pega a criação, não a mudança de status

created_from e created_to filtram pela data de criação da venda. Uma venda de agosto estornada em setembro não aparece na busca de setembro. Para achar mudanças recentes em vendas antigas, confira os pedidos em GET /refunds, que filtra pela data do pedido, e os eventos recebidos no período.

Baixe as assinaturas

GET /subscriptions aceita created_from e created_to, como a listagem de vendas. Pagine do mesmo jeito do passo 3. O exemplo em Node.js reaproveita fetchPage, createdFrom e createdTo do passo 3.

curl "https://api.pagpolar.com/v1/subscriptions?page=1&per_page=100&created_from=2026-09-01T00:00:00-03:00&created_to=2026-09-15T23:59:59-03:00" \
  -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>"

Repita a chamada com page=2, page=3 e assim por diante, até o valor de meta.total_pages.

const subscriptionsById = new Map();
let subscriptionPage = 1;
let subscriptionTotalPages = 1;

do {
  const body = await fetchPage('/subscriptions', {
    page: String(subscriptionPage),
    per_page: '100',
    created_from: createdFrom,
    created_to: createdTo,
  });

  for (const subscription of body.data) {
    subscriptionsById.set(subscription.id, subscription);
  }

  subscriptionTotalPages = body.meta.total_pages;
  subscriptionPage += 1;
} while (subscriptionPage <= subscriptionTotalPages);

console.log(`${subscriptionsById.size} assinaturas criadas no período`);

Resposta resumida de uma assinatura. Todos os campos estão na referência de GET /subscriptions.

{
  "id": "5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f",
  "status": "ACTIVE",
  "payment_method": "CREDIT_CARD",
  "start_at": "2026-09-02T10:00:00.000Z",
  "next_billing_at": "2026-10-02T10:00:00.000Z",
  "next_billing_amount": 97,
  "total_amount": 97,
  "canceled_at": null,
  "created_at": "2026-09-02T10:00:00.000Z"
}

Cada cobrança da assinatura é uma venda própria, com id próprio e o cycle da cobrança. Para ligar a venda à assinatura, use o data.subscription.id do webhook: a venda da API não informa a assinatura.

Eventos de webhook deste fluxo

EventoO que traz para a conciliação
TRANSACTION_CREATEDO id da venda, os valores e o net_amount.
TRANSACTION_PAIDO paid_at e o status PAID.
TRANSACTION_REFUNDED, TRANSACTION_CANCELED, TRANSACTION_EXPIRED, TRANSACTION_CHARGEBACK_APPROVEDA mudança de status de uma venda que pode ser de outro período.
SUBSCRIPTION_CREATEDO id da assinatura.
SUBSCRIPTION_RENEWEDO aviso de um novo ciclo pago no cartão.

Quando algo dá errado

Além dos erros comuns a todas as rotas, este fluxo pode responder:

PassoSituaçãoRespostaComo resolver
Baixar as vendasper_page acima de 100.400 invalid_requestUse per_page de 1 a 100.
Baixar as vendascreated_from ou created_to fora do formato ISO 8601.400 invalid_requestEnvie data e hora completas: 2026-09-15T23:59:59-03:00.
Baixar as vendasstatus ou payment_method com valor que não existe.400 invalid_requestUse os valores da referência de GET /sales.
Baixar as vendasFiltro que a listagem não aceita, como email.400 invalid_requestVeja os filtros de cada rota em Paginação e filtros.
Baixar as vendasA mesma venda veio em duas páginas.200Guarde as vendas pelo id.
CompararA soma das vendas ficou maior que o esperado.—Separe BILLING de TRANSFER.
CompararOs valores do webhook não batem com os da API.—Converta os textos do webhook com Number() antes de comparar.
CompararGET /sales/{identifier} com um id que não é da sua conta.404 not_found com Venda não encontradaConfira o id gravado.
CompararGET /sales/{identifier} com uma external_reference no formato do código da venda (10 dígitos ou o prefixo seguido de 10 dígitos) devolveu outra venda.200 com a venda erradaA busca por código vem antes da busca por referência. Use a listagem com ?external_reference=, que só procura pela referência.
Comparar?external_reference= não trouxe as renovações de uma assinatura.200 só com a primeira vendaAs renovações não têm referência. Ligue as vendas pelo data.subscription.id do webhook.

Próximos passos