Consent flows
Authorize, track, and reuse wallet consent for NuPay and PayPal Wallet.
On this page
Consent is the customer authorization that a supported wallet requires before DEUNA can retrieve payment options, store an account, or complete a purchase. DEUNA exposes one public consent contract while adapting the provider-specific flow behind it.
Supported providers and behavior#
| Provider | Who uses consent | Approval experience | Status source | Reuse |
|---|---|---|---|---|
| NuPay | Guest or authenticated customer, when enabled for the connection | The customer approves in the Nubank experience | Provider webhook; GET /merchants/orders/{order_token}/consent returns the latest DEUNA state | Bound to the order and customer identity; expired authorization can be refreshed when supported |
| PayPal Wallet | Authenticated customer using PayPal Vault | Redirect the customer to the returned redirect_url | DEUNA polls PayPal when the consent becomes eligible for verification | The approved PayPal account is exposed as a stored payment method for later purchases |
Other payment methods may use redirects, OTP, or 3DS, but they do not use this consent API. Do not call the consent endpoints unless the selected wallet connection is configured for consent.
Lifecycle#
The normal state progression is pending to success. Treat failed and expired as terminal. Treat any unrecognized or denied state as non-successful and do not submit the purchase with it.
Before you start#
- Configure the NuPay or PayPal Wallet connection for the correct store and environment.
- Create an order and retain its
order_token. - For reusable PayPal accounts, authenticate the customer and send the user bearer token on consent requests.
- Read the payment-method response. For PayPal Vault,
authorization.required: trueandauthorization.flow: "consent"indicate that the customer needs consent. Use the returned polling fields instead of hard-coding a cadence. - Configure
order.webhook_urls.notify_orderso your backend receives consent state changes.
Create consent#
POST /merchants/orders/{order_token}/consent
The public API Gateway route intentionally does not include the internal /api/v1 service prefix.
curl --request POST \
--url 'https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent' \
--header 'X-API-KEY: YOUR_PRIVATE_API_KEY' \
--header 'X-Store-Code: all' \
--header 'Authorization: Bearer USER_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"payment_method": "wallet",
"payment_method_id": "MERCHANT_PAYMENT_METHOD_ID",
"identity_document": "58188896454",
"identity_document_type": "CPF"
}'const response = await fetch("https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent", {
method: "POST",
headers: {
"X-API-KEY": "YOUR_PRIVATE_API_KEY",
"X-Store-Code": "all",
"Authorization": "Bearer USER_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
"payment_method": "wallet",
"payment_method_id": "MERCHANT_PAYMENT_METHOD_ID",
"identity_document": "58188896454",
"identity_document_type": "CPF"
})
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();import requests
response = requests.request(
"POST",
"https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent",
headers={
"X-API-KEY": "YOUR_PRIVATE_API_KEY",
"X-Store-Code": "all",
"Authorization": "Bearer USER_TOKEN",
"Content-Type": "application/json"
},
json={
"payment_method": "wallet",
"payment_method_id": "MERCHANT_PAYMENT_METHOD_ID",
"identity_document": "58188896454",
"identity_document_type": "CPF"
},
)
response.raise_for_status()
data = response.json()<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent",
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-API-KEY: YOUR_PRIVATE_API_KEY",
"X-Store-Code: all",
"Authorization: Bearer USER_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"payment_method" => "wallet",
"payment_method_id" => "MERCHANT_PAYMENT_METHOD_ID",
"identity_document" => "58188896454",
"identity_document_type" => "CPF"
])
]);
$response = curl_exec($curl);
curl_close($curl);import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent"))
.header("X-API-KEY", "YOUR_PRIVATE_API_KEY")
.header("X-Store-Code", "all")
.header("Authorization", "Bearer USER_TOKEN")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"payment_method\": \"wallet\",\n \"payment_method_id\": \"MERCHANT_PAYMENT_METHOD_ID\",\n \"identity_document\": \"58188896454\",\n \"identity_document_type\": \"CPF\"\n }"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
}
}package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
request, err := http.NewRequest("POST", "https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent", strings.NewReader("{\n \"payment_method\": \"wallet\",\n \"payment_method_id\": \"MERCHANT_PAYMENT_METHOD_ID\",\n \"identity_document\": \"58188896454\",\n \"identity_document_type\": \"CPF\"\n }"))
if err != nil { panic(err) }
request.Header.Set("X-API-KEY", "YOUR_PRIVATE_API_KEY")
request.Header.Set("X-Store-Code", "all")
request.Header.Set("Authorization", "Bearer USER_TOKEN")
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
fmt.Println(string(body))
}| Field | Required | Description |
|---|---|---|
payment_method | Recommended | Use wallet. If omitted, DEUNA uses the payment method on the order. |
payment_method_id | Recommended | Connection identifier returned by the order payment-method response. It avoids ambiguity when more than one wallet connection is enabled. |
identity_document | NuPay | Customer identity document. If omitted, DEUNA can derive it from the order address when present. |
identity_document_type | NuPay | Document type, such as CPF. If omitted, DEUNA can derive it from the order address when present. |
For an authenticated user, the bearer token lets DEUNA associate the successful consent with that user and connection. A guest consent remains order-bound.
Response envelope
{
"id": "1625a32a-df4c-4d9b-aec1-4510b3625865",
"type": "transaction.authentication.pending",
"created": "1740608931",
"data": {
"request_id": "req_01JQ7X",
"order": {
"order_token": "7e975d44-a061-4d70-af0f-673f6ee56445",
"transaction_id": "merchant-order-1042",
"external_transaction_id": ""
},
"consent": {
"id": "6fe9a045-9a46-4b25-8b60-d5a9586494c5",
"status": "pending",
"expires_at": "2026-10-02T18:30:00Z",
"authorization_id": "provider-authorization-id",
"redirect_url": "https://provider.example/approve"
}
}
}Store data.consent.id, but drive the UI from data.consent.status. Open redirect_url only when it is present. NuPay can require approval in the provider app without returning a browser redirect.
Complete provider approval#
For PayPal Wallet, send the customer to data.consent.redirect_url. The provider returns through DEUNA's consent redirect route and DEUNA verifies the final provider state. If the customer cancels, the consent is marked as failed and must not be used for purchase.
For NuPay, instruct the customer to approve the request in the Nubank experience. The provider webhook updates the consent stored by DEUNA.
Never construct provider approval URLs or DEUNA redirect URLs yourself. Use the URLs in the response.
Read the latest status#
GET /merchants/orders/{order_token}/consent
curl --request GET \
--url 'https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent?payment_method=wallet&payment_method_id=MERCHANT_PAYMENT_METHOD_ID' \
--header 'X-API-KEY: YOUR_PRIVATE_API_KEY' \
--header 'X-Store-Code: all' \
--header 'Authorization: Bearer USER_TOKEN'const response = await fetch("https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent?payment_method=wallet&payment_method_id=MERCHANT_PAYMENT_METHOD_ID", {
method: "GET",
headers: {
"X-API-KEY": "YOUR_PRIVATE_API_KEY",
"X-Store-Code": "all",
"Authorization": "Bearer USER_TOKEN"
}
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();import requests
response = requests.request(
"GET",
"https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent?payment_method=wallet&payment_method_id=MERCHANT_PAYMENT_METHOD_ID",
headers={
"X-API-KEY": "YOUR_PRIVATE_API_KEY",
"X-Store-Code": "all",
"Authorization": "Bearer USER_TOKEN"
},
)
response.raise_for_status()
data = response.json()<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent?payment_method=wallet&payment_method_id=MERCHANT_PAYMENT_METHOD_ID",
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-API-KEY: YOUR_PRIVATE_API_KEY",
"X-Store-Code: all",
"Authorization: Bearer USER_TOKEN"
]
]);
$response = curl_exec($curl);
curl_close($curl);import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent?payment_method=wallet&payment_method_id=MERCHANT_PAYMENT_METHOD_ID"))
.header("X-API-KEY", "YOUR_PRIVATE_API_KEY")
.header("X-Store-Code", "all")
.header("Authorization", "Bearer USER_TOKEN")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
}
}package main
import (
"fmt"
"io"
"net/http"
)
func main() {
request, err := http.NewRequest("GET", "https://api.sandbox.deuna.io/merchants/orders/ORDER_TOKEN/consent?payment_method=wallet&payment_method_id=MERCHANT_PAYMENT_METHOD_ID", nil)
if err != nil { panic(err) }
request.Header.Set("X-API-KEY", "YOUR_PRIVATE_API_KEY")
request.Header.Set("X-Store-Code", "all")
request.Header.Set("Authorization", "Bearer USER_TOKEN")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
fmt.Println(string(body))
}Use authorization.start_polling_after_in_seconds and authorization.polling_interval_in_seconds from the payment-method response when supplied. Stop polling as soon as the status is no longer pending, when expires_at is reached, or when the customer leaves the flow.
The GET response uses the same envelope as create consent. Always inspect both data.consent.status and data.error; a business-level terminal outcome can be represented in the envelope even when the HTTP request itself succeeded.
Handle consent webhooks#
DEUNA sends consent updates to the order's webhook_urls.notify_order URL. Handle these event types:
| Event type | Meaning |
|---|---|
transaction.authentication.pending | Authorization was created and still needs customer or provider action. |
transaction.authentication.updated | Authorization succeeded; the consent can be used. |
transaction.authentication.failed | Authorization failed or the customer canceled. |
transaction.authentication.expired | The consent expired before it could be used. |
Respond with 2xx quickly, deduplicate by the event id, and then fetch the latest consent if your processing depends on current state. Webhook delivery and GET polling complement one another; your checkout should tolerate either arriving first. See Webhooks.
Use the approved consent#
NuPay
After success, request the order's payment methods and NuPay installment options. DEUNA uses the valid consent authorization while retrieving those options and while processing the wallet purchase. If the authorization has expired and the provider supports refresh, DEUNA attempts to refresh it.
PayPal Wallet
For an authenticated user, the approved PayPal account appears in stored_payment_methods. Send that stored method identifier as the purchase payment_method; DEUNA verifies that the consent belongs to the same user, merchant, and PayPal connection before using it.
Removing the stored wallet payment method also removes the associated reusable consent.
Remove a reusable PayPal account
DELETE /users/payment-methods/{payment_method_id}/tokens/{payment_method}
Use payment_method_id for the PayPal connection identifier and payment_method for the stored method identifier returned in stored_payment_methods. This user-authenticated endpoint removes the wallet account at the provider and deletes the corresponding reusable consent in DEUNA.
curl --request DELETE \
--url 'https://api.sandbox.deuna.io/users/payment-methods/MERCHANT_PAYMENT_METHOD_ID/tokens/STORED_PAYMENT_METHOD_ID' \
--header 'Authorization: Bearer USER_TOKEN' \
--header 'X-Merchant-ID: MERCHANT_ID'const response = await fetch("https://api.sandbox.deuna.io/users/payment-methods/MERCHANT_PAYMENT_METHOD_ID/tokens/STORED_PAYMENT_METHOD_ID", {
method: "DELETE",
headers: {
"Authorization": "Bearer USER_TOKEN",
"X-Merchant-ID": "MERCHANT_ID"
}
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();import requests
response = requests.request(
"DELETE",
"https://api.sandbox.deuna.io/users/payment-methods/MERCHANT_PAYMENT_METHOD_ID/tokens/STORED_PAYMENT_METHOD_ID",
headers={
"Authorization": "Bearer USER_TOKEN",
"X-Merchant-ID": "MERCHANT_ID"
},
)
response.raise_for_status()
data = response.json()<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.deuna.io/users/payment-methods/MERCHANT_PAYMENT_METHOD_ID/tokens/STORED_PAYMENT_METHOD_ID",
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer USER_TOKEN",
"X-Merchant-ID: MERCHANT_ID"
]
]);
$response = curl_exec($curl);
curl_close($curl);import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.deuna.io/users/payment-methods/MERCHANT_PAYMENT_METHOD_ID/tokens/STORED_PAYMENT_METHOD_ID"))
.header("Authorization", "Bearer USER_TOKEN")
.header("X-Merchant-ID", "MERCHANT_ID")
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
}
}package main
import (
"fmt"
"io"
"net/http"
)
func main() {
request, err := http.NewRequest("DELETE", "https://api.sandbox.deuna.io/users/payment-methods/MERCHANT_PAYMENT_METHOD_ID/tokens/STORED_PAYMENT_METHOD_ID", nil)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer USER_TOKEN")
request.Header.Set("X-Merchant-ID", "MERCHANT_ID")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
fmt.Println(string(body))
}A successful deletion returns 204 No Content. If the provider reports that the stored account is invalid during a purchase, DEUNA also removes the invalid reusable consent so it is not offered again.
Safe retry behavior#
- Before creating a new consent, read the current state if the previous response was lost.
- A still-valid
pendingorsuccessconsent is reused instead of creating another provider authorization. - Do not retry
failedorexpiredindefinitely. Start a new customer-driven attempt after resolving the cause. - A
404means DEUNA could not find consent for the order or user context. - A
400can indicate invalid fields, an unsupported connection, a disabled guest flow, or a terminal provider result. - Keep private API keys and user tokens on trusted surfaces. Do not log request headers or provider authorization data.
Integration checklist#
- The order, connection, currency, store, and environment match.
- The identity document and type are available for NuPay.
- The customer is authenticated before creating reusable PayPal consent.
- The app supports provider redirect and app-approval experiences.
- Polling uses the cadence returned by DEUNA and stops on terminal states.
notify_orderaccepts and deduplicates authentication events.- Purchase starts only after
data.consent.statusissuccess. - Failure, expiration, cancellation, and retry paths are tested in sandbox.