# Autenticação (/pt-BR/docs/getting-started/authentication)

> OAuth client credentials, escopos, expiração de 3600 s, credencial pausada ou revogada e limites do endpoint de token.

A API usa OAuth 2.0 com o fluxo **client credentials**: o seu sistema troca `client_id` e `client_secret` por um token de acesso e envia esse token em todas as chamadas. Não há login de usuário, redirecionamento nem refresh token.

1. Seu sistema chama `POST /oauth/token` com a credencial.
2. A API responde com um JWT válido por 3600 segundos e os escopos concedidos.
3. Seu sistema envia o token em `Authorization: Bearer` em cada chamada.
4. Antes de o token expirar, seu sistema pede outro.

## Obtendo o token [#obtendo-o-token]

```http
POST https://api.meupedido.io/open-delivery/oauth/token
```

O caminho `POST /v1/oauth/token` é um alias e responde igual.

A credencial pode ser enviada de três formas. O formulário é a forma canônica do OAuth 2.0 e a que a maioria das bibliotecas usa por padrão.

**Formulário**

`Content-Type: application/x-www-form-urlencoded`, com os três campos no corpo.

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=mp_7f3a9c1e5b2d4a6f8e0c1b3d" \
  -d "client_secret=$CLIENT_SECRET"
```

**JSON**

`Content-Type: application/json`. Os nomes podem vir em snake\_case ou camelCase (`clientId`, `clientSecret`, `grantType`).

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "mp_7f3a9c1e5b2d4a6f8e0c1b3d",
    "client_secret": "'"$CLIENT_SECRET"'"
  }'
```

**HTTP Basic**

`Authorization: Basic` com `client_id:client_secret` em Base64 e apenas o `grant_type` no corpo do formulário.

```bash title="Terminal"
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -u "mp_7f3a9c1e5b2d4a6f8e0c1b3d:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials"
```

Qualquer outro `Content-Type` responde `415`.

### Resposta [#resposta]

```json title="200 OK"
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIG1lcmNoYW50OnJlYWQgY2F0YWxvZzpyZWFkIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6IjZkMmY0YzhhLTFiM2UtNGY1YS05YzdkLTJlOGIwYTFmM2M1ZCIsImNsaWVudF9pZCI6Im1wXzdmM2E5YzFlNWIyZDRhNmY4ZTBjMWIzZCIsInNjb3BlIjoib3JkZXJzOnJlYWQgb3JkZXJzOndyaXRlIG1lcmNoYW50OnJlYWQgY2F0YWxvZzpyZWFkIn0.Q1w7Xy0Yt3cJ5w8NnZK2p9Vb4Ls6Hd1Ff0Rr8Mm3Aa0",
  "token_type": "bearer",
  "tokenType": "bearer",
  "expires_in": 3600,
  "expiresIn": 3600,
  "scope": "orders:read orders:write merchant:read catalog:read"
}
```

Cada campo vem em snake\_case (como o RFC 6749 define) e em camelCase (como a especificação Open Delivery escreve). Os valores são idênticos; leia o que a sua biblioteca esperar.

| Campo          | Significado                                    |
| -------------- | ---------------------------------------------- |
| `access_token` | O JWT a enviar em `Authorization: Bearer`      |
| `token_type`   | Sempre `bearer`                                |
| `expires_in`   | Validade em segundos, sempre `3600`            |
| `scope`        | Os escopos da credencial, separados por espaço |

O token é um JWT assinado com HS256 pelo MeuPedido. Ele carrega as claims `merchant_id` (a loja), `client_id` e `scope`. Trate-o como opaco: não dependa do formato interno, e nunca tente validá-lo localmente, porque a chave de assinatura não é pública.

### Em código [#em-código]

**cURL**

```bash
curl -X POST https://api.meupedido.io/open-delivery/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"
```

**Node.js**

