Skip to main content
Faira lets your customers pay with credit at checkout. This guide walks a merchant from account creation to a fully wired integration: registering, authenticating your API calls, initiating a checkout session, and receiving signed webhooks.
Everything below is real, working API behavior. Amounts are always in minor units (for example, 10000 = £100.00 / ₦100.00). Timestamps are RFC 3339 unless a field is explicitly described as a Unix timestamp.

Base URLs

EnvironmentBase URL
Productionhttps://api.faira.io
Sandbox / Staginghttps://api.staging.faira.io
Local developmenthttp://localhost:8080
All routes in this guide are prefixed with /api/v1. Replace the host with the environment you are integrating against.
Use the exact host issued to your account. If you are unsure which environment your credentials belong to, contact your Faira representative before going live.

Credentials & tokens at a glance

Faira issues several distinct secrets. They are not interchangeable — each is used for a specific purpose.
CredentialPrefixWhere it is usedLifetime
API keyfai_live_mk_Identifies your account. Sent as the X-Api-Key header (HMAC flow) or as a Bearer token for read-only dashboard endpoints.Until rotated
API secretfai_secret_Signs HMAC requests (X-Signature). Shown once at registration/rotation. Never sent over the wire.Until rotated
Webhook signing secretfai_whsec_Verifies webhooks Faira sends you, and signs payment events you send Faira.Until rotated
Dashboard session tokenfai_mst_Returned by login. Authorizes sensitive dashboard actions.8 hours (default)
Relationship access tokenfai_at_Short-lived token that authorizes a customer’s limit/transaction calls.~15 minutes
The API secret and webhook signing secret are shown only at creation or rotation and are never returned again by any endpoint. Store them in your secrets manager immediately. If lost, rotate your credentials to get a new pair.

Step 1 — Create your merchant account

There are two ways an account is created. Most self-serve integrations use registration; enterprise accounts are provisioned by the Faira team.
Register directly and receive live credentials immediately. The account is created in active status — you can log in and integrate right away.
POST /api/v1/merchants/register
Content-Type: application/json
organization_name
string
required
Registered business / trading name. Must be unique across Faira.
contact_name
string
required
Primary contact’s full name.
contact_email
string
required
Valid email. This becomes your dashboard login and receives the welcome email.
contact_phone
string
required
E.164 format, e.g. +447700900000.
business_type
string
required
One of limited_company, sole_trader, partnership, other.
country
string
required
ISO 3166-1 alpha-2 country code, e.g. GB or NG.
website_url
string
Optional business website (must be a valid URL).
webhook_url
string
Optional. HTTPS endpoint that will receive webhook deliveries. You can set this at registration or add it later via your profile. See Webhook URL requirements.
password
string
required
Minimum 8 characters. Used for dashboard login.
password_confirmation
string
required
Must match password.
Request
{
  "organization_name": "Acme Retail Ltd",
  "contact_name": "Jane Doe",
  "contact_email": "jane@acme.example",
  "contact_phone": "+447700900000",
  "business_type": "limited_company",
  "country": "GB",
  "website_url": "https://acme.example",
  "webhook_url": "https://acme.example/webhooks/faira",
  "password": "s3cure-passw0rd",
  "password_confirmation": "s3cure-passw0rd"
}
201 Created
{
  "merchant_id": "11111111-2222-3333-4444-555555555555",
  "api_key": "fai_live_mk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "api_secret": "fai_secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "status": "active",
  "organization_name": "Acme Retail Ltd",
  "business_type": "limited_company",
  "contact_name": "Jane Doe",
  "website_url": "https://acme.example",
  "webhook_url": "https://acme.example/webhooks/faira",
  "welcome_email_sent": true
}
The api_secret in this response is the only time it is ever shown. Copy it now. The webhook_signing_secret is generated at the same time but is retrieved from your dashboard (see Webhooks).
Failure responses
StatusCodeMeaning
400invalid_jsonBody is not valid JSON
409registration_failedAn organization with that name already exists
422passwords_do_not_matchPassword and confirmation differ
422registration_failedA required field failed validation

Step 2 — Log in to the dashboard

Exchange your email and password for a dashboard session token (fai_mst_). The session is required for sensitive actions such as rotating credentials.
POST /api/v1/merchants/login
Content-Type: application/json
Request
{
  "email": "jane@acme.example",
  "password": "s3cure-passw0rd"
}
200 OK
{
  "session_token": "fai_mst_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "merchant_id": "11111111-2222-3333-4444-555555555555",
  "expires_at": "2026-07-13T20:00:00Z",
  "name": "Acme Retail Ltd",
  "api_key": "fai_live_mk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "webhook_url": null,
  "contact_name": "Jane Doe",
  "contact_email": "jane@acme.example",
  "business_type": "limited_company",
  "country": "GB",
  "website_url": "https://acme.example",
  "status": "active"
}
The session token expires after 8 hours by default. Send it as a bearer token on dashboard endpoints:
Authorization: Bearer fai_mst_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Request a reset link. The response is always 200 to prevent email enumeration — a link is only sent if the email exists.
POST /api/v1/merchants/forgot-password
{ "email": "jane@acme.example" }
Then apply the emailed token (valid for 1 hour):
POST /api/v1/merchants/reset-password
{
  "token": "one-time-reset-token",
  "password": "new-s3cure-passw0rd",
  "password_confirmation": "new-s3cure-passw0rd"
}

