# Authentication

The Monetarie external API uses a security model with three main authentication layers -- API Key + Secret, per-request HMAC-SHA512 signature (only on `POST`) and mandatory IP whitelist -- complemented by content validation, rate limiting and idempotency.

## Validation Overview

The HTTP request passes through the validations below, **in this order**. Any rejection returns an error before business logic runs:

```
POST /api/external/...
  │
  ├─ 1. Content-Type ──────── Not application/json or multipart/form-data? → 415
  ├─ 2. X-Key-Case (KeyCase) ─ Converts params/response snake_case ↔ camelCase (optional)
  ├─ 3. API Key + Secret ───── Missing/invalid credentials? → 401 | Inactive API Key? → 401 | Expired API Key? → 401
  ├─ 4. IP Whitelist ───────── Empty whitelist? → 403 "ip whitelist required" | IP not allowed? → 403 "ip not allowed" | Inactive account? → 403
  ├─ 5. HMAC-SHA512 (POST) ─── Missing `hmac` header? → 401 | Invalid signature? → 401 | Invalid body? → 400 | API Key without secret? → 403
  ├─ 6. Rate Limiting ─── More than 90,000 req/min per IP? → 429 Retry-After: 60
  ├─ 7. Idempotency (POST) ─── Key > 256 chars? → 400 | Replay within 24h? → returns cached body + X-Idempotent-Replay: true
  └─ 8. API Key permission ───── API Key lacks the permission required by the route? → 403
       │
       └─ Request accepted → Business logic
```

::: info Validations by HTTP method
- **`GET /balance`** uses only `Content-Type → KeyCase → API Key + Secret → IP Whitelist → API Key permission` -- no rate limiter, no HMAC, no idempotency (high-frequency polling is authorized).
- **Other `GET` / `DELETE`** use `Content-Type → KeyCase → API Key + Secret → IP Whitelist → Rate Limiter → API Key permission` -- without HMAC and without idempotency.
- **`POST`** uses the full validation flow above (all validations).
:::

## Layer 1 -- API Key + Secret

All requests must include the `Authorization` header. The API accepts **two equivalent formats** -- the native `ApiKey` scheme or HTTP Basic Authentication. Both are validated by the same authentication layer and have the same behavior. Choose whichever is more convenient for your HTTP client.

### Recommended format -- `ApiKey` scheme

```
Authorization: ApiKey {client_id}:{client_secret}
```

### Alternative format -- HTTP Basic

```
Authorization: Basic {base64(client_id:client_secret)}
```

Example in Bash:

```bash
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"

# Equivalent to Authorization: ApiKey cli_a1b2...:sk_0123...
BASIC=$(printf '%s:%s' "$CLIENT_ID" "$CLIENT_SECRET" | base64)
curl -X GET https://api.monetarie.com/api/external/balance \
  -H "Authorization: Basic $BASIC"
```

::: tip When to use Basic vs ApiKey
Use `Basic` if your HTTP client (library, gateway, proxy) already builds credentials via base64 automatically (nearly all do). Use `ApiKey` if you prefer to send the secret as plain text inside the header -- same API validation.
:::

### Credential fields

| Component | Description | Prefix |
|-----------|-------------|--------|
| `client_id` | Public API Key identifier | `cli_` |
| `client_secret` | Secret key (we only store the hash) | `sk_` |

The secret is never stored in plain text. When a request arrives, the submitted secret is compared against the stored hash. If it does not match, the request is rejected before reaching business logic.

::: warning The API Key can expire
Even though in practice most API Keys are created without an expiration date, **the `expires_at` field may exist on the credential**. If a key is configured with `expires_at` in the past, authentication fails with `401 API key has expired`. It is also possible to revoke (mark as inactive): returns `401 API key is inactive`. This **replaces** the previous information in this documentation that stated API Keys were permanent.
:::

## Layer 2 -- HMAC-SHA512

Transactional requests (POST, PUT, PATCH) require HMAC-SHA512 signature of the body in the `hmac` header. The validation uses **constant-time comparison** to prevent timing attacks.

See [HMAC-SHA512](/en/hmac) for implementation examples in 6 languages.

## Layer 3 -- IP Whitelist

**Every API Key must have at least one IP in the whitelist** -- even a freshly created API Key with valid credentials is rejected while the whitelist is empty:

