> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zouti.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Sincronização de pedidos

> Mantenha uma cópia local dos pedidos novos e alterados com uma consulta periódica.

Para manter um ERP, CRM ou BI em dia, consulte de tempos em tempos os pedidos que **mudaram**
desde a última leitura. Assim vêm os pedidos novos e também os antigos que mudaram: pagamento
aprovado, reembolso, chargeback, cancelamento, cliente corrigido.

A sincronização usa três peças: `sort=updated_at_asc`, o filtro `updated_gte` com uma **janela
de 5 minutos** e a gravação por `id`.

## Como funciona

<Steps>
  <Step title="Primeira carga">
    Chame `GET /v1/orders?sort=updated_at_asc`, sem `updated_gte`, e percorra todas as
    [páginas](/concepts/pagination). Grave cada pedido pelo `id`. Guarde o maior `updated_at`
    lido: essa é a **marca**.
  </Step>

  <Step title="Rodadas seguintes">
    Chame `GET /v1/orders?sort=updated_at_asc&updated_gte=<marca − 5 min>` e percorra todas as
    páginas.
  </Step>

  <Step title="Grave por id, substituindo">
    Para cada pedido lido, substitua a versão que você já tem pelo mesmo `id`, ou insira se for
    novo.
  </Step>

  <Step title="Avance a marca">
    Depois de ler a última página, troque a marca pelo maior `updated_at` lido. Se a rodada falhar
    no meio, não avance a marca: a próxima rodada relê do mesmo ponto.
  </Step>
</Steps>

Rode o processo periodicamente, por exemplo a cada 5 minutos. Uma rodada sem mudanças costuma
custar uma única requisição.

## Exemplo com horários

| Rodada | Chamada | O que volta | Marca depois |
| - | - | - | - |
| 1ª (carga) | `sort=updated_at_asc` | Todos os pedidos, do menos para o mais recentemente atualizado. Os dois últimos: `ord_1q3s5u7w9y0a2c4e6g8i0k` (`2026-10-13T18:43:52.009Z`) e `ord_8k2m4q6s1v3x5z7b9d0f2h` (`2026-10-14T13:05:25.117Z`). | `2026-10-14T13:05:25.117Z` |
| 2ª | `sort=updated_at_asc&updated_gte=2026-10-14T13:00:25.117Z` | `ord_8k2m4q6s1v3x5z7b9d0f2h` de novo (está na janela) e todo pedido com `updated_at` a partir de 13:00:25.117. | Maior `updated_at` lido nesta rodada |

Na 2ª rodada, `ord_8k2m4q6s1v3x5z7b9d0f2h` volta mesmo sem ter mudado. Gravado por `id`, ele só
substitui a cópia igual que você já tinha.

## Por que a janela de 5 minutos

* **A mudança chega à API alguns segundos depois de acontecer.** Um pedido atualizado pouco antes
  da marca pode ficar visível só depois da sua leitura. Sem a janela, essa mudança nunca seria
  lida.
* **`updated_gte` é inclusivo.** O pedido da marca sempre volta na rodada seguinte. Por isso a
  gravação por `id` é obrigatória, mesmo sem a janela.

O custo da janela é pequeno: reler os pedidos que mudaram nos 5 minutos antes da marca.

Se você também recebe webhooks, um webhook pode chegar antes de o `GET` refletir a mudança:
repita a leitura em alguns segundos.

## Por que gravar por id

O mesmo pedido pode vir mais de uma vez: na janela de sobreposição e quando ele muda durante a
leitura (com `sort=updated_at_asc`, um pedido atualizado no meio da paginação volta numa página
seguinte, já com os dados novos). A ordem crescente de `updated_at` garante que a **última versão
lida é a mais nova**. Substitua o pedido inteiro; não some nem acumule versões.

## O que conta como atualização

`updated_at` avança quando **algum campo que a API devolve** muda: status, valores, pagamento
(inclusive estorno, recusa e parcelas), cliente, endereço, itens, oferta, cupom, UTM, rastreamento
(`tracking`), navegador (`navigator`) ou metadados. Reprocessamentos internos da Zouti que não
mudam nada do que a API devolve **não** avançam o `updated_at`: um pedido não volta à
sincronização sem motivo.

* **`updated_at` é o instante em que a mudança chegou à API**, não o horário do evento no
  adquirente. Para saber quando o pedido foi pago ou estornado, use `paid_at` e
  `payment.refunded_at`.
* **Pedidos anteriores a esta funcionalidade** receberam como `updated_at` inicial uma estimativa
  da última mudança conhecida.
