# Conciliar vendas e assinaturas

URL: https://staging.pagpolar.com/docs/guias/jornadas/conciliar-vendas

> 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](/docs/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.

```mermaid
sequenceDiagram
  autonumber
  participant A as API PagPolar
  participant W as Seu servidor de webhook
  participant S as Seu sistema
  A-)W: TRANSACTION_CREATED e TRANSACTION_PAID com transaction.id e net_amount
  W->>S: grava a venda pelo id
  loop cada página até total_pages
    S->>A: GET /sales com created_from, created_to e per_page=100
    A-->>S: 200 com data e meta
  end
  loop cada página até total_pages
    S->>A: GET /subscriptions com created_from, created_to e per_page=100
    A-->>S: 200 com data e meta
  end
  S->>S: junta pelo id e marca o que falta ou está diferente
```

## Antes de começar

* Um token de acesso. Veja [Autenticação](/docs/guias/fundamentos/autenticacao#obter-o-token).
* Um webhook que recebe os eventos de venda. Veja [Configurar o webhook](/docs/webhooks/configurar).
* Leia [Percorra todas as páginas sem perder registros](/docs/guias/fundamentos/paginacao-e-filtros#percorrer). 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

1. **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](/docs/guias/fundamentos/valores-datas-e-identificadores#como-a-venda-e-encontrada).

       A resposta `201` de [`POST /payments/pix`](/docs/referencia/vendas/create-pix-payment), [`POST /payments/boleto`](/docs/referencia/vendas/create-boleto-payment) e [`POST /payments/credit-card`](/docs/referencia/vendas/create-credit-card-payment) traz `data.transactions`: a lista de `id` das vendas criadas. Grave esses `id` junto do seu pedido.

       Resposta resumida:

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

       Na assinatura, a resposta `201` de [`POST /plans/offer/{id}/subscribe`](/docs/referencia/assinaturas/create-subscription) 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.

2. **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](/docs/guias/fundamentos/valores-datas-e-identificadores#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:

       ```js
       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](/docs/webhooks/processar-sem-duplicar).

3. **Baixe as vendas do período**

   Chame [`GET /sales`](/docs/referencia/vendas/list-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](/docs/guias/fundamentos/paginacao-e-filtros#percorrer) 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

   ```bash
           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`.

   #### Node.js

   ```js
           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`](/docs/referencia/vendas/list-sales).

       ```json
       {
         "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](/docs/webhooks/formato-do-evento#source).

4. **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. |

       ```js
       const allSales = [...salesById.values()];
       const billingSales = allSales.filter((sale) => sale.type === 'BILLING');
       const transferSales = allSales.filter((sale) => sale.type === 'TRANSFER');
       ```

5. **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](/docs/webhooks/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}`](/docs/referencia/vendas/get-sale) 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

   ```bash
           curl "https://api.pagpolar.com/v1/sales?external_reference=PEDIDO-1234" \
             -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>"
           ```

   #### Node.js

   ```js
           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`](/docs/referencia/reembolsos/list-refunds), que filtra pela data do pedido, e os eventos recebidos no período.

6. **Baixe as assinaturas**

   [`GET /subscriptions`](/docs/referencia/assinaturas/list-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

   ```bash
           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`.

   #### Node.js

   ```js
           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`](/docs/referencia/assinaturas/list-subscriptions).

       ```json
       {
         "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`](/docs/webhooks/eventos/transaction-created)                                                                                                                                                                                                                                            | O `id` da venda, os valores e o `net_amount`.                   |
| [`TRANSACTION_PAID`](/docs/webhooks/eventos/transaction-paid)                                                                                                                                                                                                                                                  | O `paid_at` e o status `PAID`.                                  |
| [`TRANSACTION_REFUNDED`](/docs/webhooks/eventos/transaction-refunded), [`TRANSACTION_CANCELED`](/docs/webhooks/eventos/transaction-canceled), [`TRANSACTION_EXPIRED`](/docs/webhooks/eventos/transaction-expired), [`TRANSACTION_CHARGEBACK_APPROVED`](/docs/webhooks/eventos/transaction-chargeback-approved) | A mudança de status de uma venda que pode ser de outro período. |
| [`SUBSCRIPTION_CREATED`](/docs/webhooks/eventos/subscription-created)                                                                                                                                                                                                                                          | O `id` da assinatura.                                           |
| [`SUBSCRIPTION_RENEWED`](/docs/webhooks/eventos/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](/docs/guias/fundamentos/erros#erros-comuns), 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`](/docs/referencia/vendas/list-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](/docs/guias/fundamentos/paginacao-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.                                        |

## Próximos passos

- [Acompanhar reembolsos e chargebacks](/docs/guias/jornadas/acompanhar-reembolsos-e-chargebacks) — Trate as vendas que mudam de status depois do período.
- [Formato do evento](/docs/webhooks/formato-do-evento) — Veja todos os campos de valor que chegam no webhook.
- [Limites de requisição](/docs/guias/fundamentos/limites-de-requisicao) — Pagine sem estourar o limite por minuto.
