Skip to main content
Send order and customer data to Zupy for automatic loyalty processing. Webhooks handle customer enrollment, points calculation, and deduplication — all asynchronously.
Prerequisites: Your API key (zupy_pk_*) and integration slug (e.g., repediu). See Getting Started if you don’t have these yet.

How It Works

Partners send order data to Zupy via a single webhook endpoint. Zupy processes each order asynchronously:
  1. Receive — Zupy validates your API key, stores the raw payload, and returns immediately
  2. Deduplicate — SHA-256 hash of the request body prevents double-processing
  3. Process — A background worker identifies customers, calculates points, and enrolls new users
  4. Enroll — Customers not found by phone/email/CPF are automatically created and enrolled in the restaurant’s loyalty program

Endpoint

POST /api/v2/webhooks/integrations/{partner}/
  • {partner} is your integration slug (e.g., repediu, saipos, goomer)

Headers

HeaderRequiredValue
X-API-KeyYesYour partner API key (zupy_pk_*)
Content-TypeYesapplication/json

Payload Format

The webhook accepts any JSON payload. Zupy stores the raw payload and processes it using a partner-specific adapter.
Each partner has their own payload format. The adapter maps your fields to Zupy’s internal data model. Below is the Repediu format as a reference — your format may differ.

Example: Repediu Payload (JSON Array of Orders)

Send a JSON array of sale objects in a single POST:
[
  {
    "cliente": "Maria Santos",
    "Celular": "+55 11 98765-4321",
    "Email": null,
    "CpfCnpj": null,
    "Valor": "89.90",
    "id_venda": 123456789,
    "DataVenda": "2026-03-22T14:30:00.000Z",
    "loja_cnpj": "12.345.678/0001-90",
    "loja_nome": "Pizzaria Exemplo"
  },
  {
    "cliente": "João Silva",
    "Celular": "+55 19 99228-5367",
    "Email": null,
    "CpfCnpj": null,
    "Valor": "62.00",
    "id_venda": 247346205,
    "DataVenda": "2026-03-22T15:10:00.000Z",
    "loja_cnpj": "12.345.678/0001-90",
    "loja_nome": "Pizzaria Exemplo"
  }
]

Repediu Field Reference

FieldTypeRequiredDescription
clientestringYesCustomer name
CelularstringNoPhone number (any format — Zupy normalizes to E.164)
EmailstringNoCustomer email
CpfCnpjstringNoCPF or CNPJ
ValorstringYesOrder total in BRL (e.g., "89.90")
id_vendaintegerYesUnique sale ID (used for order-level deduplication)
DataVendastringYesSale timestamp (ISO 8601)
loja_cnpjstringYesRestaurant CNPJ
loja_nomestringYesRestaurant name
At least one customer identifier (Celular, Email, or CpfCnpj) must be present. Orders without any identifier are skipped during processing.

Sending Webhooks

curl -X POST "https://api.zupy.com/api/v2/webhooks/integrations/repediu/" \
  -H "X-API-Key: zupy_pk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "cliente": "Maria Santos",
      "Celular": "+55 11 98765-4321",
      "Email": null,
      "CpfCnpj": null,
      "Valor": "89.90",
      "id_venda": 123456789,
      "DataVenda": "2026-03-22T14:30:00.000Z",
      "loja_cnpj": "12.345.678/0001-90",
      "loja_nome": "Pizzaria Exemplo"
    }
  ]'

Response Format

New Webhook (200)

{
  "data": {
    "status": "received",
    "id": "2bxRKmpWJY8lHHqsGfsQQtwCDef"
  },
  "meta": {}
}

Duplicate Webhook (200)

{
  "data": {
    "status": "duplicate",
    "id": "2bxRKmpWJY8lHHqsGfsQQtwCDef"
  },
  "meta": {}
}
Duplicates return HTTP 200 — not an error. Your integration does not need special error handling for re-sent payloads.

Idempotency

Zupy computes a SHA-256 hash of the raw request body for every webhook. If the same payload is sent twice:
  • The second request returns "status": "duplicate" with HTTP 200
  • No duplicate processing occurs
  • The original id is returned so you can reference it
This means you can safely retry failed sends without worrying about double-processing.
Webhook-level deduplication (SHA-256):
  • Every incoming request body is hashed with SHA-256
  • The hash is stored as idempotency_key in the WebhookEvent record
  • If a matching hash already exists, the request is treated as a duplicate
Order-level deduplication (per adapter):
  • Each order’s unique ID (e.g., Repediu’s id_venda) is checked before awarding points
  • If points were already awarded for that order, the order is skipped silently
  • This protects against reprocessing if the same orders appear in different batches