* **`updated_at` `null`** indica um pedido que ainda não passou pelo preenchimento inicial. Ele
  entra na primeira carga (no fim da lista), mas nunca numa consulta com `updated_gte`.
* **Um pedido que deixa de aparecer na API não é sinalizado** pelo `updated_at`.

<RequestExample>
  ```bash cURL theme={null}
  # Requer jq. Grava cada versão lida em orders.ndjson (carregue no seu banco com upsert por id:
  # a última linha de cada id é a mais nova) e guarda a marca em zouti-mark.txt.
  mark=$(cat zouti-mark.txt 2>/dev/null || true)
  since=""
  if [ -n "$mark" ]; then
    # marca − 5 min; o jq faz a conta igual no Linux e no macOS
    since=$(jq -rn --arg m "$mark" '$m | sub("\\.[0-9]+Z$"; "Z") | fromdate - 300 | todate')
  fi
  cursor=""
  while :; do
    page=$(curl -sS -G "https://api.zouti.com.br/v1/orders" \
      -H "Authorization: Bearer $ZOUTI_API_KEY" \
      --data-urlencode "sort=updated_at_asc" \
      --data-urlencode "limit=100" \
      ${since:+--data-urlencode "updated_gte=$since"} \
      ${cursor:+--data-urlencode "cursor=$cursor"})
    echo "$page" | jq -e '.data' > /dev/null || { echo "$page" >&2; exit 1; }
    echo "$page" | jq -c '.data[]' >> orders.ndjson
    page_mark=$(echo "$page" | jq -r '[.data[].updated_at | select(. != null)] | max // empty')
    if [ -n "$page_mark" ] && [[ "$page_mark" > "$mark" ]]; then mark=$page_mark; fi
    [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
    cursor=$(echo "$page" | jq -r '.next_cursor')
  done
  if [ -n "$mark" ]; then echo "$mark" > zouti-mark.txt; fi
  ```

  ```javascript Node.js theme={null}
  // Node.js 18+ (fetch nativo). Sincroniza os pedidos num arquivo local, zouti-orders.json.
  // Rode de tempos em tempos: a 1ª execução faz a carga completa; as seguintes leem só o que
  // mudou. Troque o arquivo pelo seu banco de dados.
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';

  const API_KEY = process.env.ZOUTI_API_KEY;
  const BASE_URL = 'https://api.zouti.com.br';
  const STATE_FILE = 'zouti-orders.json';
  const OVERLAP_MS = 5 * 60 * 1000;

  // Estado: a marca (maior updated_at já lido) e a cópia dos pedidos por id.
  const state = existsSync(STATE_FILE)
    ? JSON.parse(readFileSync(STATE_FILE, 'utf8'))
    : { mark: null, orders: {} };

  async function listPage(query) {
    const response = await fetch(`${BASE_URL}/v1/orders?${query}`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    const body = await response.json();
    if (!response.ok) {
      throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`);
    }
    return body;
  }

  const query = new URLSearchParams({ sort: 'updated_at_asc', limit: '100' });
  if (state.mark) {
    query.set('updated_gte', new Date(Date.parse(state.mark) - OVERLAP_MS).toISOString());
  }

  let mark = state.mark;
  let read = 0;
  let cursor = null;
  do {
    if (cursor) query.set('cursor', cursor);
    const page = await listPage(query);
    for (const order of page.data) {
      state.orders[order.id] = order; // upsert: a versão lida substitui a anterior
      // Datas em UTC no mesmo formato: comparar como texto dá a ordem cronológica.
      if (order.updated_at && (!mark || order.updated_at > mark)) mark = order.updated_at;
      read++;
    }
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);

  // A marca só avança depois da última página.
  state.mark = mark;
  writeFileSync(STATE_FILE, JSON.stringify(state));
  console.log(`${read} pedidos lidos; ${Object.keys(state.orders).length} na cópia local; marca ${mark}`);
  ```

  ```python Python theme={null}
  # Python 3 com requests. Sincroniza os pedidos num arquivo local, zouti-orders.json.
  # Rode de tempos em tempos: a 1ª execução faz a carga completa; as seguintes leem só o que
  # mudou. Troque o arquivo pelo seu banco de dados.
  import json
  import os
  from datetime import datetime, timedelta

  import requests

  API_KEY = os.environ["ZOUTI_API_KEY"]
  BASE_URL = "https://api.zouti.com.br"
  STATE_FILE = "zouti-orders.json"
  OVERLAP = timedelta(minutes=5)

  # Estado: a marca (maior updated_at já lido) e a cópia dos pedidos por id.
  if os.path.exists(STATE_FILE):
      with open(STATE_FILE, encoding="utf-8") as file:
          state = json.load(file)
  else:
      state = {"mark": None, "orders": {}}


  def list_page(params):
      response = requests.get(
          f"{BASE_URL}/v1/orders",
          params=params,
          headers={"Authorization": f"Bearer {API_KEY}"},
          timeout=30,
      )
      body = response.json()
      if not response.ok:
          error = body["error"]
          raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})")
      return body


  params = {"sort": "updated_at_asc", "limit": 100}
  if state["mark"]:
      mark_time = datetime.fromisoformat(state["mark"].replace("Z", "+00:00"))
      since = mark_time - OVERLAP
      params["updated_gte"] = since.isoformat(timespec="milliseconds").replace("+00:00", "Z")

  mark = state["mark"]
  read = 0
  while True:
      page = list_page(params)
      for order in page["data"]:
          state["orders"][order["id"]] = order  # upsert: a versão lida substitui a anterior
          # Datas em UTC no mesmo formato: comparar como texto dá a ordem cronológica.
          if order["updated_at"] and (mark is None or order["updated_at"] > mark):
              mark = order["updated_at"]
          read += 1
      if not page["has_more"]:
          break
      params["cursor"] = page["next_cursor"]

  # A marca só avança depois da última página.
  state["mark"] = mark
  with open(STATE_FILE, "w", encoding="utf-8") as file:
      json.dump(state, file)
  print(f"{read} pedidos lidos; {len(state['orders'])} na cópia local; marca {mark}")
  ```

  ```php PHP theme={null}
  <?php
  // PHP 7.4+ com a extensão curl. Sincroniza os pedidos num arquivo local, zouti-orders.json.
  // Rode de tempos em tempos: a 1ª execução faz a carga completa; as seguintes leem só o que
  // mudou. Troque o arquivo pelo seu banco de dados.
  const BASE_URL = 'https://api.zouti.com.br';
  const STATE_FILE = 'zouti-orders.json';
  const OVERLAP_SECONDS = 5 * 60;

  // Estado: a marca (maior updated_at já lido) e a cópia dos pedidos por id.
  $state = is_file(STATE_FILE)
      ? json_decode(file_get_contents(STATE_FILE), true)
      : ['mark' => null, 'orders' => []];

  function list_page(array $query): array
  {
      $ch = curl_init(BASE_URL . '/v1/orders?' . http_build_query($query));
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_TIMEOUT => 30,
          CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('ZOUTI_API_KEY')],
      ]);
      $raw = curl_exec($ch);
      if ($raw === false) {
          throw new RuntimeException(curl_error($ch));
      }
      $body = json_decode($raw, true);
      if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) {
          $error = $body['error'];
          throw new RuntimeException("{$error['code']}: {$error['message']} ({$error['request_id']})");
      }
      return $body;
  }

  $query = ['sort' => 'updated_at_asc', 'limit' => 100];
  if ($state['mark'] !== null) {
      $since = (new DateTimeImmutable($state['mark']))->modify('-' . OVERLAP_SECONDS . ' seconds');
      $query['updated_gte'] = $since->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s.v\Z');
  }

  $mark = $state['mark'];
  $read = 0;
  do {
      $page = list_page($query);
      foreach ($page['data'] as $order) {
          $state['orders'][$order['id']] = $order; // upsert: a versão lida substitui a anterior
          // Datas em UTC no mesmo formato: comparar como texto dá a ordem cronológica.
          if ($order['updated_at'] !== null && ($mark === null || strcmp($order['updated_at'], $mark) > 0)) {
              $mark = $order['updated_at'];
          }
          $read++;
      }
      $query['cursor'] = $page['next_cursor'];
  } while ($page['has_more']);

  // A marca só avança depois da última página.
  $state['mark'] = $mark;
  file_put_contents(STATE_FILE, json_encode($state));
  printf("%d pedidos lidos; %d na cópia local; marca %s\n", $read, count($state['orders']), $mark ?? '-');
  ```
</RequestExample>

<ResponseExample>
  ```json 200 (última página) theme={null}
  {
    "object": "list",
    "data": [
      {
        "object": "order",
        "id": "ord_1q3s5u7w9y0a2c4e6g8i0k",
        "role": "COPRODUCER",
        "status": "PAID",
        "currency": "BRL",
        "amount_total": 9700,
        "amount_subtotal": 9700,
        "amount_in_brl": 9700,
        "created_at": "2026-10-13T18:42:10.036Z",
        "paid_at": "2026-10-13T18:43:51.274Z",
        "canceled_at": null,
        "updated_at": "2026-10-13T18:43:52.009Z",
        "customer": {
          "name": "Maria Oliveira",
          "email": "maria.oliveira@example.com",
          "phone": "+5511999998888",
          "document": "12345678909",
          "document_type": "CPF"
        },
        "shipping_address": null,
        "payment": {
          "method": "PIX",
          "status": "PAID",
          "installments": 1,
          "amount": 9700,
          "interest_amount": 0,
          "discount_amount": 0,
          "fee": 485,
          "amount_refunded": 0,
          "refunded_at": null,
          "decline_code": null,
          "decline_message": null,
          "card": null
        },
        "items": [
          {
            "product_id": "prod_9x1z3b5d7f9h1k3m5p7r9t",
            "name": "Mentoria em Grupo",
            "quantity": 1,
            "unit_price": 9700,
            "sub_total": 9700,
            "currency": "BRL",
            "offer_id": "prod_offer_4w6y8a0c2e4g6j8l0n2q4s",
            "plan_id": null,
            "order_bump_id": null
          }
        ],
        "offer": {
          "id": "prod_offer_4w6y8a0c2e4g6j8l0n2q4s",
          "name": "Mentoria em Grupo"
        },
        "promocode": null,
        "utm": null,
        "tracking": null,
        "navigator": null,
        "metadata": {
          "order_parent_id": "ord_7h9k1m3p5r7t9v1x3z5b7d"
        }
      },
      {
        "object": "order",
        "id": "ord_8k2m4q6s1v3x5z7b9d0f2h",
        "role": "OWNER",
        "status": "PAID",
        "currency": "BRL",
        "amount_total": 27730,
        "amount_subtotal": 29700,
        "amount_in_brl": 27730,
        "created_at": "2026-10-14T13:05:22.418Z",
        "paid_at": "2026-10-14T13:05:24.902Z",
        "canceled_at": null,
        "updated_at": "2026-10-14T13:05:25.117Z",
        "customer": {
          "name": "Maria Oliveira",
          "email": "maria.oliveira@example.com",
          "phone": "+5511999998888",
          "document": "12345678909",
          "document_type": "CPF"
        },
        "shipping_address": {
          "line1": "Rua das Flores",
          "line2": "123",
          "line3": "Apto 45",
          "neighborhood": "Centro",
          "city": "São Paulo",
          "state": "SP",
          "country": "BR",
          "postal_code": "01001000",
          "full_name": "Maria Oliveira"
        },
        "payment": {
          "method": "CREDIT_CARD",
          "status": "PAID",
          "installments": 1,
          "amount": 27730,
          "interest_amount": 0,
          "discount_amount": 1970,
          "fee": 1386,
          "amount_refunded": 0,
          "refunded_at": null,
          "decline_code": null,
          "decline_message": null,
          "card": {
            "brand": "MASTERCARD",
            "last4": "4242"
          }
        },
        "items": [
          {
            "product_id": "prod_3n7p1r5t9w2y4a6c8e0g1j",
            "name": "Curso Completo de Fotografia",
            "quantity": 1,
            "unit_price": 19700,
            "sub_total": 19700,
            "currency": "BRL",
            "offer_id": "prod_offer_5d1f3h7k9m2p4r6t8v0x2z",
            "plan_id": null,
            "order_bump_id": null
          },
          {
            "product_id": "prod_6b8d0f2h4k6m8p0r2t4v6x",
            "name": "Pacote de Presets",
            "quantity": 1,
            "unit_price": 10000,
            "sub_total": 10000,
            "currency": "BRL",
            "offer_id": null,
            "plan_id": null,
            "order_bump_id": "ob_2c4e6g8j0l2n4q6s8u0w2y"
          }
        ],
        "offer": {
          "id": "prod_offer_5d1f3h7k9m2p4r6t8v0x2z",
          "name": "Oferta de lançamento"
        },
        "promocode": {
          "code": "BEMVINDO10",
          "description": "10% de desconto no curso",
          "amount": -1970
        },
        "utm": {
          "utm_source": "instagram",
          "utm_medium": "social",
          "utm_campaign": "lancamento-outubro",
          "utm_term": null,
          "utm_content": "stories"
        },
        "tracking": {
          "sck": "parceiro-123",
          "src": "bio"
        },
        "navigator": {
          "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 350.0.0",
          "referer": "https://www.instagram.com/",
          "device": "Mobile",
          "browser": "Instagram",
          "origin": "instagram.com"
        },
        "metadata": {}
      }
    ],
    "has_more": false,
    "next_cursor": null
  }
  ```
</ResponseExample>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.