```json
{
  "error": {
    "status": 403,
    "message": "IP whitelist required. Configure at least one allowed IP to use this API key."
  }
}
```

Once the whitelist has at least one entry, requests from IPs outside the list receive:

```json
{
  "error": {
    "status": 403,
    "message": "Request IP not in API key whitelist"
  }
}
```

### Accepted formats

| Format | Example | Comment |
|--------|---------|---------|
| Individual IPv4 | `203.0.113.45` | Only one public endpoint |
| CIDR notation (IPv4) | `203.0.113.0/24` | Full /24 subnet (256 IPs). Use `/32` for a single host |
| Aggregated CIDR | `172.20.16.0/20` | Private range (example) -- accepted literally |

::: tip IPv4 vs IPv6
The API normalizes `::ffff:A.B.C.D` addresses (IPv4 mapped in IPv6, used by trusted load balancers) to the corresponding IPv4 address before comparing against the whitelist. You **do not need** to include the IPv6-mapped form; simply register the plain IPv4. For clients that egress exclusively via IPv6, register the full IPv6 address in standard notation (e.g., `2001:db8::1`).
:::

::: warning String format
The whitelist expects **exact** strings. A whitespace before/after, a wrong mask (`/28` when the range has 256 IPs), or an IP in notation with leading zeros (`203.000.113.045`) silently rejects requests with no warning in the response other than the standard 403. Always validate in the Merchant Portal using a test IP before going to production.
:::

Configure the whitelist in the Merchant Portal when creating or editing the API Key.

## Headers

### Mandatory headers

| Header | Value | Required |
|--------|-------|----------|
| `Authorization` | `ApiKey {client_id}:{client_secret}` or `Basic {base64(client_id:client_secret)}` | Yes -- all requests |
| `Content-Type` | `application/json` (or `multipart/form-data` in uploads) | Yes -- `POST`, `PUT`, `PATCH` with body. Sending `application/x-www-form-urlencoded` (default of `curl -d` without `-H`) returns `415 Unsupported Media Type` |
| `hmac` | HMAC-SHA512 signature of the body in lowercase hexadecimal | Yes -- only `POST` in `/api/external/*` |

### Optional headers

| Header | Value | Effect |
|--------|-------|--------|
| `Idempotency-Key` | Unique key ≤ 256 chars (UUID v4 recommended) | Dedupes replays within 24h. **Only works on `POST`** -- on `GET`/`DELETE` the header is silently ignored (no error) |
| `X-Key-Case` | `camelCase` | Converts `camelCase` from the request params to `snake_case` (input) and `snake_case` to `camelCase` in all keys of the JSON response (output). Useful for clients in JavaScript, TypeScript, or Kotlin |
| `X-Forwarded-For` | IP(s) separated by comma | Respected only when the direct TCP connection comes from a trusted proxy. Ignored in direct client connections |

::: tip `Idempotency-Key` -- server response
When you send the `Idempotency-Key` header, the server echoes the same value back in `Idempotency-Key` in the response and, if the request is a replay of one already processed in the last 24 hours (same key + same HTTP method + same path), also adds the `X-Idempotent-Replay: true` header and returns the cached body **exactly as it was returned in the first execution** (same HTTP status, same body byte-for-byte). The cache is scoped by `(method, path, key)` -- using the same key in different endpoints does not cause collision. Keys longer than 256 characters are rejected with `400 Idempotency-Key must be at most 256 characters`. Only `2xx` responses are cached -- error responses (`4xx`/`5xx`) allow retry with the same key.
:::

::: tip `X-Key-Case` -- conversion to camelCase
If your stack works in `camelCase` (JS/TS, Kotlin, Swift), send `X-Key-Case: camelCase` and the API accepts the request body in `camelCase` (e.g., `externalId`, `pixKey`) and returns the response with keys in `camelCase`. Without this header, the API stays in `snake_case` (e.g., `external_id`, `pix_key`). The header can be sent on any `/api/external/*` endpoint -- it need not always be the same value per API Key.
:::

::: warning HMAC signs the body **before** the internal conversion
If you send `X-Key-Case: camelCase` together with a `POST` that requires HMAC, sign the body **exactly as it travels on the wire** (i.e., in `camelCase` if that is the serialization you are using). The HMAC is computed on the server over the body as received, not over the internal `snake_case` form.
:::