Async Processing

Webhooks are processed asynchronously:
1

Immediate Response

The webhook endpoint validates your API key, stores the raw payload, and returns HTTP 200 immediately. No processing happens at this stage.
2

Background Queue

A Celery worker picks up the stored event and processes it using the partner-specific adapter. This typically happens within seconds.
3

Per-Order Processing

Each order in the batch is processed independently:
  • Extract customer identifier (phone > email > CPF)
  • Find or create the customer in Zupy
  • Enroll in the restaurant’s loyalty program (if new)
  • Calculate and award points based on the order value
One bad order does not block the batch. Each order is processed independently. If one order fails (e.g., missing identifier), the rest continue normally.

Batch Processing

Send multiple orders in a single request. Each order is processed independently:
[
  {"cliente": "Maria Santos", "Celular": "+55 11 98765-4321", "Valor": "89.90", "id_venda": 100, "DataVenda": "2026-03-22T14:30:00.000Z", "loja_cnpj": "12.345.678/0001-90", "loja_nome": "Pizzaria Exemplo", "Email": null, "CpfCnpj": null},
  {"cliente": "João Silva", "Celular": "+55 19 99228-5367", "Valor": "62.00", "id_venda": 101, "DataVenda": "2026-03-22T15:10:00.000Z", "loja_cnpj": "12.345.678/0001-90", "loja_nome": "Pizzaria Exemplo", "Email": null, "CpfCnpj": null},
  {"cliente": "Não Informado", "Celular": null, "Valor": "45.00", "id_venda": 102, "DataVenda": "2026-03-22T16:00:00.000Z", "loja_cnpj": "12.345.678/0001-90", "loja_nome": "Pizzaria Exemplo", "Email": null, "CpfCnpj": null}
]
In this example:
  • Order 100 (Maria): Processed — customer found/created, points awarded
  • Order 101 (João): Processed — customer found/created, points awarded
  • Order 102 (Não Informado): Skipped — no phone, email, or CPF to identify the customer

Error Handling

StatusTypeWhenWhat to Do
200Webhook received or duplicateSuccess — no action needed
400validation-errorInvalid JSON bodyCheck your payload is valid JSON
401authentication-requiredMissing or invalid X-API-KeyVerify your API key
429rate-limit-exceededToo many requestsWait for Retry-After header, then retry

Error Response Format (RFC 7807)

{
  "type": "https://api.zupy.com/errors/validation-error",
  "title": "Bad Request",
  "status": 400,
  "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)"
}
import time
import requests

def send_webhook(orders, max_retries=3):
    """Send webhook with automatic retry on rate limit."""
    url = f"{BASE_URL}/webhooks/integrations/repediu/"

    for attempt in range(max_retries):
        response = requests.post(url, json=orders, headers=HEADERS)

        if response.status_code == 200:
            return response.json()

        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

        # Non-retryable error
        error = response.json()
        raise Exception(f"Webhook error {error['status']}: {error['detail']}")

    raise Exception("Max retries exceeded")

Rate Limits

Webhook requests are subject to two rate-limiting layers: Gateway level (per IP address, applied by APISIX):
LimitValue
Rate100 requests/minute
Burst150 requests
Consumer level (per API key, based on your tier):
TierRequests/min
Free60
Standard300
Enterprise3,000
Both limits apply simultaneously. The lower of the two is your effective limit.
Best practice: Send orders in batches (arrays) rather than one order per request. A single batch counts as one request regardless of how many orders it contains.
When rate-limited, you receive a 429 response with X-RateLimit-Remaining and Retry-After headers indicating your remaining quota and how many seconds to wait.

Best Practices

Send multiple orders in a single array rather than one request per order. This reduces API calls and stays well within rate limits. A batch of 100 orders is a single request.
Webhooks are idempotent. If a request times out or fails, retry with the same payload. Zupy detects duplicates automatically — no risk of double-processing.
Orders without any identifier (phone, email, or CPF) are skipped during processing. Ensure at least one identifier is present whenever possible.
Include a unique order ID in your payload (e.g., Repediu’s id_venda). This enables order-level deduplication and makes debugging easier.
The webhook returns immediately. Processing happens asynchronously in the background (typically within seconds). Don’t poll for results — use the Customer API to verify data after a short delay.

Next Steps

OTP Flow

Learn about customer identity verification for sensitive actions

Partner Onboarding

Complete the onboarding checklist for production deployment

API Reference

Browse all endpoints with request/response schemas