> ## 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.

# Storefront headless

> API pública para montar sua própria vitrine usando o catálogo, checkout e pedidos da loja.gg.

Com a **Headless Storefront API** você monta a vitrine como quiser — Next.js, WordPress, app mobile, o que for. O catálogo, o carrinho e os pedidos vêm desta API. **O pagamento continua acontecendo no checkout hospedado** da loja.gg: você redireciona o comprador para uma `checkout_url` e recebe a confirmação por webhook.

**Base URL:** `https://api.loja.gg/storefront/v1`

## 1. Antes de começar

No painel, em **Loja → Storefront API**:

<Steps>
  <Step title="Habilite a API headless">
    Sem isso, todas as chamadas respondem `403 headless_disabled`.
  </Step>

  <Step title="Cadastre as origens permitidas">
    Os domínios de onde o navegador vai chamar a API — ex.: `https://minhaloja.com.br`. Precisa ser `https` (exceto `http://localhost`), sem caminho e sem wildcard. Se a origem não estiver cadastrada, o navegador bloqueia a chamada.
  </Step>

  <Step title="Cadastre as URLs de retorno">
    Para onde o comprador volta depois do checkout — ex.: `https://minhaloja.com.br/obrigado`. Sem isso, a criação da sessão de checkout falha com `redirect_url_not_allowed`.
  </Step>

  <Step title="Crie suas chaves">
    A chave completa aparece **uma única vez**, no momento da criação. Só o hash é armazenado — se perder, revogue e gere outra.
  </Step>
</Steps>

## 2. As duas chaves

|                                   | Publishable             | Secret                     |
| --------------------------------- | ----------------------- | -------------------------- |
| Formato                           | `hk_live_pub_...`       | `hk_live_sec_...`          |
| Onde usar                         | JavaScript do site, app | **Apenas** no seu servidor |
| Pode aparecer no bundle do front? | Sim                     | **Nunca**                  |
| Escopos                           | catálogo, checkout      | todos                      |

```
Authorization: Bearer hk_live_pub_xxxxx
```

<Warning>
  Se a chave secreta for usada a partir de um navegador, a requisição é bloqueada com `secret_key_in_browser` e um alerta é disparado. Não é um bug — é a detecção de que a chave vazou no bundle do seu front. Revogue e emita outra.
</Warning>

### Modo de teste

Chaves `hk_test_*` funcionam contra o sandbox dos gateways e nunca movimentam dinheiro real. Toda resposta traz `livemode: true|false`. Construa a loja inteira em test antes de trocar as chaves para `hk_live_*`.

## 3. Catálogo

```bash theme={null}
curl https://api.loja.gg/storefront/v1/products?limit=20 \
  -H "Authorization: Bearer hk_live_pub_xxxxx"
```

```json Resposta theme={null}
{
  "data": [
    {
      "id": "6512ab...",
      "slug": "vip-gold",
      "name": "VIP Gold",
      "images": ["https://..."],
      "price": 4990,
      "compare_at_price": 6990,
      "currency": "BRL",
      "available": true,
      "promotion": { "discount_type": "percentage", "discount_amount": 28 }
    }
  ],
  "has_more": true,
  "next_cursor": "2",
  "livemode": true,
  "request_id": "req_01hz..."
}
```

<Warning>
  Todos os valores estão **em centavos**. `4990` é R\$ 49,90. Nunca use ponto flutuante para dinheiro — é assim que surgem diferenças como `49.900000000000006` e o total da vitrine diverge do total do checkout.
</Warning>

| Método | Rota                     | Escopo            |
| ------ | ------------------------ | ----------------- |
| GET    | `/store`                 | `catalog:read`    |
| GET    | `/products`              | `catalog:read`    |
| GET    | `/products/{id_ou_slug}` | `catalog:read`    |
| GET    | `/categories`            | `categories:read` |
| GET    | `/payment-methods`       | `catalog:read`    |

Filtros em `/products`: `page`, `limit` (máx. 100), `category`, `category_id`, `search`.

<Tip>
  Use o `ETag` retornado: repita a chamada com `If-None-Match` e receba `304` sem consumir banda.
</Tip>

## 4. Checkout

Você monta o carrinho no seu site e, na hora de pagar, cria a sessão e redireciona o comprador.

