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

# Integrando com Next.js

> Como consumir a Headless Storefront API em um projeto Next.js (App Router).

Este guia cobre o lado Next.js da [Storefront headless](/api-reference/storefront-headless). O contrato completo da API (rotas, formatos, erros) está documentado lá — aqui o foco é como estruturar o projeto.

## Modelo mental: três fronteiras que não podem se misturar

| Camada                                        | Chave              | Onde roda                     |
| --------------------------------------------- | ------------------ | ----------------------------- |
| Catálogo público (produtos, categorias, loja) | `hk_*_pub_*`       | Server Component ou browser   |
| Criar checkout, validar cupom                 | `hk_*_pub_*`       | Server Action / Route Handler |
| Listar pedidos, ler eventos                   | `hk_*_sec_*`       | **Somente servidor**          |
| Entrega do produto                            | segredo do webhook | Route Handler                 |

<Tip>
  A chave publishable *pode* ir para o browser, mas mesmo assim é comum fazer tudo passar pelo servidor Next.js: dá um lugar único para logar `request_id` e tratar erro. A exceção é o rate limit por IP do checkout (10/min) — se a loja tiver volume alto e concentrado, prefira chamar `/checkout/sessions` direto do browser com a chave publishable, para não concentrar todas as requisições no IP do seu servidor.
</Tip>

### Chamando do browser: o query param `key` não é opcional

Se alguma chamada sair do navegador (fetch em Client Component, ou uma SPA), inclua a chave publishable como query param **além** do header:

```
GET /storefront/v1/products?limit=100&key=hk_live_pub_xxxxx
```

Isso é necessário porque `Authorization` não é um header safelisted do CORS: toda requisição com esse header dispara um preflight `OPTIONS`, e o preflight não carrega `Authorization`. Sem outro sinal, a API não consegue identificar a loja para checar a allowlist de origens, e responde `403` sem `Access-Control-Allow-Origin` — o que no console do navegador parece um erro de CORS por origem não cadastrada, mesmo quando a origem está correta. Chamadas server-to-server não têm header `Origin`, não disparam preflight e não precisam do query param.

## Variáveis de ambiente

```bash .env.local theme={null}
HUSKPAY_API_URL=https://api.loja.gg/storefront/v1
HUSKPAY_PUBLISHABLE_KEY=hk_test_pub_xxxxxxxx
HUSKPAY_SECRET_KEY=hk_test_sec_xxxxxxxx
HUSKPAY_WEBHOOK_SECRET=whsec_xxxxxxxx
NEXT_PUBLIC_SITE_URL=http://localhost:3000
```

<Warning>
  **Nenhuma chave usa o prefixo `NEXT_PUBLIC_`.** No Next.js, esse prefixo decide se a variável é embutida no bundle do cliente — prefixar a chave secreta é exatamente como ela vaza. Se você realmente precisar chamar o catálogo direto do browser, crie uma variável separada `NEXT_PUBLIC_HUSKPAY_PUBLISHABLE_KEY` e use **só** ela nesse caminho. Confirme que `.env*.local` está no `.gitignore`.
</Warning>

## Um cliente de API mínimo

`src/lib/huskpay/client.ts` — o `import 'server-only'` no topo faz o build quebrar se algum Client Component importar este arquivo por engano. É a garantia real contra vazamento de chave; organização de pastas sozinha não é suficiente.

```ts src/lib/huskpay/client.ts theme={null}
import 'server-only';

const BASE = process.env.HUSKPAY_API_URL!;

export type Envelope<T> = {
  data: T;
  livemode: boolean;
  request_id: string;
  has_more?: boolean;
  next_cursor?: string;
};

export class HuskpayError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    readonly type: string,
    message: string,
    readonly param?: string,
    readonly requestId?: string,
  ) {
    super(message);
    this.name = 'HuskpayError';
  }
}

type Options = RequestInit & {
  /** 'secret' só em código que nunca chega ao browser. */
  key?: 'publishable' | 'secret';
  idempotencyKey?: string;
  customerToken?: string;
};

export async function huskpay<T>(path: string, opts: Options = {}): Promise<Envelope<T>> {
  const { key = 'publishable', idempotencyKey, customerToken, ...init } = opts;

  const token =
    key === 'secret'
      ? process.env.HUSKPAY_SECRET_KEY
      : process.env.HUSKPAY_PUBLISHABLE_KEY;

  if (!token) throw new Error(`Chave ${key} não configurada no ambiente.`);

  const headers = new Headers(init.headers);
  headers.set('Authorization', `Bearer ${token}`);
  headers.set('Accept', 'application/json');
  if (init.body) headers.set('Content-Type', 'application/json');
  if (idempotencyKey) headers.set('Idempotency-Key', idempotencyKey);
  if (customerToken) headers.set('X-Huskpay-Customer-Token', customerToken);

  const res = await fetch(`${BASE}${path}`, { ...init, headers });

  if (!res.ok) {
    let body: any = null;
    try { body = await res.json(); } catch {}
    throw new HuskpayError(
      res.status,
      body?.error?.code ?? 'unknown_error',
      body?.error?.type ?? 'api_error',
      body?.error?.message ?? `Falha na API (${res.status}).`,
      body?.error?.param,
      body?.request_id,
    );
  }

  return res.json();
}
```

## Padrões recomendados

<CardGroup cols={2}>
  <Card title="Catálogo em Server Component" icon="layout-grid">
    Busque produtos com `huskpay('/products', { next: { revalidate: 60 } })` diretamente no componente de servidor — sem expor a chave ao cliente.
  </Card>

  <Card title="Checkout via Server Action" icon="credit-card">
    Receba o carrinho do cliente, gere um `Idempotency-Key` (UUID) por tentativa de compra e chame `/checkout/sessions` no servidor antes de redirecionar.
  </Card>

  <Card title="Pedidos com chave secreta" icon="lock">
    Só use `key: 'secret'` em Server Actions ou Route Handlers — nunca em código que pode acabar em um Client Component.
  </Card>

  <Card title="Entrega no webhook" icon="webhook">
    Implemente a entrega do produto em uma Route Handler dedicada para o webhook, validando a assinatura antes de processar. Veja [Webhooks](/api-reference/webhooks).
  </Card>
</CardGroup>

<Warning>
  Reaproveite o mesmo `Idempotency-Key` apenas entre retries da **mesma** tentativa de compra do usuário. Um UUID novo por tentativa evita sessões de checkout duplicadas quando a rede falha e o cliente clica em "comprar" de novo.
</Warning>
