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:- Receive — Zupy validates your API key, stores the raw payload, and returns immediately
- Deduplicate — SHA-256 hash of the request body prevents double-processing
- Process — A background worker identifies customers, calculates points, and enrolls new users
- Enroll — Customers not found by phone/email/CPF are automatically created and enrolled in the restaurant’s loyalty program
Endpoint
{partner}is your integration slug (e.g.,repediu,saipos,goomer)
Headers
| Header | Required | Value |
|---|---|---|
X-API-Key | Yes | Your partner API key (zupy_pk_*) |
Content-Type | Yes | application/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:Repediu Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
cliente | string | Yes | Customer name |
Celular | string | No | Phone number (any format — Zupy normalizes to E.164) |
Email | string | No | Customer email |
CpfCnpj | string | No | CPF or CNPJ |
Valor | string | Yes | Order total in BRL (e.g., "89.90") |
id_venda | integer | Yes | Unique sale ID (used for order-level deduplication) |
DataVenda | string | Yes | Sale timestamp (ISO 8601) |
loja_cnpj | string | Yes | Restaurant CNPJ |
loja_nome | string | Yes | Restaurant name |
Sending Webhooks
Response Format
New Webhook (200)
Duplicate Webhook (200)
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
idis returned so you can reference it
How idempotency works internally
How idempotency works internally
Webhook-level deduplication (SHA-256):
- Every incoming request body is hashed with SHA-256
- The hash is stored as
idempotency_keyin theWebhookEventrecord - If a matching hash already exists, the request is treated as a duplicate
- 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:Immediate Response
The webhook endpoint validates your API key, stores the raw payload, and returns HTTP 200 immediately. No processing happens at this stage.
Background Queue
A Celery worker picks up the stored event and processes it using the partner-specific adapter. This typically happens within seconds.
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:- 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
| Status | Type | When | What to Do |
|---|---|---|---|
| 200 | — | Webhook received or duplicate | Success — no action needed |
| 400 | validation-error | Invalid JSON body | Check your payload is valid JSON |
| 401 | authentication-required | Missing or invalid X-API-Key | Verify your API key |
| 429 | rate-limit-exceeded | Too many requests | Wait for Retry-After header, then retry |
Error Response Format (RFC 7807)
Handling errors in your code
Handling errors in your code
Rate Limits
Webhook requests are subject to two rate-limiting layers: Gateway level (per IP address, applied by APISIX):| Limit | Value |
|---|---|
| Rate | 100 requests/minute |
| Burst | 150 requests |
| Tier | Requests/min |
|---|---|
| Free | 60 |
| Standard | 300 |
| Enterprise | 3,000 |
429 response with X-RateLimit-Remaining and Retry-After headers indicating your remaining quota and how many seconds to wait.
Best Practices
Batch your orders
Batch your orders
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.
Retry safely
Retry safely
Webhooks are idempotent. If a request times out or fails, retry with the same payload. Zupy detects duplicates automatically — no risk of double-processing.
Include customer identifiers
Include customer identifiers
Orders without any identifier (phone, email, or CPF) are skipped during processing. Ensure at least one identifier is present whenever possible.
Use your unique sale ID
Use your unique sale ID
Include a unique order ID in your payload (e.g., Repediu’s
id_venda). This enables order-level deduplication and makes debugging easier.Don't wait for processing
Don't wait for processing
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