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

# Getting Started

> Get your first API call working in under 15 minutes with the Zupy Partner API

Get up and running with the Zupy Partner API in 4 steps.

<Note>
  **Estimated Time**: Under 15 minutes
</Note>

## Prerequisites

* Your API key (format: `zupy_pk_*`) — provided by the Zupy team during onboarding
* The company ID for the restaurant you're integrating with
* Your integration slug (e.g., `repediu`) for webhook URLs

<Tip>
  Don't have credentials yet? Contact [webmaster@zupy.com.br](mailto:webmaster@zupy.com.br) to start the onboarding process.
</Tip>

## Step 1: Verify Your API Key

Test your credentials with a simple customer search:

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

<Note>
  **Phone format**: The `phone` parameter accepts digits only (e.g., `5511987654321`). The API normalizes all phone numbers to E.164 format (`+5511987654321`) internally, so any input format will match the same customer.
</Note>

**If your key is valid**, you receive a `200` response (even if no customers match):

```json theme={null}
{
  "count": 0,
  "next": null,
  "previous": null,
  "results": []
}
```

**If your key is invalid**, you receive a `401` error in RFC 7807 format:

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

## Step 2: Send a Test Webhook

Send order data to Zupy. Replace `{partner}` with your integration slug (e.g., `repediu`).

The webhook accepts **any JSON payload** — Zupy stores it raw and processes it asynchronously using a partner-specific adapter. During onboarding, the Zupy team will map your payload format. For new integrations, use this standard catch-all format:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.zupy.com/api/v2/webhooks/integrations/{partner}/" \
    -H "X-API-Key: zupy_pk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '[
      {
        "customer_name": "Maria Santos",
        "customer_phone": "+5511987654321",
        "customer_email": null,
        "customer_cpf": null,
        "order_total": "89.90",
        "order_id": "123456789",
        "order_date": "2026-03-22T14:30:00.000Z",
        "store_cnpj": "12.345.678/0001-90",
        "store_name": "Pizzaria Exemplo"
      }
    ]'
  ```

  ```python Python theme={null}
  payload = [
      {
          "customer_name": "Maria Santos",
          "customer_phone": "+5511987654321",
          "customer_email": None,
          "customer_cpf": None,
          "order_total": "89.90",
          "order_id": "123456789",
          "order_date": "2026-03-22T14:30:00.000Z",
          "store_cnpj": "12.345.678/0001-90",
          "store_name": "Pizzaria Exemplo",
      }
  ]

  response = requests.post(
      f"{BASE_URL}/webhooks/integrations/{{partner}}/",
      json=payload,
      headers=HEADERS,
  )
  print(response.json())
  ```

  ```javascript JavaScript theme={null}
  const partner = "your_slug"; // e.g., "repediu"
  const payload = [
    {
      customer_name: "Maria Santos",
      customer_phone: "+5511987654321",
      customer_email: null,
      customer_cpf: null,
      order_total: "89.90",
      order_id: "123456789",
      order_date: "2026-03-22T14:30:00.000Z",
      store_cnpj: "12.345.678/0001-90",
      store_name: "Pizzaria Exemplo",
    },
  ];

  const response = await fetch(
    `${BASE_URL}/webhooks/integrations/${partner}/`,
    {
      method: "POST",
      headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }
  );
  const data = await response.json();
  console.log(data);
  ```
</CodeGroup>

<Tip>
  **Already have your own payload format?** Send it as-is — Zupy accepts any JSON. During onboarding, the Zupy team will create a mapper for your specific field names, just like we did for Repediu, Saipos, and other partners.
</Tip>

**Success response** (campos na raiz):

```json theme={null}
{
  "status": "received",
  "id": "2bxRKmpWJY8lHHqsGfsQQtwCDef"
}
```

The webhook is processed **asynchronously**. You receive an immediate `200` with `"status": "received"`. Orders are processed in a background queue (typically within seconds).

<Note>
  **Idempotency**: Sending the same payload twice returns `"status": "duplicate"` — no error, no double-processing. Zupy computes a SHA-256 hash of the request body to detect duplicates.
</Note>

## Step 3: Look Up the Customer

After the webhook processes (typically within seconds), search for the customer:

<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}
  response = requests.get(
      f"{BASE_URL}/customers/",
      params={"phone": "5511987654321"},
      headers=HEADERS,
  )
  customers = response.json()["results"]
  print(f"Found {len(customers)} customer(s)")
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `${BASE_URL}/customers/?phone=5511987654321`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const { results: customers } = await response.json();
  console.log(`Found ${customers.length} customer(s)`);
  ```
</CodeGroup>

**Response** (campos na raiz):

```json theme={null}
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "2awTHloSJX7kGGprFerOOsvABcd",
      "full_name": "Maria Santos",
      "email": null,
      "phone": "+5511987654321",
      "cpf": null,
      "birth_date": null,
      "points_balance": 89,
      "points_earned": 89,
      "points_spent": 0,
      "tier": "default",
      "card_number": "ZP-ABC123",
      "join_date": "2026-03-22",
      "last_activity_date": "2026-03-22T14:30:00Z",
      "rfm_segment": "new",
      "program_id": "2awTHmNw8X7kGGpr...",
      "program_name": "Clube Pizzaria Exemplo",
      "company_id": "2awTHkVw8X7kGGpr..."
    }
  ]
}
```