```bash theme={null}
curl -X POST https://api.loja.gg/storefront/v1/checkout/sessions \
  -H "Authorization: Bearer hk_live_pub_xxxxx" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{ "product_id": "6512ab...", "quantity": 1, "variables": {"nick": "Joao"} }],
    "customer": { "email": "cliente@exemplo.com", "discord_id": "1234" },
    "coupon_code": "BLACK10",
    "success_url": "https://minhaloja.com.br/obrigado",
    "cancel_url": "https://minhaloja.com.br/carrinho",
    "metadata": { "cart_ref": "abc123" }
  }'
```

```json Resposta theme={null}
{
  "data": {
    "id": "6512cd...",
    "checkout_url": "https://checkout.loja.gg/c/OSC1nX...",
    "expires_at": "2026-08-15T18:30:00Z",
    "amount_subtotal": 6990,
    "amount_discount": 2499,
    "amount_total": 4491,
    "currency": "BRL",
    "line_items": [ "..." ]
  },
  "livemode": true,
  "request_id": "req_01hz..."
}
```

Redirecione o comprador para `checkout_url`.

<Warning>
  **Não existe campo de preço na requisição.** O valor é sempre recalculado a partir do produto no banco — mesmo que você envie um preço, ele é ignorado.
</Warning>

<ParamField header="Idempotency-Key" type="string" required>
  Obrigatória. Se a requisição der timeout e você repetir com a mesma chave e o mesmo corpo, a resposta original é devolvida em vez de criar uma segunda sessão. Corpo diferente com a mesma chave retorna `409`.
</ParamField>

<Note>A sessão de checkout expira em 30 minutos.</Note>

### Validando um cupom

```bash theme={null}
curl -X POST https://api.loja.gg/storefront/v1/coupons/validate \
  -H "Authorization: Bearer hk_live_pub_xxxxx" \
  -d '{"code":"BLACK10","items":[{"product_id":"6512ab...","quantity":1}]}'
```

Um cupom inválido devolve `200` com `valid: false` e uma mensagem genérica — a API não distingue "não existe" de "expirado" de "não se aplica", para não entregar um oráculo de força bruta contra códigos de cupom.

## 5. Pedidos

```bash theme={null}
GET /storefront/v1/orders?limit=25&status=paid    # chave secreta
GET /storefront/v1/orders/{id}                     # chave secreta
GET /storefront/v1/customers/me/orders             # + token do comprador
```

Paginação por cursor: passe o `next_cursor` da resposta anterior em `starting_after`.

<Note>
  O CPF/CNPJ vem sempre mascarado (`***.***.789-**`), inclusive para a chave secreta. Dados de gateway, taxas, split e margem não são expostos nesta API — eles ficam no painel. Um pedido que não é da sua loja responde `404`, nunca `403` (um `403` confirmaria que o id existe).
</Note>

## 6. Erros

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "product_unavailable",
    "message": "Um dos produtos não está disponível para compra.",
    "param": "items",
    "docs_url": "https://docs.loja.gg/storefront/errors#product_unavailable"
  },
  "request_id": "req_01hz..."
}
```

Programe contra o `code`, não contra a `message` — a mensagem pode mudar, o código não.

| Código                     | HTTP | O que fazer                                  |
| -------------------------- | ---- | -------------------------------------------- |
| `missing_api_key`          | 401  | Faltou o header `Authorization`              |
| `invalid_api_key`          | 401  | Chave errada ou de outro ambiente            |
| `api_key_revoked`          | 401  | Emita uma nova                               |
| `secret_key_in_browser`    | 403  | Chave secreta vazou no front — revogue       |
| `origin_not_allowed`       | 403  | Cadastre o domínio nas configurações         |
| `headless_disabled`        | 403  | Habilite a API headless no painel            |
| `insufficient_scope`       | 403  | A chave não tem o escopo da rota             |
| `store_maintenance`        | 503  | Loja em manutenção; tente depois             |
| `redirect_url_not_allowed` | 400  | Cadastre a URL de retorno                    |
| `product_unavailable`      | 400  | Produto inativo, bloqueado ou de outra loja  |
| `rate_limit_exceeded`      | 429  | Respeite o `Retry-After`                     |
| `idempotency_key_reuse`    | 409  | Mesma chave com corpo diferente              |
| `request_in_progress`      | 409  | A requisição anterior ainda está processando |

Inclua sempre o `request_id` ao abrir um chamado de suporte.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Integrando com Next.js" icon="code" href="/api-reference/storefront-nextjs">
    Um cliente de API pronto e o modelo mental de chaves publishable/secret no App Router.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/api-reference/webhooks">
    Como validar assinatura e entregar o produto de forma confiável.
  </Card>
</CardGroup>
