SpareSpare Docs
GuidesAPI Reference

Setup Your First Mandate

Create a Variable On Demand payment request in the UAE sandbox, complete bank consent, and obtain a mandate.

Integration guide

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 pis subscription
  • 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.AccessToken

Tokens 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:

  1. Choosing their bank from the supported list.
  2. Reviewing the mandate summary (limits, creditor, reference).
  3. 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:

UsernamePassword
mitsmits
omar.farsi@testmail.aePIX

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 redirectUrl from the create response. Do not skip bank authorization.

On this page