> ## Documentation Index
> Fetch the complete documentation index at: https://dev.tryno.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Receitas de integração

> Fluxos completos ponta a ponta: webhook entra, você enriquece com a leitura de /v1 e empurra para planilha, Zapier ou n8n.

## O padrão único

Todas as receitas desta página são a mesma forma, porque a Platform API v1 é
**somente leitura**:

<Steps>
  <Step title="Evento entra">
    A Tribo gera um `member.joined`, `order.paid`, `post.created`… e a Tryno
    **empurra** a entrega assinada para o seu endpoint.
  </Step>

  <Step title="Você enriquece (opcional)">
    O payload traz identificadores, não o registro completo. Se precisar do nome
    do membro ou dos detalhes do pedido, busque em `/api/v1` com a sua chave.
  </Step>

  <Step title="Você empurra para o seu destino">
    Planilha, CRM, Slack, banco de dados, ferramenta de e-mail — o que for. Esse
    passo acontece **fora** da Tryno.
  </Step>
</Steps>

<Warning>
  **Não existe escrita de volta na Tryno.** A v1 não tem `POST`, `PATCH` nem
  `DELETE`: nenhuma automação pode criar posts, adicionar membros, dar pontos,
  aprovar vitórias ou alterar pedidos pela API pública. Se um tutorial de
  automação promete "ação na Tryno", ele está descrevendo algo que a v1 não faz.
  O fluxo é sempre **Tryno → fora**.
</Warning>

<Note>
  Não há **app oficial** da Tryno no Zapier, no Make ou no n8n. Todas as receitas
  abaixo usam blocos genéricos: *Webhook* (para receber) e *HTTP Request* (para
  ler `/v1`). Isso funciona bem, mas significa que você configura os cabeçalhos
  na mão — inclusive a verificação de assinatura, que essas ferramentas não fazem
  por você.
</Note>

## Antes de qualquer receita

<Steps>
  <Step title="Crie a chave com o mínimo de escopos">
    Uma chave **por automação**. Ver [Criar API key](/guides/api-keys).
  </Step>

  <Step title="Registre um endpoint de webhook por destino">
    Um endpoint por ferramenta, com só os eventos daquele fluxo. Ver
    [Configurar webhooks](/guides/webhooks).
  </Step>

  <Step title="Dispare o `ping` e confirme a assinatura">
    Nunca ligue uma automação a um receptor que ainda não rejeitou uma
    assinatura inválida em teste.
  </Step>
</Steps>

<Warning>
  **A verificação de assinatura continua sendo sua responsabilidade**, mesmo
  dentro de uma ferramenta no-code. Uma URL de webhook do Zapier/Make/n8n é
  pública: sem verificar `x-tryno-signature`, qualquer pessoa que descubra a URL
  consegue injetar "vendas" e "membros" falsos na sua planilha. Cada receita
  abaixo mostra onde encaixar a verificação.
</Warning>

***

## Receita 1 — Toda venda numa planilha

**Objetivo:** cada `order.paid` vira uma linha em Google Sheets, com o nome de
quem comprou (que o payload não traz).

**Você precisa de:** escopos `orders:read` e `members:read`; um endpoint de
webhook assinado em `order.paid`.