```ts title="auth.ts"
const BASE_URL = 'https://api.meupedido.io/open-delivery';

export async function getToken(clientId: string, clientSecret: string) {
  const body = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: clientId,
    client_secret: clientSecret,
  });

  const res = await fetch(`${BASE_URL}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });

  if (!res.ok) {
    const error = await res.json().catch(() => ({}));
    throw new Error(`token ${res.status}: ${error.error ?? 'unknown'}`);
  }

  const data = await res.json();
  return {
    accessToken: data.access_token as string,
    // Renova 5 minutos antes de expirar, para nunca usar um token no limite.
    expiresAt: Date.now() + (data.expires_in - 300) * 1000,
  };
}
```

**Python**

```python title="auth.py"
import time
import requests

BASE_URL = "https://api.meupedido.io/open-delivery"

def get_token(client_id: str, client_secret: str) -> tuple[str, float]:
    res = requests.post(
        f"{BASE_URL}/oauth/token",
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
        },
        timeout=10,
    )
    res.raise_for_status()
    data = res.json()
    # Renova 5 minutos antes de expirar, para nunca usar um token no limite.
    expires_at = time.time() + data["expires_in"] - 300
    return data["access_token"], expires_at
```

**C#**

```csharp title="MeuPedidoAuth.cs"
using System.Net.Http.Json;

public sealed class MeuPedidoAuth(HttpClient http, string clientId, string clientSecret)
{
    private const string BaseUrl = "https://api.meupedido.io/open-delivery";

    private string? _token;
    private DateTimeOffset _expiresAt;

    public async Task<string> GetTokenAsync(CancellationToken ct = default)
    {
        if (_token is not null && DateTimeOffset.UtcNow < _expiresAt)
            return _token;

        using var content = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["grant_type"] = "client_credentials",
            ["client_id"] = clientId,
            ["client_secret"] = clientSecret,
        });

        using var res = await http.PostAsync($"{BaseUrl}/oauth/token", content, ct);
        res.EnsureSuccessStatusCode();

        var body = await res.Content.ReadFromJsonAsync<TokenResponse>(ct)
            ?? throw new InvalidOperationException("Resposta vazia do endpoint de token.");

        _token = body.AccessToken;
        // Renova 5 minutos antes de expirar, para nunca usar um token no limite.
        _expiresAt = DateTimeOffset.UtcNow.AddSeconds(body.ExpiresIn - 300);
        return _token;
    }

    private sealed record TokenResponse(string AccessToken, int ExpiresIn, string Scope);
}
```

## Usando o token [#usando-o-token]

Envie o token no header `Authorization` de todas as chamadas à API.

```bash title="Terminal"
curl "https://api.meupedido.io/open-delivery/v1/events:polling" \
  -H "Authorization: Bearer $TOKEN"
