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

# Authentication

> Authenticate with the Zupy Partner API using API key authentication via X-API-Key header

## Overview

Zupy uses **API key authentication** for all partner API access. Each restaurant (company) you integrate with gets its own API key, provisioned by the Zupy team during onboarding.

* **One key per restaurant** — if you integrate with 3 restaurants, you receive 3 API keys
* **No OAuth flows** — authenticate once per request with a single header
* **No self-service** — keys are provisioned by Zupy ops, not generated in a dashboard

## API Key Format

All partner API keys follow this format:

```
zupy_pk_ + 32 alphanumeric characters
```

Example:

```
zupy_pk_abc123def456ghi789jkl012mno345pq
```

<Note>
  API keys are generated by the Zupy team and delivered to you during onboarding. You cannot create or rotate keys yourself.
</Note>

## Authentication Flow

<Steps>
  <Step title="Zupy provisions your key">
    During onboarding, the Zupy team creates your API key and assigns it to a specific restaurant (company). You also receive your webhook URL.
  </Step>

  <Step title="You receive credentials">
    You receive your `zupy_pk_*` API key, the company ID, and your integration slug (e.g., `repediu`).
  </Step>

  <Step title="Include X-API-Key in every request">
    Add the `X-API-Key` header to all API requests. No `Authorization: Bearer` — just the header and the key.
  </Step>
</Steps>

## Making Authenticated Requests

