Card Payments
Accept Visa, Mastercard, American Express, Elo, Hipercard, and other major schemes through a Demo Pay-hosted payment page — no card data ever touches your servers, 3D Secure handled automatically.
#Flow at a glance
1. POST /v1/payments ──────▶ Demo Pay returns transactionId + status WAITING_PAYMENT
2. GET /v1/payments/{id} ───▶ Within ~1s, cardRedirectUrl is populated
3. Redirect (or open in iframe) the customer to cardRedirectUrl
4. Customer enters card details + completes 3DS challenge on Demo Pay's secure page
5. Customer is bounced back to your returnUrl (success or failure)
6. Webhook payment.completed (or payment.failed) confirms the outcome — trust the webhook, not the redirectWhy a redirect? It keeps cardholder data off your infrastructure entirely. Your application — backend or frontend — never sees a PAN, CVV, or expiry. Demo Pay's processor handles 3DS, tokenisation, and fraud screening on our side.
#1. Create the payment
Endpoint POST /v1/payments
curl -X POST https://demo.zentry.cloud/v1/payments \
-H "apikey: $DEMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 8990,
"currency": "EUR",
"paymentMethods": ["CREDIT_CARD"],
"customerName": "João Costa",
"customerEmail": "joao@example.com",
"metadata": {
"orderId": "ORD-9821",
"returnUrl": "https://shop.example.com/orders/9821/return"
},
"idempotencyKey": "ORD-9821"
}'| Field | Notes |
|---|---|
amount | Smallest unit (cents). 8990 = € 89,90. |
currency | BRL, EUR, or USD (additional currencies on request). |
paymentMethods | ["CREDIT_CARD"] |
metadata.returnUrl | Where to send the customer after they finish (success or failure). |
customerEmail | Strongly recommended — used for receipts, fraud signals, and chargeback dispute proof. |
Response 201 Created
{
"id": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"status": "WAITING_PAYMENT",
"amount": 8990,
"currency": "EUR",
"paymentMethods": ["CREDIT_CARD"],
"createdAt": "2026-04-25T15:50:11.000Z"
}#2. Redirect the customer
Fetch the transaction once cardRedirectUrl is populated (typically <1s):
curl https://demo.zentry.cloud/v1/payments/b2c3d4e5-f6a7-4890-9bcd-ef0123456789 \
-H "apikey: $DEMO_API_KEY"{
"id": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"status": "WAITING_PAYMENT",
"amount": 8990,
"currency": "EUR",
"cardRedirectUrl": "https://checkout.demo.zentry.cloud/c/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"createdAt": "2026-04-25T15:50:11.000Z"
}Send the customer to cardRedirectUrl:
<a href="{{ cardRedirectUrl }}">Pay with card</a>Or do a server-side 302 Found:
res.redirect(303, transaction.cardRedirectUrl);After the customer completes the flow (or aborts), they bounce back to the metadata.returnUrl you supplied — with ?transactionId=...&status=... appended. Do not trust those query parameters. They're informational only — wait for the webhook before fulfilling.
#3. Confirm via webhook
{
"event": "payment.completed",
"data": {
"transactionId": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"amount": 8990,
"status": "APPROVED",
"previousStatus": "PROCESSING",
"paidWith": "CREDIT_CARD",
"providerFee": 280,
"platformFee": 90,
"netAmount": 8620,
"occurredAt": "2026-04-25T15:52:34.000Z"
}
}For card, the success status is APPROVED (not PAID). Both events arrive as payment.completed so a single handler covers both.
Card declines arrive as payment.failed with data.status ∈ REFUSED, EXPIRED, CANCELLED. Common decline reasons (when surfaced by the issuer) are echoed in data.declineReason.
#Lifecycle
WAITING_PAYMENT ──▶ PROCESSING ──▶ APPROVED ✓ fulfill order
──▶ REFUSED issuer declined
──▶ CANCELLED customer abandoned
──▶ EXPIRED redirect link aged out (24h)After settlement (D+1 to D+30 depending on scheme and your contract), you may also see:
APPROVED ──▶ CHARGEBACK issuer raised a chargeback
APPROVED ──▶ DISPUTE cardholder opened a dispute
APPROVED ──▶ REFUNDED you (or Demo Pay) issued a refundEach one fires a webhook so you can sync the order state.
#Test cards
In sandbox, the redirect page accepts these deterministic numbers (any future expiry, any CVV):
| Number | Outcome |
|---|---|
4111 1111 1111 1111 | Always APPROVED |
4000 0000 0000 0002 | Always REFUSED — generic decline |
4000 0000 0000 0069 | Always REFUSED — expired card |
4000 0000 0000 9995 | Always REFUSED — insufficient funds |
4000 0000 0000 3220 | Triggers 3DS challenge then APPROVED |
See sandbox for the full list.
#FAQ
Q: Can I keep the customer on my domain? A: Phase 2 we'll ship an embeddable widget (iframe + tokeniser) that keeps your branding while still keeping cardholder data off your infrastructure. For now: hosted redirect.
Q: Do you support saved cards? A: Yes, but it requires your side to hold its own card-data-security compliance attestation. Email suporte@demo.zentry.cloud to enable the Vault API.
Q: Which 3DS version is used? A: 3DS 2.x with frictionless flow when the issuer permits. Strong Customer Authentication (SCA) is mandatory for EUR — this is enforced server-side.
Q: Refunds?
A: Supported — POST /v1/payments/{id}/refund, full or partial. Card refunds settle through Stripe. See the API reference.
Q: My customer was charged but I never got a webhook.
A: Check GET /v1/webhooks/deliveries — likely we tried and your endpoint returned a non-2xx. We retry up to 10 times. After that, contact support to replay.
#Next
→ MB WAY · Multibanco · API Reference