Step 3 — Complete your profile

Fill in the fields Faira needs to route payments and settle funds to you. Send the API key or a dashboard session on these endpoints.
GET   /api/v1/merchants/profile      # read your profile
PATCH /api/v1/merchants/profile      # update editable fields
PATCH body
{
  "contact_email": "billing@acme.example",
  "contact_name": "Jane Doe",
  "contact_phone": "+447700900000",
  "website_url": "https://acme.example",
  "webhook_url": "https://acme.example/webhooks/faira",
  "settlement_account_no": "12345678",
  "settlement_bank_code": "040004"
}
200 OK
{ "status": "updated" }
organization_name, business_type, country, and status are locked after registration and cannot be changed here. Contact support if any of these need to change.
Both endpoints accept either an API key or a dashboard session token:
Authorization: Bearer fai_live_mk_...     # API key
# or
Authorization: Bearer fai_mst_...         # dashboard session

Step 4 — Store & rotate credentials

Rotating credentials revokes your current API key and secret and issues a new pair. This requires an active dashboard session — API-key auth is intentionally rejected here so a leaked key cannot rotate itself.
POST /api/v1/merchants/credentials/reset
Authorization: Bearer fai_mst_...
200 OK
{
  "api_key": "fai_live_mk_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
  "api_secret": "fai_secret_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
The previous key and secret stop working immediately. Update your servers with the new pair before rotating in production to avoid downtime.

Authenticating your requests

Faira supports three auth styles depending on the endpoint:

API key (bearer)

Read-only dashboard endpoints. Send Authorization: Bearer fai_live_mk_....

Dashboard session

Sensitive dashboard actions. Send Authorization: Bearer fai_mst_....

HMAC signing

Server-to-server checkout endpoints. Sign the request with your API secret.

HMAC request signing

Checkout endpoints (POST /api/v1/sessions/initiate, POST /api/v1/sessions/token) require a signed request. Send these headers:
HeaderValue
X-Merchant-IdYour merchant UUID
X-Api-KeyYour fai_live_mk_ API key
X-TimestampCurrent time in RFC 3339 (e.g. 2026-07-13T12:00:00Z)
X-NonceA unique, single-use string per request
X-SignatureLowercase hex HMAC-SHA256 (see below)
The string-to-sign is five lines joined by \n (newline), in this exact order:
METHOD
/request/path
timestamp
nonce
raw-json-body
The signature is HMAC-SHA256(api_secret, string-to-sign), hex-encoded and lowercase. The raw-json-body must be the exact bytes you send — sign the serialized body, then send that same body unchanged.
Faira rejects a request if the X-Timestamp is more than 5 minutes from server time, or if the X-Nonce has been seen before (nonces are single-use for ~6 minutes). Generate a fresh timestamp and nonce for every request.
import crypto from "node:crypto";

function signRequest({ apiSecret, method, path, body }) {
  const timestamp = new Date().toISOString().replace(/\.\d+Z$/, "Z"); // RFC3339
  const nonce = crypto.randomUUID();
  const rawBody = JSON.stringify(body);

  const stringToSign = [method.toUpperCase(), path, timestamp, nonce, rawBody].join("\n");
  const signature = crypto
    .createHmac("sha256", apiSecret)
    .update(stringToSign)
    .digest("hex"); // lowercase hex

  return { timestamp, nonce, rawBody, signature };
}

// usage
const { timestamp, nonce, rawBody, signature } = signRequest({
  apiSecret: process.env.FAIRA_API_SECRET,
  method: "POST",
  path: "/api/v1/sessions/initiate",
  body: { merchant_id: "1111...", transaction: { amount: 10000, currency: "GBP" } },
});

await fetch("https://api.faira.io/api/v1/sessions/initiate", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Merchant-Id": "11111111-2222-3333-4444-555555555555",
    "X-Api-Key": process.env.FAIRA_API_KEY,
    "X-Timestamp": timestamp,
    "X-Nonce": nonce,
    "X-Signature": signature,
  },
  body: rawBody, // send the exact bytes that were signed
});
import hmac, hashlib, json, uuid
from datetime import datetime, timezone

