SWAP Payments API

Version 1 · Base URL https://payment.swapkuwait.com/api/v1

SWAP Payments is the single payment service for SWAP applications. Your application never talks to MyFatoorah directly: it asks this service to create a payment, sends the customer to the returned payment page, and is told the outcome. This service keeps the record of every attempt, confirms every outcome with MyFatoorah before reporting it, and handles refunds.

1. How a payment works

your app  ──POST /api/v1/payments/─────────▶  SWAP Payments  ──▶  MyFatoorah
          ◀── { id, status: initiated, payment_url }

your app sends the customer's browser to payment_url          (MyFatoorah's hosted page)
customer pays
MyFatoorah ──▶ SWAP Payments confirms the result with MyFatoorah
           ──▶ redirects the customer to your return_url?transaction=<id>&status=paid
SWAP Payments ──POST callback_url (signed)──▶ your app        (server to server)

Two independent signals tell your app the result: the customer's browser arriving at return_url, and the signed callback to callback_url. Treat the callback, or a GET /payments/{id}/ you make yourself, as the truth. The browser redirect is a convenience for showing the customer a page; a customer can close the tab before it happens.

2. Authentication

Each application has its own API key, issued once by the SWAP Payments team. Send it on every request in either header:

X-API-Key: swp_…
Authorization: Bearer swp_…

An application can only see its own transactions. Another app's transaction id answers 404. A missing or wrong key answers 401. Keys start with swp_; keep them server-side only, never in a mobile app or browser.

Along with the key you receive a callback signing secret, used in section 6.

3. Create a payment

POST /api/v1/payments/
curl -X POST https://payment.swapkuwait.com/api/v1/payments/ \
  -H "X-API-Key: swp_…" -H "Content-Type: application/json" \
  -d '{
    "reference": "ORDER-1042",
    "amount": "12.500",
    "currency": "KWD",
    "return_url": "https://shop.example/orders/1042/payment",
    "description": "Order 1042",
    "customer": { "name": "Ali Al-Sabah", "email": "ali@example.com",
                  "mobile": "12345678", "mobile_country_code": "+965" },
    "language": "en",
    "payment_method_id": 1,
    "idempotency_key": "order-1042-attempt-1",
    "metadata": { "order_id": 1042, "cashier": "web" }
  }'
FieldRequiredMeaning
referenceyesYour own id for the order, invoice or booking. Up to 100 characters. Shown in the MyFatoorah portal as <app>:<reference>. Not required to be unique.
amountyesDecimal as a string, up to 3 decimal places. Must be greater than zero.
currencyno3-letter ISO code. Default KWD.
return_urlyesWhere the customer's browser is sent after paying, with transaction and status appended as query parameters. Must be https and, if your app was configured with allowed origins, on one of them.
descriptionnoShown on the payment page as the item name. Up to 255 characters.
customernoObject with name, email, mobile, mobile_country_code. Shown on the payment page; the name defaults to "Customer" if omitted.
languagenoen or ar for the payment page. Default en.
payment_method_idnoSend the customer straight to one method (ids from section 8, e.g. 1 = KNET, 2 = VISA/MASTER). Omit to let the customer choose on MyFatoorah's page.
idempotency_keynoAny string up to 100 characters, unique per attempt on your side. Repeating a create with the same key returns the same transaction with status 200 instead of opening a second invoice. Use it whenever your call might be retried.
metadatanoAny JSON object. Stored and echoed back in every response and callback. Not sent to MyFatoorah.

Response 201 Created (or 200 when the idempotency key matched an existing transaction):

{
  "id": "9a02f7a4-102f-4ef4-844e-8752353c0020",
  "reference": "ORDER-1042",
  "status": "initiated",
  "amount": "12.500",
  "currency": "KWD",
  "paid_amount": null,
  "paid_currency": null,
  "refunded_amount": "0.000",
  "payment_url": "https://portal.myfatoorah.com/KWT/ie/…",
  "gateway": { "invoice_id": "226960809", "invoice_reference": null, "payment_id": null,
               "transaction_id": null, "payment_gateway": null, "error": null },
  "customer": { "name": "Ali Al-Sabah", "email": "ali@example.com", "mobile": "12345678" },
  "metadata": { "order_id": 1042, "cashier": "web" },
  "created_at": "2026-09-12T16:51:02+00:00",
  "paid_at": null,
  "expires_at": "2026-09-12T17:51:02+00:00"
}

Send the customer to payment_url. The link is valid for 60 minutes; after that the transaction becomes expired and you create a new one. Store id against your order: it is how you read the result later.

4. Read a payment

GET /api/v1/payments/{id}/

Returns the same object as above with the current status. Poll this if you have no callback URL, or whenever you need to be sure.

POST /api/v1/payments/{id}/sync/

Forces SWAP Payments to ask MyFatoorah right now and returns the refreshed object. Use it when a customer says they paid but you see initiated. Not needed routinely: a reconciliation job already re-checks every open payment every five minutes.

GET /api/v1/payments/?reference=ORDER-1042&status=paid&limit=50&offset=0

Lists your transactions, newest first. All filters are optional. Response: { "results": [...], "has_more": false, "next_offset": null }. Maximum limit is 200.

5. The customer's return

After the customer pays, or cancels, MyFatoorah sends the browser to SWAP Payments, which confirms the outcome and redirects to your return_url with two query parameters:

https://shop.example/orders/1042/payment?transaction=9a02f7a4-…&status=paid

status is one of the values in section 9. Use it only to choose which page to show. To mark the order as paid, read the transaction with GET /payments/{id}/ or wait for the callback. Never trust the query string alone: anyone can type it.

If MyFatoorah cannot be reached at that moment the customer sees a short "confirming your payment" page with a link back to your return_url carrying status=pending; the real status follows by callback.

