Setup Your First Mandate
Create a Variable On Demand payment request in the UAE sandbox, complete bank consent, and obtain a mandate.
Goal: Create a Variable On Demand payment request, complete payer bank authorization, and finish with a Spare-created mandate.
Estimated time: 25 minutes
Prerequisites
- Sandbox account with an active
pissubscription - App ID, API key, and private key (for
x-signature) - A creditor bank account (portal or API)
Watch a multipayment first-time setup walkthrough:
Multi-payment types create mandates that you manage after consent. This tutorial uses VariableOnDemand.
Before you start
Make sure you have:
- An onboarded Spare sandbox account with whitelisted user emails and an active Payment Initiation (
pis) subscription - An App ID, API key, and private key. Refer to the Quick Start Setup guide to create them.
- A registered creditor account. Refer to the Setup your Bank Account guide.
If any of these are missing, contact your Spare sales point of contact or email support@tryspare.com.
Authenticate
Exchange your credentials for a short-lived access token, then include it on every subsequent request.
export SPARE_APP_ID="your-app-id"
export SPARE_API_KEY="your-api-key"
export SPARE_TENANT="UAE"curl -X POST https://api.sandbox.tryspare.ae/auth/api-keys/sessions \
-H "x-tenant: UAE" \
-H "app-id: $SPARE_APP_ID" \
-H "x-api-key: $SPARE_API_KEY"const res = await fetch(
"https://api.sandbox.tryspare.ae/auth/api-keys/sessions",
{
method: "POST",
headers: {
"x-tenant": "UAE",
"app-id": process.env.SPARE_APP_ID!,
"x-api-key": process.env.SPARE_API_KEY!,
},
},
);
const { accessToken, expiresIn } = await res.json();import os
import requests
res = requests.post(
"https://api.sandbox.tryspare.ae/auth/api-keys/sessions",
headers={
"x-tenant": "UAE",
"app-id": os.environ["SPARE_APP_ID"],
"x-api-key": os.environ["SPARE_API_KEY"],
},
)
data = res.json()
access_token = data["accessToken"]import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.tryspare.ae/auth/api-keys/sessions"))
.header("x-tenant", "UAE")
.header("app-id", System.getenv("SPARE_APP_ID"))
.header("x-api-key", System.getenv("SPARE_API_KEY"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse the JSON response and read the accessToken field
JsonNode data = new ObjectMapper().readTree(response.body());
String accessToken = data.get("accessToken").asText();using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using var client = new HttpClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"https://api.sandbox.tryspare.ae/auth/api-keys/sessions");
request.Headers.Add("x-tenant", "UAE");
request.Headers.Add("app-id", Environment.GetEnvironmentVariable("SPARE_APP_ID"));
request.Headers.Add("x-api-key", Environment.GetEnvironmentVariable("SPARE_API_KEY"));
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var accessToken = doc.RootElement.GetProperty("accessToken").GetString();package main
import (
"encoding/json"
"net/http"
"os"
)
req, err := http.NewRequest(
http.MethodPost,
"https://api.sandbox.tryspare.ae/auth/api-keys/sessions",
nil,
)
if err != nil {
panic(err)
}
req.Header.Set("x-tenant", "UAE")
req.Header.Set("app-id", os.Getenv("SPARE_APP_ID"))
req.Header.Set("x-api-key", os.Getenv("SPARE_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data struct {
AccessToken string `json:"accessToken"`
ExpiresIn int `json:"expiresIn"`
}
json.NewDecoder(res.Body).Decode(&data)
accessToken := data.AccessTokenTokens expire. Refresh via POST /auth/api-keys/sessions/refresh before they do, or on a 401.
Choose a Multi-Payment Type
Six payment types create a mandate: FixedPeriodicSchedule, VariablePeriodicSchedule, FixedOnDemand, VariableOnDemand, FixedDefinedSchedule, and VariableDefinedSchedule. Pick the type that matches your product. This tutorial uses VariableOnDemand, where the payer authorizes a per-debit ceiling and you trigger each debit later. See the Variable On Demand deep dive for field rules.
Create the Payment Request
Send POST /payment-requests with type: "VariableOnDemand" and a periodicSchedule that includes maxAmount plus controls that meet the minimum control set.
Request signature required
POST /payment-requests requires an x-signature header. See Request signing for the canonical serialization rules and how to build the detached ES256 JWS.
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" \
-H "x-signature: $X_SIGNATURE" \
-d '{
"type": "VariableOnDemand",
"creditorType": "MERCHANT",
"creditorReference": "SWEEP01",
"merchantReference": "vod-sweep-01",
"purpose": "GDDS",
"creditorAccount": {
"schemeName": "IBAN",
"identification": "10000109010101",
"name": "Acme Trading LLC"
},
"successRedirectUrl": "https://yourapp.com/mandates/complete",
"failureRedirectUrl": "https://yourapp.com/mandates/failed",
"instructions": {
"maxCumulativeAmount": { "amount": "200.00", "currency": "AED" },
"periodicSchedule": {
"frequency": "Week",
"startDate": "2026-08-13",
"endDate": "2027-08-01",
"maxAmount": { "amount": "50.00", "currency": "AED" },
"controls": {
"maxCumulativeAmountPerPeriod": { "amount": "100.00", "currency": "AED" }
}
}
}
}'const paymentRequest = {
type: "VariableOnDemand",
creditorType: "MERCHANT",
creditorReference: "SWEEP01",
merchantReference: "vod-sweep-01",
purpose: "GDDS",
creditorAccount: {
schemeName: "IBAN",
identification: "10000109010101",
name: "Acme Trading LLC",
},
successRedirectUrl: "https://yourapp.com/mandates/complete",
failureRedirectUrl: "https://yourapp.com/mandates/failed",
instructions: {
maxCumulativeAmount: { amount: "200.00", currency: "AED" },
periodicSchedule: {
frequency: "Week",
startDate: "2026-08-13",
endDate: "2027-08-01",
maxAmount: { amount: "50.00", currency: "AED" },
controls: {
maxCumulativeAmountPerPeriod: { amount: "100.00", currency: "AED" },
},
},
},
};
// Build xSignature from paymentRequest (see Request signing).
const res = await fetch("https://api.sandbox.tryspare.ae/payment-requests", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"x-tenant": "UAE",
"Content-Type": "application/json",
"x-signature": xSignature,
},
body: JSON.stringify(paymentRequest),
});
const { data } = await res.json();
const redirectUrl = data.redirectUrl;import json
import requests
payment_request = {
"type": "VariableOnDemand",
"creditorType": "MERCHANT",
"creditorReference": "SWEEP01",
"merchantReference": "vod-sweep-01",
"purpose": "GDDS",
"creditorAccount": {
"schemeName": "IBAN",
"identification": "10000109010101",
"name": "Acme Trading LLC",
},
"successRedirectUrl": "https://yourapp.com/mandates/complete",
"failureRedirectUrl": "https://yourapp.com/mandates/failed",
"instructions": {
"maxCumulativeAmount": {"amount": "200.00", "currency": "AED"},
"periodicSchedule": {
"frequency": "Week",
"startDate": "2026-08-13",
"endDate": "2027-08-01",
"maxAmount": {"amount": "50.00", "currency": "AED"},
"controls": {
"maxCumulativeAmountPerPeriod": {"amount": "100.00", "currency": "AED"},
},
},
},
}
# Build x_signature from payment_request (see Request signing).
res = requests.post(
"https://api.sandbox.tryspare.ae/payment-requests",
headers={
"Authorization": f"Bearer {access_token}",
"x-tenant": "UAE",
"Content-Type": "application/json",
"x-signature": x_signature,
},
data=json.dumps(payment_request),
)
data = res.json()["data"]
redirect_url = data["redirectUrl"]import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String paymentRequestJson = """
{
"type": "VariableOnDemand",
"creditorType": "MERCHANT",
"creditorReference": "SWEEP01",
"merchantReference": "vod-sweep-01",
"purpose": "GDDS",
"creditorAccount": {
"schemeName": "IBAN",
"identification": "10000109010101",
"name": "Acme Trading LLC"
},
"successRedirectUrl": "https://yourapp.com/mandates/complete",
"failureRedirectUrl": "https://yourapp.com/mandates/failed",
"instructions": {
"maxCumulativeAmount": { "amount": "200.00", "currency": "AED" },
"periodicSchedule": {
"frequency": "Week",
"startDate": "2026-08-13",
"endDate": "2027-08-01",
"maxAmount": { "amount": "50.00", "currency": "AED" },
"controls": {
"maxCumulativeAmountPerPeriod": { "amount": "100.00", "currency": "AED" }
}
}
}
}
""";
// Build xSignature from paymentRequestJson (see Request signing).
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.tryspare.ae/payment-requests"))
.header("Authorization", "Bearer " + accessToken)
.header("x-tenant", "UAE")
.header("Content-Type", "application/json")
.header("x-signature", xSignature)
.POST(HttpRequest.BodyPublishers.ofString(paymentRequestJson))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse data.redirectUrl from the JSON response.using System.Net.Http;
using System.Text;
using System.Text.Json;
var paymentRequest = new
{
type = "VariableOnDemand",
creditorType = "MERCHANT",
creditorReference = "SWEEP01",
merchantReference = "vod-sweep-01",
purpose = "GDDS",
creditorAccount = new
{
schemeName = "IBAN",
identification = "10000109010101",
name = "Acme Trading LLC",
},
successRedirectUrl = "https://yourapp.com/mandates/complete",
failureRedirectUrl = "https://yourapp.com/mandates/failed",
instructions = new
{
maxCumulativeAmount = new { amount = "200.00", currency = "AED" },
periodicSchedule = new
{
frequency = "Week",
startDate = "2026-08-13",
endDate = "2027-08-01",
maxAmount = new { amount = "50.00", currency = "AED" },
controls = new
{
maxCumulativeAmountPerPeriod = new { amount = "100.00", currency = "AED" },
},
},
},
};
// Build xSignature from paymentRequest (see Request signing).
var body = JsonSerializer.Serialize(paymentRequest);
using var client = new HttpClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"https://api.sandbox.tryspare.ae/payment-requests");
request.Headers.Add("Authorization", $"Bearer {accessToken}");
request.Headers.Add("x-tenant", "UAE");
request.Headers.Add("x-signature", xSignature);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
// Parse data.redirectUrl from the JSON response.paymentRequest := map[string]any{
"type": "VariableOnDemand",
"creditorType": "MERCHANT",
"creditorReference": "SWEEP01",
"merchantReference": "vod-sweep-01",
"purpose": "GDDS",
"creditorAccount": map[string]string{
"schemeName": "IBAN",
"identification": "10000109010101",
"name": "Acme Trading LLC",
},
"successRedirectUrl": "https://yourapp.com/mandates/complete",
"failureRedirectUrl": "https://yourapp.com/mandates/failed",
"instructions": map[string]any{
"maxCumulativeAmount": map[string]string{"amount": "200.00", "currency": "AED"},
"periodicSchedule": map[string]any{
"frequency": "Week",
"startDate": "2026-08-13",
"endDate": "2027-08-01",
"maxAmount": map[string]string{"amount": "50.00", "currency": "AED"},
"controls": map[string]any{
"maxCumulativeAmountPerPeriod": map[string]string{"amount": "100.00", "currency": "AED"},
},
},
},
}
// Build xSignature from paymentRequest (see Request signing).
body, _ := json.Marshal(paymentRequest)
req, err := http.NewRequest(
http.MethodPost,
"https://api.sandbox.tryspare.ae/payment-requests",
bytes.NewReader(body),
)
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("x-tenant", "UAE")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-signature", xSignature)
res, err := http.DefaultClient.Do(req)
// Parse data.redirectUrl from the JSON response.Example success response:
{
"data": {
"id": "7f2a8c1b-4e09-4d6a-9c12-8b3f5e6d7a01",
"type": "VariableOnDemand",
"status": "New",
"purpose": "GDDS",
"creditorType": "MERCHANT",
"creditorReference": "SWEEP01",
"merchantReference": "vod-sweep-01",
"merchantId": "03c18731-9c29-4255-bea1-593536387bf3",
"internalReference": "Kp9mQx2nRwT",
"isCopVerified": false,
"creditorAccount": {
"id": "858704f9-db10-40ff-a79d-12e658ec2713",
"schemeName": "IBAN",
"identification": "10000109010101",
"name": "Acme Trading LLC"
},
"instructions": {
"maxCumulativeAmount": {
"amount": "200.00",
"currency": "AED"
},
"periodicSchedule": {
"frequency": "Week",
"startDate": "2026-08-13",
"endDate": "2027-08-01",
"maxAmount": {
"amount": "50.00",
"currency": "AED"
},
"controls": {
"maxCumulativeAmountPerPeriod": {
"amount": "100.00",
"currency": "AED"
}
}
}
},
"redirectUrl": "https://web.sandbox.tryspare.ae/pis/Kp9mQx2nRwT",
"successRedirectUrl": "https://yourapp.com/mandates/complete",
"failureRedirectUrl": "https://yourapp.com/mandates/failed",
"createdAt": "2026-08-13T10:15:30.037+00:00",
"updatedAt": "2026-08-13T10:15:30.037+00:00"
}
}The response returns data.id, data.internalReference, and data.redirectUrl. Send the payer to data.redirectUrl to start bank authorization on Spare hosted pages.
Redirect the Payer
Send the payer to redirectUrl from the create response. Spare hosted pages walk them through:
- Choosing their bank from the supported list.
- Reviewing the mandate summary (limits, creditor, reference).
- Signing in on the bank login page and authorizing the consent.
Authorize at the Bank
On the sandbox bank login page, enter a username and password. Use either pair:
| Username | Password |
|---|---|
mits | mits |
omar.farsi@testmail.ae | PIX |
Do not skip this step. The mandate does not exist until the payer completes bank authorization.
After authorization, the payer's bank returns them to Spare. Spare creates the mandate and redirects the payer to your successRedirectUrl. The URL includes referenceId, merchantRef, mandateId, and sig query parameters. sig is an ES256 JWS of the URL. Verify it on your server before you schedule debits.
If authorization fails or the payer cancels, Spare shows an error screen and redirects to your failureRedirectUrl when you registered one at create time.
Manage Your Mandates
The mandate is live. Open Manage Your Mandates to see what you can do next and how each payment family is controlled: list transactions, approve variable amounts, schedule on-demand debits, and post immediately.
Keep mandateId from the success redirect. You will need it on that page.
Key takeaways
- Spare creates the mandate after the payer completes bank authorization on hosted pages.
- Use Manage Your Mandates to schedule and approve debits against the mandate.
- Always send the payer to
redirectUrlfrom the create response. Do not skip bank authorization.