def sign_request(api_secret: str, method: str, path: str, body: dict):
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    nonce = str(uuid.uuid4())
    raw_body = json.dumps(body, separators=(",", ":"))

    string_to_sign = "\n".join([method.upper(), path, timestamp, nonce, raw_body])
    signature = hmac.new(
        api_secret.encode(), string_to_sign.encode(), hashlib.sha256
    ).hexdigest()  # lowercase hex

    return timestamp, nonce, raw_body, signature
func signRequest(apiSecret, method, path, timestamp, nonce string, body []byte) string {
	payload := strings.Join([]string{
		strings.ToUpper(method),
		path,
		timestamp,
		nonce,
		string(body),
	}, "\n")
	mac := hmac.New(sha256.New, []byte(apiSecret))
	mac.Write([]byte(payload))
	return hex.EncodeToString(mac.Sum(nil))
}

Checkout session flow

Once authenticated, you initiate a checkout session for a customer. This creates a pending transaction and returns a redirect URL that takes your customer into Faira’s hosted login/onboarding flow.
1

Initiate the session

POST /api/v1/sessions/initiate (HMAC-signed). Include the transaction and, for returning customers, their customer_token.
Request
{
  "merchant_id": "11111111-2222-3333-4444-555555555555",
  "unique_customer_reference": "cust_7890",
  "redirect_url": "https://acme.example/faira/callback",
  "transaction": {
    "amount": 10000,
    "currency": "GBP",
    "description": "Order #1234",
    "merchant_ref": "order_1234",
    "metadata": { "cart_id": "cart_456" }
  }
}
201 Created
{
  "session_id": "sess_abc123def4567890",
  "status": "pending_authorization",
  "redirect_url": "https://checkout.faira.io/signup?code=...&session_id=sess_abc123def4567890",
  "pending_transaction_id": "22222222-3333-4444-5555-666666666666"
}
2

Redirect the customer

Send the customer’s browser to the returned redirect_url. They log in or onboard with Faira, which resolves the pending transaction from session_id.
3

Customer authorizes

The customer approves or declines the pending transaction. Faira then updates the transaction and queues a webhook to your configured webhook_url.
customer_token, unique_customer_reference, and redirect_url are all optional. Faira returns a customer_token for a customer only after their relationship with you is verified — store it and send it on future checkouts for a smoother returning experience. The full journey (references, tokens, mappings, transaction authorization) is covered in the Checkout Sessions guide.

Step 5 — Configure webhooks

Faira notifies your backend of transaction state changes by POSTing a signed JSON payload to your webhook_url. Set the URL when you register (optional webhook_url field) or later via your profile. Webhooks are only delivered once a webhook_url is configured.

Webhook URL requirements

Must be HTTPS.
Must not be localhost, a private/internal IP, or a blocked consumer domain.
Should respond with a 2xx quickly (Faira’s delivery timeout is 10 seconds).

Events

EventWhen it fires
transaction.authorizedCustomer authorized the transaction
transaction.declinedCustomer declined the transaction
transaction.paidCheckout transaction was paid (card or SteadyPay)
transaction.paid_outMerchant payout completed (reserved — not yet emitted)

Delivery headers

Every delivery includes:
Content-Type: application/json
Faira-Webhook-Event: transaction.paid
Faira-Webhook-Timestamp: 1783764000
Faira-Webhook-Signature: sha256=<hex-hmac>
Faira-Webhook-Delivery-ID: <uuid>

Example payload

{
  "event": "transaction.paid",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "merchant_id": "11111111-2222-3333-4444-555555555555",
  "customer_reference": "cust_7890",
  "status": "paid",
  "amount": 10000,
  "currency": "GBP",
  "provider": "acquired",
  "payment_transaction_id": "33333333-4444-5555-6666-777777777777",
  "authorized_at": "2026-07-11T10:00:00Z",
  "paid_at": "2026-07-11T10:02:00Z"
}
Some fields appear only when available. Legacy authorization/decline deliveries may omit provider and payment_transaction_id.

Verifying the signature

The signature is HMAC-SHA256 of timestamp + "." + raw_body, using your webhook signing secret (fai_whsec_...), prefixed with sha256=.
import crypto from "node:crypto";

function verifyFairaWebhook(secret, req) {
  const timestamp = req.headers["faira-webhook-timestamp"];
  const signature = req.headers["faira-webhook-signature"];
  const rawBody = req.rawBody; // the raw bytes, NOT a re-serialized object

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret)
      .update(timestamp + "." + rawBody)
      .digest("hex");

  const ok =
    signature &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

  // Reject stale deliveries to limit replay risk
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  return ok && ageSeconds < 300;
}
import hmac, hashlib, time