```

Sem o header, a resposta é `401` com corpo vazio e o header `WWW-Authenticate`.

## Expiração e renovação [#expiração-e-renovação]

O token vale por **3600 segundos** a partir da emissão. Não existe refresh token: renovar é chamar `POST /oauth/token` de novo.

A estratégia recomendada:

* Guarde o token junto com o instante de expiração calculado a partir de `expires_in`.
* Renove **antes** de expirar, com uma margem de alguns minutos, em vez de esperar o `401`.
* Se ainda assim uma chamada responder `401`, peça um token novo uma vez e repita a chamada. Se o endpoint de token também responder `401 invalid_client`, a credencial foi pausada ou revogada: pare e avise o operador.
* Um token por credencial, compartilhado entre as threads ou workers do seu processo. Pedir um token a cada requisição esgota o limite do endpoint.

## Escopos [#escopos]

Os escopos são definidos pelo lojista ao criar a credencial e não podem ser alterados depois. O token carrega exatamente os escopos da credencial.

| Escopo          | Libera                                                                                 |
| --------------- | -------------------------------------------------------------------------------------- |
| `orders:read`   | `GET /v1/events:polling`, `POST /v1/events/acknowledgment`, `GET /v1/orders/{orderId}` |
| `orders:write`  | `POST /v1/orders/{orderId}/confirm` e as demais ações                                  |
| `merchant:read` | `GET /v1/merchant/{merchantId}`                                                        |
| `catalog:read`  | `GET /v1/merchant/{merchantId}/menus`                                                  |

Uma chamada a um endpoint cujo escopo a credencial não tem responde:

```json title="403 Forbidden"
{
  "error": "insufficient_scope",
  "message": "A credencial não possui o escopo orders:write."
}
```

## Credencial pausada ou revogada [#credencial-pausada-ou-revogada]

O lojista pode pausar, retomar ou revogar a credencial a qualquer momento no painel. O efeito é imediato e vale para tokens já emitidos, mesmo dentro do prazo de validade:

```json title="401 Unauthorized"
{
  "error": "invalid_token",
  "message": "A credencial foi pausada ou revogada pelo lojista."
}
```

Com a credencial pausada, os eventos continuam sendo acumulados no feed e ficam disponíveis assim que ela for retomada. Revogação é permanente: para voltar, o lojista cria outra credencial.

## Erros do endpoint de token [#erros-do-endpoint-de-token]

O endpoint de token responde no formato do RFC 6749: `error` com um código estável e `error_description` com o texto em pt-BR. É o formato que toda biblioteca OAuth 2.0 já sabe ler, e por isso é diferente do `error` e `message` das demais rotas.

| Status | `error`                  | Quando                                                                         |
| ------ | ------------------------ | ------------------------------------------------------------------------------ |
| `400`  | `unsupported_grant_type` | `grant_type` diferente de `client_credentials`                                 |
| `400`  | `invalid_request`        | Falta `client_id`, `client_secret` ou `grant_type`, ou o corpo está malformado |
| `401`  | `invalid_client`         | Credencial inexistente, segredo errado, pausada ou revogada                    |
| `413`  | `invalid_request`        | Corpo acima de 4 KB. Um pedido de token tem só três campos                     |
| `415`  | `invalid_request`        | `Content-Type` diferente de formulário ou JSON                                 |
| `429`  | `rate_limit_exceeded`    | Limite do endpoint atingido; espere o `Retry-After`                            |

```json title="400 Bad Request"
{
  "error": "invalid_request",
  "error_description": "Informe client_id e client_secret."
}
```

O `401` vem só com o código, de propósito. Credencial inexistente e segredo errado recebem exatamente a mesma resposta, para o endpoint não revelar quais `client_id` existem:

```json title="401 Unauthorized"
{
  "error": "invalid_client"
}
```

## Limites do endpoint de token [#limites-do-endpoint-de-token]

O endpoint de token tem dois limites independentes, ambos respondendo `429` com `Retry-After: 60`:

* **60 requisições por minuto por endereço IP.** Um único processo bem comportado nunca chega perto disso. O `429` vem com `error` e `message`.
* **10 falhas por minuto por credencial.** Se o seu sistema entrar em loop com um segredo errado, ele vai bater neste limite antes de qualquer outro, e o `429` vem com `error` e `error_description`. O limite vale só para o segredo errado: o segredo correto continua emitindo token normalmente, então ninguém que conheça apenas o seu `client_id` consegue derrubar a sua integração.

Os demais endpoints têm o limite geral de 600 requisições por minuto por credencial, descrito em [Boas práticas](/pt-BR/docs/getting-started/best-practices).

## Segurança do segredo [#segurança-do-segredo]

* O `client_secret` é mostrado uma única vez, ao criar a credencial. Guarde-o em um cofre de segredos ou em variável de ambiente.
* Nunca o coloque em código, repositório, log, mensagem de erro, URL ou ticket de suporte. O suporte nunca pede o segredo.
* Use uma credencial por loja e por sistema. Se um segredo vazar, o lojista revoga só aquela credencial.
* Toda comunicação é HTTPS. Não há endpoint HTTP.
* CORS está liberado apenas para o playground deste portal. A integração é servidor a servidor: chamar a API a partir de um navegador expõe o segredo e é bloqueado.

## Próximos passos [#próximos-passos]

* [Primeira integração em 10 minutos](/pt-BR/docs/getting-started/first-steps/first-integration): use o token para receber o primeiro pedido.
* [Polling](/pt-BR/docs/getting-started/orders/polling): o loop de consulta de eventos.
* [Boas práticas](/pt-BR/docs/getting-started/best-practices): limites, erros e backoff.