6. Callbacks to your app

If your app was registered with a callback_url, SWAP Payments POSTs to it whenever a transaction reaches a final status and whenever a refund changes. Body:

{ "event": "payment.paid", "transaction": { …the transaction object… } }
{ "event": "refund.refunded", "transaction": { … }, "refund": { …the refund object… } }
HeaderValue
X-Swap-Eventpayment.paid, payment.failed, payment.canceled, payment.expired, refund.pending, refund.refunded, refund.canceled
X-Swap-TransactionThe transaction id
X-Swap-Signaturesha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your callback signing secret

Answer with any 2xx within 10 seconds. Anything else, or a timeout, is recorded as undelivered and retried by a background job and by staff from the dashboard. Handle repeats: the same event may arrive more than once, so make the update idempotent (for example, "set order paid if not already").

Always verify the signature before acting. Compute the HMAC over the exact bytes received, not over re-serialised JSON.

Python (Django)

import hmac, hashlib, json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

SECRET = settings.SWAP_PAYMENTS_CALLBACK_SECRET

@csrf_exempt
def swap_payments_callback(request):
    expected = "sha256=" + hmac.new(SECRET.encode(), request.body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Swap-Signature", "")):
        return HttpResponse(status=401)
    payload = json.loads(request.body)
    txn = payload["transaction"]
    if payload["event"] == "payment.paid":
        mark_order_paid(reference=txn["reference"], transaction_id=txn["id"],
                        amount=txn["paid_amount"])
    return HttpResponse("ok")

C# (ASP.NET Core)

[HttpPost("payments/callback")]
public async Task<IActionResult> Callback()
{
    using var reader = new StreamReader(Request.Body);
    var body = await reader.ReadToEndAsync();
    var key = Encoding.UTF8.GetBytes(_config["SwapPayments:CallbackSecret"]);
    using var hmac = new HMACSHA256(key);
    var expected = "sha256=" + Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(body))).ToLower();
    var received = Request.Headers["X-Swap-Signature"].ToString();
    if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)))
        return Unauthorized();
    var payload = JsonDocument.Parse(body).RootElement;
    var evt = payload.GetProperty("event").GetString();
    var txn = payload.GetProperty("transaction");
    if (evt == "payment.paid")
        await _orders.MarkPaid(txn.GetProperty("reference").GetString(), txn.GetProperty("id").GetString());
    return Ok("ok");
}

7. Refunds

POST /api/v1/payments/{id}/refunds/
{ "amount": "4.000", "reason": "One item returned" }

Refunds all or part of a paid transaction. Omit amount to refund everything still refundable. Several partial refunds are allowed up to the paid amount. Response 201:

{
  "id": "…", "transaction_id": "…", "amount": "4.000", "status": "pending",
  "reason": "One item returned",
  "gateway": { "refund_id": "275988", "refund_reference": "2026002024", "error": null },
  "created_at": "…", "settled_at": null
}

A refund starts as pending. MyFatoorah settles it later, usually the same day for KNET and cards; the transaction then moves to partially_refunded or refunded and your app receives a refund.refunded callback. MyFatoorah's fee on the original payment is generally not returned.

GET /api/v1/payments/{id}/refunds/   GET /api/v1/refunds/{refund_id}/?sync=1

List a payment's refunds, or read one; sync=1 re-checks it with MyFatoorah first.

8. Payment methods

GET /api/v1/payment-methods/?amount=10&currency=KWD

The methods MyFatoorah offers for that amount, with the service charge each adds. Use the id as payment_method_id when creating a payment. Ids are stable per environment but differ between sandbox and live, so read them rather than hard-coding.

{ "results": [ { "id": 1, "code": "kn", "name_en": "KNET", "name_ar": "كي نت", "is_direct": false,
                 "service_charge": 0.15, "total_amount": 10.15, "currency": "KWD", "image_url": "…" }, … ] }

9. Statuses

StatusMeaningFinal?
createdRow exists, MyFatoorah not yet called. You will rarely see it.no
initiatedPayment page issued, customer has not completed payment.no
paidMyFatoorah confirmed the money. paid_amount, paid_at and gateway.payment_gateway are set.yes
failedThe bank or card declined. gateway.error carries the reason. The customer may try again with a new payment.yes
canceledCustomer or MyFatoorah canceled the invoice.yes
expiredThe 60-minute link passed without payment.yes
partially_refundedPaid, and part of it has been refunded. refunded_amount says how much.yes
refundedPaid, and fully refunded.yes

A transaction that reached paid never goes back to failed or canceled; it can only move on to the refunded states.

10. Errors

Errors are JSON with an error code and usually a detail:

HTTPerrorWhen
400invalid_jsonBody is not a JSON object.
401unauthorizedMissing, wrong or inactive API key.
404–Transaction or refund not found, or belongs to another app.
422validation_errorBad input. errors maps each field to its problem, e.g. {"amount": "must be greater than zero"}.
502gateway_errorMyFatoorah refused or was unreachable. detail carries its message. For a create, the transaction is kept as failed so staff can see the attempt.

11. Testing

The service runs in one environment at a time. Ask the SWAP Payments team whether it is currently pointed at MyFatoorah's sandbox or live. On the sandbox, no real money moves and only MyFatoorah's test cards work, for example KNET card 8888880000000001, expiry 09/30, any 4-digit PIN. On live, every payment is real.

A good integration test: create a payment with an idempotency key, open payment_url, pay, confirm your return_url received status=paid, confirm your callback endpoint received payment.paid with a valid signature, then refund and confirm refund.refunded arrives.

12. Go-live checklist

Questions or a new API key: the SWAP Payments dashboard team. Service status: https://payment.swapkuwait.com/healthz.