def verify_faira_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
    mac = hmac.new(secret.encode(), (timestamp + ".").encode() + raw_body, hashlib.sha256)
    expected = "sha256=" + mac.hexdigest()
    if not hmac.compare_digest(expected, signature):
        return False
    return abs(time.time() - int(timestamp)) < 300  # reject stale timestamps
func verifyFairaWebhook(secret, timestamp, signature string, body []byte) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp))
	mac.Write([]byte("."))
	mac.Write(body)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}
Verify against the raw request body bytes, not a re-serialized object. Any whitespace or key-ordering change breaks the HMAC. Also reject timestamps more than ~5 minutes old to reduce replay risk.

Retries & delivery

Faira treats any 2xx as delivered. Non-2xx responses, network errors, and timeouts are retried with exponential backoff:
AttemptDelay before retry
11 minute
22 minutes
34 minutes
48 minutes
516 minutes
The maximum number of attempts is 5 by default. A delivery moves through these statuses: queuedprocessingdelivered, or retryingfailed once the retry limit is reached.
Make your handler idempotent. A network failure after your server processes a delivery but before Faira reads your 2xx will cause a redelivery. Deduplicate on transaction_id + event (or the Faira-Webhook-Delivery-ID header).

Inspect your webhook deliveries

You can review the outbound webhooks Faira has sent you — useful for debugging a missed or failing delivery. These endpoints require a dashboard session (fai_mst_...).
GET /api/v1/merchants/webhooks
GET /api/v1/merchants/webhooks/{webhook_event_id}
Authorization: Bearer fai_mst_...
Query parameters (list endpoint):
ParamDescription
statusFilter by delivery status (queued, processing, delivered, retrying, failed)
eventFilter by event name (e.g. transaction.paid)
pagePage number (default 1)
per_pageItems per page (default 25)
200 OK — list
{
  "webhooks": [
    {
      "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "direction": "outbound",
      "source": "faira",
      "event": "transaction.paid",
      "transaction_id": "22222222-3333-4444-5555-666666666666",
      "url": "https://acme.example/webhooks/faira",
      "status": "delivered",
      "attempts": 1,
      "last_error": null,
      "delivered_at": "2026-07-11T10:02:01Z",
      "created_at": "2026-07-11T10:02:00Z",
      "payload": { "event": "transaction.paid", "amount": 10000, "currency": "GBP" }
    }
  ],
  "pagination": { "page": 1, "per_page": 25, "total": 1 }
}
The detail endpoint returns a single event wrapped as { "webhook": { ... } }. Use status, attempts, and last_error to diagnose failures.

Inbound payment events (optional)

If you settle payments yourself, you can notify Faira of payment/payout events with a signed request to:
POST /api/v1/webhooks/merchants/payment-events
Headers:
X-Merchant-Id: <merchant_id>
X-Webhook-Timestamp: <unix-or-rfc3339 timestamp>
X-Webhook-Signature: sha256=<hex-hmac>
The signature covers timestamp.raw_json_body using your webhook signing secret — the same algorithm Faira uses for outbound deliveries.
Request
{
  "event_id": "merchant_evt_123",
  "event": "payment.paid",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "merchant_ref": "order_1234",
  "paid_at": "2026-07-11T10:02:00Z",
  "metadata": { "channel": "bank_transfer" }
}
202 Accepted
{
  "status": "accepted",
  "event_id": "merchant_evt_123",
  "event": "transaction.paid",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "duplicate": false
}
Provide either transaction_id (Faira’s ID) or merchant_ref (your reference). Events are deduplicated by event_id; a duplicate returns 202 with "duplicate": true. Accepted event names: payment.paid / transaction.paid / paid, and payment.paid_out / payout.paid_out / paid_out / settled.

Error handling

Errors use a consistent JSON envelope:
{
  "error": {
    "code": "login_failed",
    "message": "invalid email or password"
  }
}
StatusTypical cause
400Malformed JSON or invalid parameters
401Missing/invalid credentials, bad HMAC signature, or expired session
404Resource not found
409Conflict (e.g. duplicate organization name)
422Validation error (e.g. passwords don’t match)
429Rate limited (registration, login, and password endpoints are throttled)

Go-live checklist

1

Credentials stored securely

API key, API secret, and webhook signing secret are in your secrets manager — not in source control.
2

Profile complete

Settlement account, contact details, and a production webhook_url are set.
3

HMAC signing verified

A signed POST /api/v1/sessions/initiate returns 201 in sandbox.
4

Webhook endpoint live

HTTPS, publicly reachable, verifies signatures, is idempotent, and replies 2xx within 10 seconds.
5

Redirect handling

Your redirect_url correctly resumes the customer after the Faira flow.
6

Idempotency + retries handled

Duplicate deliveries are safely ignored; failed webhooks are reconciled.
Need help? Contact your Faira representative or email support with your merchant_id (never share your API secret or webhook signing secret).