Payment Flow
End-to-end flow from payment request to settled payment.
This guide walks through a complete single instant payment, the most common onboarding path. Multi-payment, scheduled, and mandate flows build on the same primitives, with the extra steps documented in Mandates.
In this guide
- The four phases every one-off payment passes through
- Which API calls happen on your server vs in the browser
- How payment request and consent statuses relate
- Direct API vs Spare Link integration paths
Overview
Every one-off payment follows the same logical sequence:
- Create a payment request, declare amount, beneficiary, and payment type. This is the only step you always perform.
- Consent & authorization, Spare raises the consent and the payer authorizes at their bank
- Execute payment, Spare submits to the bank after authorization
- Confirm settlement, track status via API or webhooks
You create the request; Spare does the rest
You declare the payment request, your intention to collect a payment. From there Spare raises the consent, handles the payer's bank authorization, and drives the payment to settlement. Hand the payer off with Spare Link and you never touch consent directly.
End-to-end sequence
Phase 1 : Create a payment request
A payment request is your intent. It captures:
- Payment type (e.g.
SingleInstantPayment) - Amount and currency inside
instructions - Creditor account and references
- Success and failure redirect URLs
curl -X POST https://api.sandbox.tryspare.ae/payment-requests \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-tenant: UAE" \
-H "Content-Type: application/json" \
-d '{
"type": "SingleInstantPayment",
"creditorType": "MERCHANT",
"creditorReference": "INV-10042",
"merchantReference": "inv-10042",
"purpose": "GDDS",
"creditorAccount": {
"schemeName": "IBAN",
"identification": "AE070331234567890123456",
"name": "Acme Supplier LLC"
},
"instructions": {
"amount": { "amount": "125.50", "currency": "AED" }
}
}'const request = await client.paymentRequests.create({
amount: 100_00,
currency: "AED",
description: "Invoice #12345",
redirectUrl: "https://yourapp.com/payments/complete",
});request = client.payment_requests.create(
amount=100_00,
currency="AED",
description="Invoice #12345",
redirect_url="https://yourapp.com/payments/complete",
)Configuration config = Configuration.builder()
.appId(System.getenv("SPARE_APP_ID"))
.apiKey(System.getenv("SPARE_API_KEY"))
.tenant("UAE")
.environment("sandbox")
.build();
SpareApiClient client = SpareApiClient.fromConfiguration(config);
var request = client.paymentRequests().create(CreatePaymentRequestRequest.builder()
.amount(100_00)
.currency("AED")
.description("Invoice #12345")
.redirectUrl("https://yourapp.com/payments/complete")
.build());var client = new SpareApiClient(new Configuration(new ConfigurationOptions
{
AppId = Environment.GetEnvironmentVariable("SPARE_APP_ID")!,
ApiKey = Environment.GetEnvironmentVariable("SPARE_API_KEY")!,
Tenant = "UAE",
Environment = "sandbox"
}));
var request = await client.PaymentRequests.CreateAsync(new CreatePaymentRequestRequest
{
Amount = 100_00,
Currency = "AED",
Description = "Invoice #12345",
RedirectUrl = "https://yourapp.com/payments/complete"
});config, err := spareapi.NewConfiguration(spareapi.ConfigurationOptions{
AppID: os.Getenv("SPARE_APP_ID"),
APIKey: os.Getenv("SPARE_API_KEY"),
Tenant: "UAE",
Environment: "sandbox",
})
if err != nil {
log.Fatal(err)
}
client := spareapi.NewClientFromConfiguration(config)
ctx := context.Background()
request, err := client.PaymentRequests.Create(ctx, &spareapi.CreatePaymentRequestRequest{
Amount: 100_00,
Currency: "AED",
Description: "Invoice #12345",
RedirectURL: "https://yourapp.com/payments/complete",
})
if err != nil {
log.Fatal(err)
}Payment request statuses
| Status | Meaning |
|---|---|
New | Request created; consent not yet raised |
ConsentRaised | Linked consent created; awaiting payer action |
Processed | Payment executed successfully |
Consumed | Request fully used (e.g. single payment completed) |
Rejected | Bank or payer declined |
Errored | Technical failure during processing |
Scheduled | Associated with a future-dated or mandate flow |
See Payment Requests API for full schemas.
Phase 2 : Consent (usually handled for you)
Most merchants never create consent
In the standard flow you only create the payment request. Spare raises the consent, handles the payer's bank authorization, and drives the payment through to settlement, use Spare Link to hand the payer off.
Creating the consent yourself (below) is an advanced path, available only to merchants who serve their own payment widgets and hold Nebras CX Certification. Talk to your Spare account team before building it.
When you do drive it yourself, consent links the payment request to a specific payer and bank:
- List providers (banks)
- Create consent from the payment request
- Receive an
authorizationUrl
const consent = await client.paymentConsents.createFromPaymentRequest({
paymentRequestId: request.data.id,
provider: { providerId: "...", providerCode: "..." },
});
// Redirect payer to consent.data.authorizationUrlconsent = client.payment_consents.create_from_payment_request(
payment_request_id=request.data.id,
provider_id="...",
provider_code="...",
)
# Redirect payer to consent.data.authorization_urlvar consent = client.paymentConsents().createFromPaymentRequest(CreateFromPaymentRequestRequest.builder()
.paymentRequestId(request.getData().getId())
.provider(Provider.builder().providerId("...").providerCode("...").build())
.build());
// Redirect payer to consent.getData().getAuthorizationUrl()var consent = await client.PaymentConsents.CreateFromPaymentRequestAsync(new CreateFromPaymentRequestRequest
{
PaymentRequestId = request.Data.Id,
Provider = new Provider { ProviderId = "...", ProviderCode = "..." }
});
// Redirect payer to consent.Data.AuthorizationUrlconsent, err := client.PaymentConsents.CreateFromPaymentRequest(ctx, &spareapi.CreateFromPaymentRequestRequest{
PaymentRequestID: request.Data.Id,
Provider: &spareapi.Provider{ProviderID: "...", ProviderCode: "..."},
})
if err != nil {
log.Fatal(err)
}
// Redirect payer to consent.Data.AuthorizationUrlUntil the payer completes bank authorization, consent stays in a pending state. See Consent lifecycle.
Phase 3 : Payer authorization
The payer leaves your app and authenticates at their bank. This step is never skipped in production.
Your responsibilities
- Redirect only to the
authorizationUrlreturned by Spare - Handle return to your
redirectUrlorfailureRedirectUrl - The payer landing back on your site doesn't confirm the payment; always verify consent status
Spare's responsibilities
- OAuth / bank redirect handling
- Token exchange with the LFI
- Updating consent status to
AuthorisedorRejected
Phase 4 : Execution and settlement
After authorization, Spare submits the payment to the bank. Execution may be synchronous or complete asynchronously depending on the LFI.
How to know payment succeeded
| Method | When to use |
|---|---|
| Poll consent | Simple sandbox tests; GET consent by ID |
| Poll payments | List payments filtered by paymentRequestId |
| Webhooks | Production, see Webhooks |
curl "https://api.sandbox.tryspare.ae/consent/payment/CONSENT_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-tenant: UAE"
curl "https://api.sandbox.tryspare.ae/payments" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "x-tenant: UAE"const status = await client.paymentConsents.get({ consentId: consent.data.id });
const payments = await client.payments.list({ paymentRequestId: request.data.id });status = client.payment_consents.get(consent_id=consent.data.id)
payments = client.payments.list(payment_request_id=request.data.id)var status = client.paymentConsents().get(consent.getData().getId());
var payments = client.payments().list(ListPaymentsRequest.builder()
.paymentRequestId(request.getData().getId())
.build());var status = await client.PaymentConsents.GetAsync(consent.Data.Id);
var payments = await client.Payments.ListAsync(new ListPaymentsRequest
{
PaymentRequestId = request.Data.Id
});status, err := client.PaymentConsents.Get(ctx, consent.Data.Id)
if err != nil {
log.Fatal(err)
}
payments, err := client.Payments.List(ctx, &spareapi.ListPaymentsRequest{
PaymentRequestID: request.Data.Id,
})
if err != nil {
log.Fatal(err)
}Fulfill orders only after verification
Redirect back to your app does not guarantee settlement. Wait for consent Authorised and payment confirmation before shipping goods or closing an invoice.
Payment types
The same flow structure applies across types; instructions differ:
| Type | Use case |
|---|---|
SingleInstantPayment | One-off immediate payment |
FixedPeriodicSchedule / VariablePeriodicSchedule | Recurring on a cadence |
FixedOnDemand / VariableOnDemand | Payer pre-authorizes; you trigger individual debits |
FixedDefinedSchedule / VariableDefinedSchedule | Known future payment dates |
Scheduled and on-demand types create mandates, see Mandates.
Integration paths compared
| Step | Direct API | Spare Link |
|---|---|---|
| Create request | POST /payment-requests | Embedded in POST /link/token/payment or separate |
| Get authorization URL | From consent response | Open Link with link_token |
| After bank login | Poll / webhook | POST /link/exchange with public token |
| Ongoing status | Consent + payments API | Same APIs |
Error handling
| Scenario | What to do |
|---|---|
| Payer abandons bank page | Consent remains pending; expire or create new request |
Rejected consent | Show a failure state; retry only with a new consent |
Errored request | Check API error body; contact support with request ID |
| Timeout waiting for settlement | Poll with backoff; rely on webhooks in production |
Next steps
- Payments, hands-on guide
- Consent lifecycle, status state machine
- Payment Requests API
Key takeaways
- Payment request = intent; consent = authorization; payment = bank execution record.
- Always verify status after redirect, never trust the callback alone.
- Use webhooks in production; polling is fine for sandbox learning.