## Complete Example

```bash
CLIENT_ID="cli_a1b2c3d4e5f6"
CLIENT_SECRET="sk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"

# Balance query (GET -- no HMAC)
curl -X GET https://api.monetarie.com/api/external/balance \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"

# PIX Cash-Out (POST -- with HMAC + Idempotency-Key)
# IMPORTANT: keys in alphabetical order (amount < description < pix_key < pix_key_type).
# The server reorders alphabetically before computing the expected HMAC - see /hmac.
BODY='{"amount":3000,"description":"Pagamento","pix_key":"12345678901","pix_key_type":"cpf"}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')

curl -X POST https://api.monetarie.com/api/external/pix/cash-out \
  -H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -H "hmac: $HMAC" \
  -H "Idempotency-Key: cashout-order-9876" \
  -d "$BODY"
```

## Additional Protections

| Protection | Description |
|------------|-------------|
| **Rate Limiting (API)** | 90,000 req/min (1,500 req/s) **per IP** across all `/api/external/*`, except `GET /balance` which has no rate limit -- high-frequency polling is allowed. Limiter key: `(IP, 60s window)`. When the limit is exceeded, the server responds `429 Too Many Requests` with header `Retry-After: 60`. On all `2xx` responses that pass through the limiter, the header `x-ratelimit-remaining` is attached with the number of remaining requests in the window |
| **Rate Limiting (edge WAF)** | Per-API-key/IP rules at the edge WAF when configured. Operates **before** the API, so a 429 from this layer does not increment the API counter |
| **Rate Limiting (auth)** | 5 req/min on admin/merchant authentication endpoints (does not apply to `/api/external/*`) |
| **DICT protection on Cash-Out** | Specific to `POST /pix/cash-out` when resolving PIX key via DICT. When applied, returns `HTTP 202` with `status: "queued"` and dispatches webhook `pix.payout.queued` while the transaction awaits automatic reprocessing. See [PIX Cash Out (by key)](/en/pix-cashout-key) |
| **AWS WAF** | Application firewall protecting the API with OWASP rules (XSS, SQLi, LFI, RFI, RCE) |
| **HTTPS + TLS 1.2+** | Mandatory encryption on all connections |
| **HSTS** | Browsers forced to use HTTPS |

### Rate limiting headers on the response

Whenever a request passes the rate limiter, the response includes:

| Header | Appears in | Value |
|--------|-----------|-------|
| `x-ratelimit-remaining` | 2xx responses (after passing the limiter) | Integer: remaining requests in the current 60s window, scoped per IP |
| `Retry-After` | Only on `429 Too Many Requests` | `60` (always, in seconds) -- wait before retrying |

::: tip How the limiter counts "windows"
Rate limiting uses **fixed 60-second windows**, not sliding windows. In practice, the stable 1,500 req/s per-IP limit is the behavior clients should consider.
:::

## Why HMAC-SHA512 and not mTLS?

mTLS (mutual TLS) authenticates the **connection**, not the **content**. If the connection is authenticated, all requests pass through without individual validation.

HMAC validates **each request separately**. Even within a valid connection, any change in the payload causes the request to be rejected.

| Aspect | mTLS | HMAC-SHA512 |
|--------|------|-------------|
| Validates | TLS channel | Request payload |
| Management | X.509 certificates (issuance, rotation, revocation, CRL/OCSP) | Generate pair, update, invalidate |
| Operational risk | Expired certificates -- frequent cause of incidents | Key is a simple string |
| Content integrity | No | Yes |

TLS already guarantees transport encryption. HMAC adds payload integrity and authenticity -- something that mTLS alone does not cover.

## Error Responses

::: warning The API has 4 distinct error shapes
Depending on which validation layer rejects the request, the shape of the JSON body is different. Always inspect the shape before parsing the error in the client:

1. **`{"error": {status, message}}`** -- `Content-Type` (415), `API Key + IP Whitelist` (401/403), `Idempotency` (400), rate limiting (429).
2. **`{"error": "forbidden", "message": "..."}`** -- only `API Key permission` (403).
3. **`{"worked": false, "detail": "..."}`** -- only `HMAC validation` (400, 401, 403).
4. **`{"errors": {atom: "msg"}}`** -- `business error handler` (any business error 4xx/5xx).
:::