Include your API key in the `X-API-Key` header on every request:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.zupy.com/api/v2/customers/?phone=5511987654321" \
    -H "X-API-Key: zupy_pk_your_api_key_here"
  ```

  ```python Python theme={null}
  import requests

  BASE_URL = "https://api.zupy.com/api/v2"
  API_KEY = "zupy_pk_your_api_key_here"
  HEADERS = {"X-API-Key": API_KEY}

  response = requests.get(
      f"{BASE_URL}/customers/",
      params={"phone": "5511987654321"},
      headers=HEADERS,
  )
  print(response.json())
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.zupy.com/api/v2";
  const API_KEY = "zupy_pk_your_api_key_here";

  const response = await fetch(
    `${BASE_URL}/customers/?phone=5511987654321`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const data = await response.json();
  console.log(data);
  ```
</CodeGroup>

## Access Levels

API keys have two access levels, configured by Zupy during onboarding:

| Level          | Can Do                                                            | Cannot Do                                      |
| -------------- | ----------------------------------------------------------------- | ---------------------------------------------- |
| **Read-only**  | Search customers, view points/rewards/coupons, list programs      | Award points, redeem rewards, validate coupons |
| **Read-write** | Everything above + award points, redeem rewards, validate coupons | N/A                                            |

If you attempt a write operation with a read-only key, you receive a `403 Forbidden` error.

## Endpoint allowlist

Your partner API key can only reach the endpoints documented in this reference — the
partner contract. Any other v2 endpoint (consumer, scanner, or internal endpoints) returns
`403 Forbidden` for a partner key, even though the same path may exist for the dashboard or
mobile apps. This is a deliberate, fail-secure default: the partner surface equals exactly
what is documented here, and new internal endpoints are never exposed to partner keys by
accident.

```json 403 — Endpoint not available to partner keys theme={null}
{
  "type": "https://api.zupy.com/errors/permission-denied",
  "title": "Permission Denied",
  "status": 403,
  "detail": "This endpoint is not available to partner API keys."
}
```

## Error Handling

All authentication errors follow the [RFC 7807](https://tools.ietf.org/html/rfc7807) Problem Details format.

<CodeGroup>
  ```json 401 — Authentication Required theme={null}
  {
    "type": "https://api.zupy.com/errors/authentication-required",
    "title": "Authentication Required",
    "status": 401,
    "detail": "Invalid API key"
  }
  ```

  ```json 403 — Permission Denied theme={null}
  {
    "type": "https://api.zupy.com/errors/permission-denied",
    "title": "Permission Denied",
    "status": 403,
    "detail": "Read-only API key cannot perform write operations"
  }
  ```

  ```json 403 — OTP Required theme={null}
  {
    "type": "https://api.zupy.com/errors/permission-denied",
    "title": "Permission Denied",
    "status": 403,
    "detail": "Customer OTP verification required for this action"
  }
  ```

  ```json 429 — Rate Limit Exceeded theme={null}
  {
    "type": "https://api.zupy.com/errors/rate-limit-exceeded",
    "title": "Rate Limit Exceeded",
    "status": 429,
    "detail": "Too many requests. Please retry after 30 seconds."
  }
  ```
</CodeGroup>

## Rate Limits

API requests are rate-limited per API key. Your tier is assigned during onboarding.

| Tier           | Requests/min | Assigned To                             |
| -------------- | ------------ | --------------------------------------- |
| **Free**       | 60           | Default for new integrations            |
| **Standard**   | 300          | Active partners (e.g., Repediu, Saipos) |
| **Enterprise** | 3,000        | High-volume partners                    |

Every response includes rate limit headers:

```http theme={null}
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 299
X-RateLimit-Reset: 1711108800
```

<Note>
  The `Retry-After` header (seconds to wait) is only included in `429` responses. When you receive a `429`, wait for the `Retry-After` duration before retrying.
</Note>

## OTP — Optional Per-Integration

OTP (One-Time Password) provides extra customer verification. **OTP is NOT required for all partners** — it depends on your integration's trust level, configured by Zupy during onboarding.

| Action                      | OTP Possible?  | Depends On                     |
| --------------------------- | -------------- | ------------------------------ |
| Search customers            | Never          | —                              |
| View points balance/history | Never          | —                              |
| Award points                | Never          | B2B operation                  |
| List rewards/coupons        | Never          | —                              |
| **Redeem reward**           | **Per config** | `require_otp_for_redemption`   |
| **Validate/use coupon**     | **Per config** | `require_otp_for_coupon_usage` |
| Send webhook                | Never          | —                              |

<Accordion title="Partner examples">
  | Partner     | Enrollment OTP | Redemption OTP | Coupon OTP | Trust Partner | Rationale                                                    |
  | ----------- | -------------- | -------------- | ---------- | ------------- | ------------------------------------------------------------ |
  | **Repediu** | No             | No             | No         | Yes           | iFood/Rappi already verify customer identity                 |
  | **Goomer**  | No             | Yes            | Yes        | No            | Open tablet — customer types phone, no prior verification    |
  | **Saipos**  | No             | Yes            | Yes        | Yes           | POS identifies customer, but coupon usage needs confirmation |
</Accordion>

If your integration requires OTP for certain actions, see the [OTP Flow guide](/guides/otp-flow) for the complete 3-step verification process.

## Security Best Practices

<Warning>
  **Never expose API keys in frontend or client-side code.** All API calls must go through your backend server.
</Warning>

* **Always use HTTPS** — HTTP requests are rejected
* **Store keys in environment variables** — never hardcode in source code

<CodeGroup>
  ```bash .env theme={null}
  ZUPY_API_KEY=zupy_pk_your_api_key_here
  ZUPY_BASE_URL=https://api.zupy.com/api/v2
  ```

  ```python Python theme={null}
  import os

  API_KEY = os.environ["ZUPY_API_KEY"]
  BASE_URL = os.environ.get("ZUPY_BASE_URL", "https://api.zupy.com/api/v2")
  ```

  ```javascript JavaScript theme={null}
  const API_KEY = process.env.ZUPY_API_KEY;
  const BASE_URL = process.env.ZUPY_BASE_URL || "https://api.zupy.com/api/v2";
  ```
</CodeGroup>

* **One key per restaurant** — do not share keys between companies
* **Contact Zupy immediately** if you suspect a key has been compromised

## Next Steps

<Card title="Quick Start" icon="rocket" href="/guides/getting-started">
  Get your first API call working in under 15 minutes
</Card>

<Card title="Webhook Setup" icon="webhook" href="/guides/webhook-setup">
  Send order data to Zupy for automatic loyalty processing
</Card>

<Card title="OTP Flow" icon="lock" href="/guides/otp-flow">
  Implement customer identity verification when required
</Card>
