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
- Um token de acesso. Veja Autenticação.
- Um webhook que recebe os eventos de venda. Veja Configurar o webhook.
- Leia Percorra todas as páginas sem perder registros. Este guia usa as mesmas regras.
Quais identificadores usar
| Campo | Onde aparece | Use para |
|---|---|---|
id | Venda na API (data[].id) e no webhook (data.transaction.id) | Chave principal da conciliação. Não muda e não se repete. |
identifier | Venda na API e no webhook, com o mesmo valor | Có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_reference | Só na venda da API | Ligar a venda ao código do seu pedido. Não chega no webhook. |
cycle | Venda na API e no webhook | Posição da cobrança na assinatura: 1, 2, 3... |
data.subscription.id | Só no webhook de venda | Ligar 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):
| Campo | No webhook | Na API |
|---|---|---|
total_amount | Texto: "197.0000" | Número: 197 |
net_amount | Número: 197 | Não existe |
installment_tax | Texto: "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.
type | O que é |
|---|---|
BILLING | A cobrança feita ao cliente numa venda sua. |
TRANSFER | Um 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ção | O 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
| Evento | O que traz para a conciliação |
|---|---|
TRANSACTION_CREATED | O id da venda, os valores e o net_amount. |
TRANSACTION_PAID | O paid_at e o status PAID. |
TRANSACTION_REFUNDED, TRANSACTION_CANCELED, TRANSACTION_EXPIRED, TRANSACTION_CHARGEBACK_APPROVED | A mudança de status de uma venda que pode ser de outro período. |
SUBSCRIPTION_CREATED | O id da assinatura. |
SUBSCRIPTION_RENEWED | O 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:
| Passo | Situação | Resposta | Como resolver |
|---|---|---|---|
| Baixar as vendas | per_page acima de 100. | 400 invalid_request | Use per_page de 1 a 100. |
| Baixar as vendas | created_from ou created_to fora do formato ISO 8601. | 400 invalid_request | Envie data e hora completas: 2026-09-15T23:59:59-03:00. |
| Baixar as vendas | status ou payment_method com valor que não existe. | 400 invalid_request | Use os valores da referência de GET /sales. |
| Baixar as vendas | Filtro que a listagem não aceita, como email. | 400 invalid_request | Veja os filtros de cada rota em Paginação e filtros. |
| Baixar as vendas | A mesma venda veio em duas páginas. | 200 | Guarde as vendas pelo id. |
| Comparar | A soma das vendas ficou maior que o esperado. | — | Separe BILLING de TRANSFER. |
| Comparar | Os valores do webhook não batem com os da API. | — | Converta os textos do webhook com Number() antes de comparar. |
| Comparar | GET /sales/{identifier} com um id que não é da sua conta. | 404 not_found com Venda não encontrada | Confira o id gravado. |
| Comparar | GET /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 errada | A 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 venda | As renovações não têm referência. Ligue as vendas pelo data.subscription.id do webhook. |