<Steps>
  <Step title="Registre o endpoint">
    ```bash theme={"system"}
    curl -X POST https://tryno.io/api/communities/sua-tribo/webhooks \
      -H "Content-Type: application/json" \
      --cookie "__session=<sua sessão>" \
      -d '{
            "url": "https://hooks.zapier.com/hooks/catch/…",
            "events": ["order.paid"],
            "description": "Planilha de vendas"
          }'
    ```

    Guarde o `whsec_…` da resposta.
  </Step>

  <Step title="Verifique a assinatura no primeiro passo do fluxo">
    No Zapier use um passo **Code by Zapier**; no n8n, um nó **Code**; no Make,
    um módulo de *custom function*. O código de verificação (Node e Python) está
    em [Configurar webhooks](/guides/webhooks#verificar-a-assinatura).

    <Warning>
      Ferramentas no-code costumam entregar o corpo **já convertido em objeto**.
      Se você não conseguir o corpo bruto exatamente como chegou, a assinatura
      **não vai bater** — e a alternativa correta não é pular a verificação, é
      colocar um pequeno receptor próprio (Cloudflare Worker, Vercel Function,
      Lambda) entre a Tryno e a ferramenta: ele verifica a assinatura sobre os
      bytes originais e só então repassa.
    </Warning>
  </Step>

  <Step title="Deduplique por x-tryno-delivery">
    Antes de escrever a linha, cheque se aquele `id` já está na planilha (uma
    coluna `delivery_id` com busca). Reentregas acontecem: sem isso, você conta a
    mesma venda duas vezes.
  </Step>

  <Step title="Enriqueça com uma leitura de /v1">
    O `data` do `order.paid` traz `orderId`, `buyerId`, `offerId`, `amountCents`
    e `currency` — mas não o nome do comprador. Puxe a lista de pedidos e case
    pelo `id`:

    ```bash theme={"system"}
    curl "https://tryno.io/api/v1/orders?pageSize=100" \
      -H "Authorization: Bearer $TRYNO_API_KEY"
    ```

    Cada item traz `id`, `offerId`, `buyerUsername`, `amountCents`,
    `amountCurrency`, `status` e `createdAt`. Encontre o item cujo `id` é igual
    ao `orderId` do evento e use o `buyerUsername`.

    <Note>
      Não existe `GET /v1/orders/{id}`. A busca por um pedido específico é feita
      na lista, que vem ordenada por `createdAt` desc — o pedido recém-pago está
      quase sempre na primeira página.
    </Note>
  </Step>

  <Step title="Escreva a linha">
    Colunas úteis: `delivery_id`, `created` (do corpo do evento), `orderId`,
    `buyerUsername`, `amountCents / 100`, `currency`, `status`.

    <Warning>
      `amountCents` é **inteiro em centavos**. Divida por 100 apenas na
      formatação. Se a célula da planilha estiver como número, escreva o valor
      já dividido; se estiver como texto, cuidado com a vírgula decimal do
      pt-BR.
    </Warning>
  </Step>
</Steps>

***

## Receita 2 — Novo membro no CRM

**Objetivo:** cada `member.joined` cria ou atualiza um contato no seu CRM, com
nome de exibição e avatar.

**Você precisa de:** escopo `members:read`; endpoint assinado em `member.joined`.

<Steps>
  <Step title="Receba e verifique">
    Igual à receita anterior: assinatura primeiro, depois dedupe por
    `x-tryno-delivery`.
  </Step>

  <Step title="Descubra o username">
    <Warning>
      O `data` do `member.joined` traz `userId` (e `handle` da tribo, e às vezes
      `via: "invite"`) — **não** traz `username`. E `/v1/members` é indexada por
      `username`, não por `userId`: não existe busca por `userId`.

      O caminho honesto é: chame `GET /v1/members?pageSize=100&page=1` logo após
      o evento. Como a lista vem ordenada por `joinedAt` **desc**, quem acabou de
      entrar está no topo. Case por `joinedAt` próximo ao `created` do evento.
    </Warning>

    ```bash theme={"system"}
    curl "https://tryno.io/api/v1/members?page=1&pageSize=100" \
      -H "Authorization: Bearer $TRYNO_API_KEY"
    ```
  </Step>

  <Step title="Busque o perfil completo, se precisar da bio">
    ```bash theme={"system"}
    curl "https://tryno.io/api/v1/members/<username>" \
      -H "Authorization: Bearer $TRYNO_API_KEY"
    ```

    O item único traz `username`, `displayName`, `avatarUrl`, `bio`, `role`,
    `points`, `level` e `joinedAt`. A **lista** traz o mesmo conjunto **sem** a
    `bio`.
  </Step>

  <Step title="Faça UPSERT no CRM">
    Chaveie pelo `username`, não pela posição na lista. Assim uma reentrega ou
    uma leitura duplicada não cria contato repetido.
  </Step>
</Steps>

<Warning>
  **A API não expõe e-mail de membro.** Nem a lista, nem o item único, nem o
  payload do webhook trazem endereço de e-mail ou telefone. Se a sua automação
  precisa disso, ela precisa coletar o contato por outro caminho (formulário,
  checkout próprio) — não há como obtê-lo pela Platform API v1.
</Warning>

***

## Receita 3 — Alerta de venda no Slack

O caso mais simples, porque não exige leitura nenhuma: tudo o que você quer já
está no payload.

<Steps>
  <Step title="Endpoint assinado em order.paid">
    Aponte-o para um receptor seu (não direto para o Slack — você precisa
    verificar a assinatura e o Slack não faz isso).
  </Step>

  <Step title="Verifique, deduplique, formate">
    ```js theme={"system"}
    // Depois de verificar a assinatura e passar pelo dedupe:
    const { amountCents, currency, orderId } = evt.data;
    const valor = (amountCents / 100).toLocaleString("pt-BR", {
      style: "currency",
      currency: currency ?? "BRL",
    });

    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ text: `Venda confirmada: ${valor} (pedido ${orderId})` }),
    });
    ```
  </Step>

  <Step title="Responda 204 à Tryno antes de chamar o Slack, se o Slack estiver lento">
    O orçamento é de 10 segundos para a resposta. Uma chamada externa dentro do
    handler é a causa mais comum de timeout — e timeout vira reentrega.
  </Step>
</Steps>

***

## Receita 4 — Espelho diário da tribo num banco

Quando o objetivo é análise, e não reação, uma varredura periódica é legítima.

<Steps>
  <Step title="Rode uma vez por dia, não em laço contínuo">
    Uma varredura completa de membros com `pageSize=100` custa 1 requisição a
    cada 100 membros. O laço pronto está em
    [Paginação](/guides/pagination#varredura-completa).
  </Step>

  <Step title="UPSERT, nunca INSERT">
    A paginação é por offset e a lista muda durante a varredura: itens na
    fronteira entre páginas podem repetir. `UPSERT` por `username` (membros) ou
    `id` (posts, eventos, pedidos) torna isso inofensivo.
  </Step>

  <Step title="Trate o 429 com backoff">
    Use o cliente de [Erros e limites](/guides/errors#retentativa-com-backoff-exponencial-e-jitter).
    120 req/min por chave dá folga para \~12.000 registros por minuto.
  </Step>

  <Step title="Combine com webhooks para o intervalo">
    A varredura diária dá a base consistente; os webhooks cobrem o que aconteceu
    entre duas varreduras sem você precisar rodar de hora em hora.
  </Step>
</Steps>

***

## O receptor mínimo, pronto para colar

Este é o "adaptador" recomendado quando o destino é uma ferramenta no-code: ele
fica entre a Tryno e o Zapier/n8n, verifica a assinatura sobre o corpo bruto,
deduplica e só então repassa.

```js Cloudflare Worker / Vercel Edge theme={"system"}
const TOLERANCE_SECONDS = 300;

async function hmacHex(secret, message) {
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
  return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
}

function timingSafeEqual(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

export default {
  async fetch(request, env, ctx) {
    if (request.method !== "POST") return new Response("method not allowed", { status: 405 });

    // Corpo BRUTO — leia uma vez, use para o HMAC e só depois faça parse.
    const raw = await request.text();

    const header = request.headers.get("x-tryno-signature") ?? "";
    const parts = Object.fromEntries(
      header.split(",").map((kv) => {
        const i = kv.indexOf("=");
        return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
      }),
    );
    const t = Number(parts.t);
    if (!Number.isFinite(t) || !parts.v1) return new Response("bad signature", { status: 401 });
    if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) {
      return new Response("stale", { status: 401 });
    }

    const expected = await hmacHex(env.TRYNO_WEBHOOK_SECRET, `${t}.${raw}`);
    if (!timingSafeEqual(expected, parts.v1)) {
      return new Response("bad signature", { status: 401 });
    }

    // Dedupe pelo id estável da entrega (KV com TTL de 7 dias basta).
    const deliveryId = request.headers.get("x-tryno-delivery") ?? "";
    if (await env.SEEN.get(deliveryId)) return new Response(null, { status: 204 });
    await env.SEEN.put(deliveryId, "1", { expirationTtl: 604_800 });

    // Repasse para a ferramenta no-code fora do caminho da resposta:
    // responder rápido evita o timeout de 10s e a reentrega.
    ctx.waitUntil(
      fetch(env.DOWNSTREAM_URL, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: raw,
      }),
    );

    return new Response(null, { status: 204 });
  },
};
```

<Note>
  Repassar o **corpo bruto** para a ferramenta downstream (em vez de um objeto
  re-serializado) mantém o payload idêntico ao que a Tryno enviou. Se você
  precisar acrescentar dados de `/v1`, faça isso no `waitUntil`, depois de já ter
  respondido `204`.
</Note>

## Limites que valem repetir

| Limite                            | Valor                                                                        |
| --------------------------------- | ---------------------------------------------------------------------------- |
| Escrita na Tryno pela API pública | **Não existe** — v1 é somente leitura                                        |
| Escopos disponíveis               | `community:read`, `members:read`, `posts:read`, `events:read`, `orders:read` |
| Requisições                       | 120/min por chave                                                            |
| Latência do webhook               | ciclo de cron a cada 5 min, lotes de 50                                      |
| Prazo de resposta do seu receptor | 10 segundos                                                                  |
| Reentregas                        | 6 tentativas: imediata, +1m, +5m, +30m, +2h, +6h                             |
| E-mail/telefone de membros        | Não exposto pela API                                                         |
| Posts restritos                   | `/v1/posts` devolve **só** posts publicados e públicos                       |
| App oficial em Zapier/Make/n8n    | Não há — use blocos Webhook + HTTP Request                                   |

<Card title="Referência completa dos endpoints →" icon="braces" href="/api-reference/overview" horizontal>
  Os campos exatos de cada resposta, gerados a partir do contrato OpenAPI que o
  produto serve.
</Card>