### Layer 0 -- Content-Type

Before any authentication, `POST`/`PUT`/`PATCH` without an accepted Content-Type (`application/json` or `multipart/form-data`) is blocked.

#### 415 -- Unsupported Content-Type

```json
{
  "error": {
    "status": 415,
    "message": "Unsupported Media Type. Expected Content-Type: application/json",
    "hint": "Add header: -H 'Content-Type: application/json'"
  }
}
```

::: tip Common pitfall with `curl -d`
`curl -d '{"...":""}' URL` (without `-H`) sends `Content-Type: application/x-www-form-urlencoded` by default. The API may interpret this as form-urlencoded rather than JSON. Add `-H 'Content-Type: application/json'` to get the expected validation behavior.
:::

### Layer 1 -- API Key + IP Whitelist

Errors for missing/invalid credentials, inactive/expired API Key, or IP outside the whitelist come in the format `{"error": {status, message}}`:

#### 401 -- Missing Credentials

```json
{
  "error": {
    "status": 401,
    "message": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
  }
}
```

#### 401 -- Invalid Credentials

```json
{
  "error": {
    "status": 401,
    "message": "Invalid API key credentials"
  }
}
```

#### 401 -- Inactive API Key

```json
{
  "error": {
    "status": 401,
    "message": "API key is inactive"
  }
}
```

#### 401 -- Expired API Key

```json
{
  "error": {
    "status": 401,
    "message": "API key has expired"
  }
}
```

#### 403 -- Empty IP Whitelist

```json
{
  "error": {
    "status": 403,
    "message": "IP whitelist required. Configure at least one allowed IP to use this API key."
  }
}
```

#### 403 -- Unauthorized IP

```json
{
  "error": {
    "status": 403,
    "message": "Request IP not in API key whitelist"
  }
}
```

#### 403 -- Inactive Account

```json
{
  "error": {
    "status": 403,
    "message": "Account is not active"
  }
}
```

#### 403 -- Insufficient Permission

::: tip This 403 comes from permission validation
This error appears when the API Key is valid, the IP is authorized, but the credential does not have the permission required by the endpoint. That is why the JSON differs from credential/IP errors: it uses `{"error": "forbidden", "message": "..."}`.
:::

```json
{
  "error": "forbidden",
  "message": "API key lacks permission: transfer:write"
}
```

### Layer 2 -- HMAC-SHA512 Validation

Errors emitted by HMAC validation always come in the format `{"worked": false, "detail": "..."}` with different HTTP codes depending on the cause:

#### 401 -- Invalid signature or missing header

```json
{
  "worked": false,
  "detail": "Invalid HMAC signature"
}
```

```json
{
  "worked": false,
  "detail": "Missing HMAC header"
}
```

#### 400 -- Missing body or invalid JSON

```json
{
  "worked": false,
  "detail": "Request body is required for HMAC validation"
}
```

```json
{
  "worked": false,
  "detail": "Request body must be valid JSON for HMAC validation"
}
```

#### 403 -- API Key without HMAC secret configured

```json
{
  "worked": false,
  "detail": "HMAC secret not configured for this API key"
}
```

### Layer 3 -- Business Errors (`business error handler`)

After authentication, validation errors, missing parameters, not-found resources, and business rules come in the format `{"errors": {atom: "msg"}}`:

#### 400 -- Bad Request

```json
{
  "errors": {
    "bad_request": {
      "amount": ["is required"]
    }
  }
}
```

#### 404 -- Resource Not Found

```json
{
  "errors": {
    "not_found": "Transaction not found"
  }
}
```

#### 401 -- Unauthorized (business rule)

```json
{
  "errors": {
    "unauthorized": "invalid credentials"
  }
}
```

#### 422 -- Unprocessable Entity

```json
{
  "errors": {
    "unprocessable_entity": "Invalid PIX key format"
  }
}
```

### Layer 4 -- Rate Limiting (rate limiting or edge WAF)

#### 429 -- Rate Limit Exceeded

Body of the 429 coming from the rate limiting layer:

```json
{
  "error": {
    "status": 429,
    "message": "Too many requests. Please try again later."
  }
}
```

Headers included in the 429 response:

| Header | Value |
|--------|-------|
| `Retry-After` | `60` (seconds to wait before retry) |

::: tip How to react to 429
- **Exponential backoff**: start at 60s (value of `Retry-After`), double each subsequent retry up to a reasonable ceiling (e.g., 5 min).
- **Never ignore the header**: even if your client has its own retry strategy, `Retry-After` is the canonical source of truth for this endpoint.
- **If 429 is from the edge WAF** (layer above the API), the body may have a different shape -- 429 status with `Retry-After: 60` remains the standard for any layer.
:::

### Layer 5 -- Idempotency

#### 400 -- Idempotency-Key too long

```json
{
  "error": {
    "status": 400,
    "message": "Idempotency-Key must be at most 256 characters"
  }
}
```

## Permissions

Each API Key has a list of permissions that determine which endpoints may be accessed. If the API Key lacks the required permission, the request is rejected with `403 Forbidden`.

### How to obtain the API Key

1. Use the production base **api.monetarie.com**
2. Request credential issuance for the target account
3. Provide the IPs that must be added to the whitelist
4. Provide the required permissions (see table below)
5. After issuance, store the `client_id` and `client_secret` securely

To edit permissions of an existing API Key, request an update to the credential permissions.

::: danger Required to send PIX
To perform **PIX Cash-Out** operations (sending PIX), the API Key **must** have the `transfer:write` permission. Without this permission, all send attempts return `403 Forbidden` with the message `API key lacks permission: transfer:write`.

Minimum permissions recommended for full operation:
- **Cash-In (receive):** `pix:write` + `transfer:read`
- **Cash-Out (send):** `transfer:write` + `transfer:read`
- **Queries:** `transfer:read` + `account:read` + `statement:read`
- **Webhooks:** `account:write` + `account:read`
:::

### Available Permissions

| Permission | Description |
|------------|-------------|
| `pix:write` | Generate QR Code (Cash-In) |
| `pix:read` | List PIX keys |
| `transfer:write` | Send PIX (Cash-Out) |
| `transfer:read` | Query transactions (by ID, E2E, Tag, External ID), receipt, list transactions |
| `payment:write` | Request refund and submit MED defense |
| `payment:read` | List and query MED |
| `account:write` | Create and remove webhooks |
| `account:read` | Query balance, list webhooks, validate CPF |
| `statement:read` | Query statement |

### Permissions per Endpoint

| Endpoint | Method | Permission |
|----------|--------|------------|
| `/pix/cash-in` | POST | `pix:write` |
| `/pix/cash-out` | POST | `transfer:write` |
| `/pix/refund` | POST | `payment:write` |
| `/med/:id/defense` | POST | `payment:write` |
| `/cpf/validate` | POST | `account:read` |
| `/webhooks` | POST | `account:write` |
| `/webhooks` | GET | `account:read` |
| `/webhooks/:id` | DELETE | `account:write` |
| `/balance` | GET | `account:read` |
| `/transactions` | GET | `transfer:read` |
| `/transactions/:id` | GET | `transfer:read` |
| `/transactions/e2e/:e2e_id` | GET | `transfer:read` |
| `/transactions/tag/:tag` | GET | `transfer:read` |
| `/transactions/ref/:external_id` | GET | `transfer:read` |
| `/transactions/:id/receipt` | GET | `transfer:read` |
| `/pix/keys` | GET | `pix:read` |
| `/med` | GET | `payment:read` |
| `/med/:id` | GET | `payment:read` |
| `/statement` | GET | `statement:read` |

### Error Response -- 403 (Insufficient Permission)

See the format in [Layer 1 -- 403 Insufficient Permission](#layer-1-api-key-ip-whitelist).

Use `payment:read` to list and query MEDs. Use `payment:write` to submit a defense through `POST /api/external/med/:id/defense`. The defense endpoint receives HMAC-signed JSON; binary file uploads remain available in the Monetarie portals.

Permissions are configured at API Key creation by the Merchant Portal or the admin API.

::: warning Security
- Never expose the `client_secret` in frontend code or public repositories
- Use environment variables on your server
- The API Key **can expire** if the `expires_at` field is set; otherwise, it stays valid until manually revoked in the Merchant Portal
- Configure allowed IPs in the whitelist -- an empty whitelist blocks the key with `403 IP whitelist required`
:::