## Step 4: Check Points Balance

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

  ```python Python theme={null}
  customer_id = "2awTHloSJX7kGGprFerOOsvABcd"
  response = requests.get(
      f"{BASE_URL}/customers/{customer_id}/points/",
      headers=HEADERS,
  )
  points = response.json()
  print(f"Balance: {points['points_balance']} {points['points_name']}")
  ```

  ```javascript JavaScript theme={null}
  const customerId = "2awTHloSJX7kGGprFerOOsvABcd";
  const response = await fetch(
    `${BASE_URL}/customers/${customerId}/points/`,
    { headers: { "X-API-Key": API_KEY } }
  );
  const points = await response.json();
  console.log(`Balance: ${points.points_balance} ${points.points_name}`);
  ```
</CodeGroup>

**Response** (campos na raiz):

```json theme={null}
{
  "customer_id": "2awTHloSJX7kGGprFerOOsvABcd",
  "points_balance": 89,
  "points_earned": 89,
  "points_spent": 0,
  "points_name": "Pontos",
  "last_activity_date": "2026-03-22T14:30:00Z",
  "tier": "default"
}
```

## You're Integrated!

You've successfully verified your API key, sent order data via webhook, looked up a customer, and checked their points balance.

## Field Glossary — balances and identifiers

To save you the time we spent figuring this out the first time, here are the partner-visible naming conventions in one table:

### Balance fields

| Field            | Where it appears                                                               | What it means                                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `points_balance` | `GET /customers/{id}/`, `GET /customers/{id}/points/`, OTP verify response     | The customer's **current** points balance. The stable, always-queryable field.                                                                     |
| `new_balance`    | `POST /rewards/{id}/redeem/` response, `POST /coupons/{id}/validate/` response | The customer's balance **immediately after** the operation. Same number as `points_balance` if you GET-after-POST — included so you don't have to. |
| `current_points` | (rare — Wallet / scanner contexts only)                                        | Synonym for `points_balance`. Will be deprecated. **Do not write new code against this.**                                                          |
| `zupy_balance`   | `GET /companies/` response                                                     | The **company's** Z\$ balance (marketing-campaign budget pool). Not a customer balance.                                                            |
| `new_z_balance`  | `POST /rewards/{id}/redeem/` response, `POST /coupons/{id}/validate/` response | The **customer's** Z\$ balance after the operation. Stringified Decimal with 6 places (e.g. `"2418.400000"`).                                      |
| `z_tokens_used`  | `POST /rewards/{id}/redeem/` response, `POST /coupons/{id}/validate/` response | Z\$ tokens consumed by **this specific operation**. `"0.000000"` (zero-as-string) for points-only redemptions.                                     |

### Coupon identifiers

| Field         | Format                                     | When to use                                                                                                                 |
| ------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `id`          | KSUID (e.g. `16a33f27fbbc1801d63d56d2027`) | When you obtained it from a prior API call (redeem response or `/coupons/issued/` list). Always works.                      |
| `coupon_code` | `CZ-XXXXXXXX` or `CP-XXXXXXXX` (legacy)    | When you have a printed code from the customer's receipt. The `validate` endpoint accepts this directly (case-insensitive). |

See [Coupon Lifecycle § Two identifiers, one coupon](/guides/coupon-lifecycle#two-identifiers-one-coupon) for the full rationale.

## Error Handling

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

```json theme={null}
{
  "type": "https://api.zupy.com/errors/{error-type}",
  "title": "Human-Readable Title",
  "status": 400,
  "detail": "Specific explanation of what went wrong"
}
```

Common errors you may encounter:

| Status | Type                      | When It Occurs                                 |
| ------ | ------------------------- | ---------------------------------------------- |
| 401    | `authentication-required` | Missing or invalid `X-API-Key` header          |
| 403    | `permission-denied`       | Read-only key attempting a write operation     |
| 429    | `rate-limit-exceeded`     | Too many requests — check `Retry-After` header |

<Accordion title="Retry logic for rate limits">
  When you receive a `429` response, wait for the `Retry-After` header duration before retrying:

  <CodeGroup>
    ```python Python theme={null}
    import time
    import requests

    def api_request(method, url, **kwargs):
        """Make an API request with automatic retry on rate limit."""
        max_retries = 3
        for attempt in range(max_retries):
            response = requests.request(method, url, **kwargs)

            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 30))
                print(f"Rate limited. Retrying in {retry_after}s...")
                time.sleep(retry_after)
                continue

            return response

        raise Exception("Max retries exceeded")
    ```

    ```javascript JavaScript theme={null}
    async function apiRequest(method, url, options = {}) {
      const maxRetries = 3;
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url, { method, ...options });

        if (response.status === 429) {
          const retryAfter = parseInt(
            response.headers.get("Retry-After") || "30"
          );
          console.log(`Rate limited. Retrying in ${retryAfter}s...`);
          await new Promise((r) => setTimeout(r, retryAfter * 1000));
          continue;
        }

        return response;
      }
      throw new Error("Max retries exceeded");
    }
    ```
  </CodeGroup>
</Accordion>

## Next Steps

<Card title="Authentication" icon="key" href="/authentication">
  Learn about access levels, rate limits, OTP, and security best practices
</Card>

<Card title="Webhook Setup" icon="webhook" href="/guides/webhook-setup">
  Configure webhooks for automatic order processing and customer enrollment
</Card>

<Card title="API Reference" icon="code" href="/api-reference">
  Browse all endpoints with request/response schemas
</Card>

<Card title="Partner Onboarding" icon="clipboard-check" href="/guides/partner-onboarding">
  Complete the onboarding checklist for production deployment
</Card>
