SpareSpare Docs
GuidesAPI Reference

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:

  1. Create a payment request, declare amount, beneficiary, and payment type. This is the only step you always perform.
  2. Consent & authorization, Spare raises the consent and the payer authorizes at their bank
  3. Execute payment, Spare submits to the bank after authorization
  4. 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

StatusMeaning
NewRequest created; consent not yet raised
ConsentRaisedLinked consent created; awaiting payer action
ProcessedPayment executed successfully
ConsumedRequest fully used (e.g. single payment completed)
RejectedBank or payer declined
ErroredTechnical failure during processing
ScheduledAssociated with a future-dated or mandate flow

See Payment Requests API for full schemas.

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:

  1. List providers (banks)
  2. Create consent from the payment request
  3. Receive an authorizationUrl
const consent = await client.paymentConsents.createFromPaymentRequest({
  paymentRequestId: request.data.id,
  provider: { providerId: "...", providerCode: "..." },
});
// Redirect payer to consent.data.authorizationUrl
consent = client.payment_consents.create_from_payment_request(
    payment_request_id=request.data.id,
    provider_id="...",
    provider_code="...",
)
# Redirect payer to consent.data.authorization_url
var 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.AuthorizationUrl
consent, 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.AuthorizationUrl

Until 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 authorizationUrl returned by Spare
  • Handle return to your redirectUrl or failureRedirectUrl
  • 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 Authorised or Rejected

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

MethodWhen to use
Poll consentSimple sandbox tests; GET consent by ID
Poll paymentsList payments filtered by paymentRequestId
WebhooksProduction, 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:

TypeUse case
SingleInstantPaymentOne-off immediate payment
FixedPeriodicSchedule / VariablePeriodicScheduleRecurring on a cadence
FixedOnDemand / VariableOnDemandPayer pre-authorizes; you trigger individual debits
FixedDefinedSchedule / VariableDefinedScheduleKnown future payment dates

Scheduled and on-demand types create mandates, see Mandates.

Integration paths compared

StepDirect APISpare Link
Create requestPOST /payment-requestsEmbedded in POST /link/token/payment or separate
Get authorization URLFrom consent responseOpen Link with link_token
After bank loginPoll / webhookPOST /link/exchange with public token
Ongoing statusConsent + payments APISame APIs

Error handling

ScenarioWhat to do
Payer abandons bank pageConsent remains pending; expire or create new request
Rejected consentShow a failure state; retry only with a new consent
Errored requestCheck API error body; contact support with request ID
Timeout waiting for settlementPoll with backoff; rely on webhooks in production

Next steps

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.

On this page