Skip to main content

Zupy is an OpenDelivery 2.0-compliant participant

Zupy is OpenDelivery 2.0 compliant for the Customer (Loyalty) capability. If your platform already speaks OpenDelivery, you can integrate with Zupy loyalty using the standard — discover us via our manifest, authenticate with client_credentials, and receive signed events. No Zupy-proprietary contract required.

What OpenDelivery is

OpenDelivery is an open Brazilian standard that defines how ordering platforms, delivery logistics, and merchant software interoperate. Zupy participates as a Customer capability provider (loyalty programs, accounts, transactions, rewards, coupons, and redemptions), so any OpenDelivery-aware ordering application can read a customer’s loyalty state and issue redemptions through a documented, non-proprietary interface.

1. Discovery — find us via the manifest

Zupy publishes a public Discovery manifest at the well-known path. It advertises our protocol version, authentication models, and the exact operations we support — no need to coordinate out-of-band.
Discovery request
Manifest (live)
appId is Zupy’s single, stable participant identity — the same value appears in every webhook we send (X-App-Id). It is not per-partner.

2. Authentication — two models, both standard

OpenDelivery allows a provider to declare more than one clientIdGeneration model. Zupy declares both, so you pick the one that fits your integration:

by_app (recommended)

One credential for your application, valid across all merchants linked to it. Best for platforms that integrate many Zupy merchants. Each merchant is activated by the merchant themselves via a toggle in their Zupy dashboard. Provisioned by the Zupy partnerships team.

by_merchant (self-serve)

One credential per merchant, provisioned by the merchant from their own dashboard (/integrations). Best when a single merchant wants to connect an app Zupy hasn’t formally partnered with yet. No partnership required.
Both models use the client_credentials OAuth2 grant to obtain a bearer token, and both are exposed identically at https://api.zupy.com/v2.

Selecting a merchant with by_app

A by_app token can reach multiple merchants, so each request must name which merchant it targets — either with the X-App-MerchantId header or the ?storeId= query parameter (equivalent; same value). Zupy only serves data for merchants whose toggle links your application — anything else is denied.
string (header)
The merchant identifier as your application knows it (the OpenDelivery merchantId). Zupy maps it to the merchant internally.
string (query parameter)
Same value and semantics as the header, as a query parameter — the store filter proposed for the standard in opendelivery-v2#24 (precedent: Order.storeId / listOrders). Use whichever fits your client.
Exactly one merchant must be selected per request: with neither header nor storeId the request is denied (403), and if both are sent with different values the request is denied as ambiguous — Zupy never silently picks one.

3. Merchant identity — two-sided by design

OpenDelivery models a merchant with two identifiers, and Zupy stores both: Call GET /v2/merchants with your token to list every merchant your credential can reach, each returned with both identifiers.

4. Supported operations

The Customer/Loyalty operations are live at https://api.zupy.com/v2, at the exact spec paths (a trailing slash is tolerated). List operations are paginated ({ items, page, pageSize, total }).
Resolving a customer is order-driven: send a phone/email to listCustomers (customerIdentifierType + customerIdentifierValue) to get the customerId, then read /customers/{customerId}/loyalty-accounts. Registration itself happens automatically when you report an order (accrual) — see §6.
Error shape. Errors to an OpenDelivery partner are returned as { "code": "...", "message": "...", "details": [...] } (the OpenDelivery ErrorResponse; details is an array of strings). HTTP codes: 400 (validation), 401 (auth), 404 (not found), 409 (conflict), 429 (rate limit), 500.
Redemption is asynchronous: POST /loyalty/redemptions returns 202 with a correlationId. A 409 means the account can’t afford the reward. Redemptions are single-unit (quantity must be 1).

5. Webhooks — signed OpenDelivery events

When loyalty state changes, Zupy sends a signed event to your registered webhook URL. Events use the OpenDelivery envelope (id, eventType, occurredAt, accountId, customerId, programId, payload).
Why only four events? The OpenDelivery LoyaltyEventType enum is closed — the standard defines exactly five values, and Zupy emits the four that apply to its loyalty model (loyalty.cashback.earned is not used; Zupy has no cashback). Zupy never emits an eventType outside the spec enum.Zupy’s native webhook surface carries nine event types. The mapping:

Extension events (proposed upstream)

The three extension eventTypes above announce transitions the OpenDelivery schema already models (LoyaltyTransactionType.adjust; LoyaltyCoupon.statusexpired/cancelled) but the event enum does not cover — including the points reversal triggered by the standard’s own order.cancelled flow. We proposed them upstream in Abrasel-Nacional/opendelivery-v2#25; until adopted, they are a vendor extension. They use the same envelope, headers, and signature as the standard events, but are never delivered by default: a subscription with empty webhook_events (= “all”) receives only the four spec-enum events. To receive an extension event, list it explicitly in your webhook_events (e.g. ["loyalty.points.earned", "loyalty.points.adjusted"]). Every webhook carries these headers:
string
Zupy’s participant appId — identical across all our webhooks.
string
The merchant this event belongs to (the merchantId you assigned).
string
Lowercase hex HMAC-SHA256 of the raw request body, signed with your webhook secret — no sha256= prefix. Verify it before trusting the payload.
Always verify X-App-Signature against the raw body bytes (not a re-serialized JSON) using your webhook secret. Reject any request whose signature does not match.

6. Inbound — accrue points from orders

If your platform owns the orders, you can have Zupy award loyalty points automatically when a sale happens — and reverse them if that sale is cancelled. Zupy is the loyalty host: you send two signals, we do the rest (we never track your order lifecycle — when to send each signal is entirely your call). Send an OpenDelivery CustomerEventEnvelope to:
Accrue / reverse
Both are authenticated with the same OAuth2 credential you use for reads (by_app or by_merchant) — no separate scheme. A minimal envelope:
order.created
object
{ type, value }type is phone or email (a document/CPF-only customer is skipped, no lookup). The customer is enrolled automatically if new.
number
Order total in cents (OpenDelivery convention). Zupy converts to the base currency before applying the points rate.
Idempotent by orderId. Re-sending the same order.created never double-credits; re-sending order.cancelled never double-reverses. A cancel for an order that was never credited is a no-op.
Accrual is opt-in per merchant. A merchant must have an OpenDelivery accrual rate configured; until then, order.created is accepted (204) but awards nothing. When points are awarded, Zupy emits the loyalty.points.earned webhook back to you (§5).
A response of 204 means accepted. Unknown OpenDelivery event types are rejected (422); other known-but-non-loyalty events (e.g. order.delivered) are acknowledged (202) and ignored.

Reference

OpenDelivery spec

The normative OpenDelivery 2.0 specification.

Partner Onboarding

What you receive and how to go live.

Outbound Webhooks

Webhook delivery, retries, and signature verification in depth.

Get a credential

Contact the Zupy partnerships team for a by_app credential.