Payment Links
Create shareable payment links. A link can be single-use or reusable, fixed-amount or open-amount (customer enters the amount, e.g. tips/donations).
/payment-linksList payment links
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)payment_links:read
Returns a paginated list of payment links for the active business in the current mode.
Parameters
pageintegerqueryPage number (1-based).
limitintegerqueryItems per page (max 100).
statusACTIVE | DISABLED | EXPIREDqueryFilter by status.
customerIdstringqueryFilter by linked customer id.
Responses
Paginated payment links
/payment-linksCreate a payment link
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)payment_links:write
Creates a payment link and returns its full record, including payPageUrl — the shareable URL you send to the customer. Omit amount to create an open-amount link where the customer enters the amount at checkout.
Request body
titlestringrequiredShort title shown to the customer on the checkout page.
amountobjectoptionalAmount in IQD. Omit for an open-amount link (customer picks the amount — tip jars / donations).
isReusablebooleanoptionalIf true, the link can be paid more than once. Default: single-use.
expiresAtstring (date-time)optionalOptional expiry. ISO-8601 (YYYY-MM-DD or full timestamp).
customerIdstringoptionalOptional existing customer id.
customerNamestringoptionalcustomerPhonestringoptionalA valid Iraqi phone number in E.164 form.
{
"title": "Order #1042",
"amount": "50000",
"isReusable": false,
"customerName": "Ahmed Ali",
"customerPhone": "+9647801234567"
}Responses
Payment link created
/payment-links/{id}Fetch a payment link
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)payment_links:read
Parameters
idstringpathrequiredResource id.
Responses
Payment link
/payment-links/{id}Update a payment link
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)payment_links:write
Partially updates a payment link. Only the fields you send are changed.
Parameters
idstringpathrequiredResource id.
Request body
titlestringoptionalamountstringoptionalAn amount in IQD (no decimals), serialized as an integer string to preserve precision.
isReusablebooleanoptionalexpiresAtstring (date-time)optionalcustomerIdstringoptionalcustomerNamestringoptionalcustomerPhonestringoptionalA valid Iraqi phone number in E.164 form.
Responses
Updated payment link
/payment-links/{id}Delete a payment link
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)payment_links:write
Parameters
idstringpathrequiredResource id.
Responses
Deleted
/payment-links/{id}/disableDisable a payment link
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)payment_links:write
Disables a payment link so it can no longer be paid. Returns the updated record with status: DISABLED.
Parameters
idstringpathrequiredResource id.
Responses
Disabled payment link
Invoices
Create itemized invoices with line items, tax, and discounts. The invoice is issued immediately and the response returns a hosted pay-page URL (payPage.url) — share that link with your customer so they can pay on the SwiftPayIQ checkout page.
/invoicesList invoices
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)invoices:read
Returns a paginated list of invoices for the active business in the current mode, with optional status, customer, date-range, and sort filters.
Parameters
pageintegerqueryPage number (1-based).
limitintegerqueryItems per page (max 100).
statusDRAFT | ISSUED | SENT | PAID | PARTIALLY_PAID | OVERDUE | CANCELLEDqueryFilter by status.
customerIdstringqueryFilter by linked customer id.
fromstring (date-time)queryInclusive lower bound on createdAt (ISO-8601).
tostring (date-time)queryInclusive upper bound on createdAt (ISO-8601).
sortBycreatedAt | updatedAt | dueDate | issuedAt | total | numberquerysortDirasc | descqueryResponses
Paginated invoices
/invoicesCreate an invoice
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)invoices:write
Creates an invoice from line items and issues it immediately. SwiftPayIQ does not message the customer for you over this API — there is no WhatsApp/SMS channel. Instead the response returns payPage.url, the hosted checkout page; deliver that link to your customer however you like (your own email/SMS, a redirect, a QR code, etc.). API-created invoices come back with status: SENT, source: API, and deliveryChannel: API.
Request body
customerIdstringoptionalOptional existing customer id.
customerNamestringrequiredSnapshot kept on the invoice even if the customer row changes later.
customerPhonestringrequiredA valid Iraqi phone number in E.164 form.
itemsobject[]requiredtaxAmountobjectoptionalExtra tax on top of the subtotal. Default "0".
discountobjectoptionalFixed IQD discount. Mutually exclusive with discountPercentage.
discountPercentagestringoptionalPercentage discount (0–99.99, up to 2 decimals). Mutually exclusive with discount.
dueDatestring (date-time)optionalISO-8601 (YYYY-MM-DD or full timestamp).
notesstringoptional{
"customerName": "API-generated customer",
"customerPhone": "+9647701112233",
"items": [
{
"description": "SaaS subscription — monthly",
"quantity": 1,
"unitPrice": "45000"
}
],
"discount": "5000",
"taxAmount": "0",
"notes": "Issued via server-to-server integration"
}Responses
Invoice created and issued
{
"invoice": {
"id": "cmptoobsi0007xnlvtyx4iux2",
"number": "INV-000002",
"slug": "1tinjslkqp",
"isLive": false,
"customerId": null,
"customerName": "API-generated customer",
"customerPhone": "+9647701112233",
"status": "SENT",
"source": "API",
"deliveryChannel": "API",
"sentAt": "2026-05-31T11:15:33.387Z",
"subtotal": "45000",
"discount": "5000",
"discountPercentage": null,
"taxAmount": "0",
"customerFeeAmount": "0",
"total": "40000",
"amountPaid": "0",
"feeRate": "0",
"feeAmount": "0",
"customerFeeShare": "0.0000",
"netAmount": "40000",
"dueDate": null,
"issuedAt": "2026-05-31T11:15:33.387Z",
"paidAt": null,
"notes": "Issued via server-to-server integration",
"createdByType": "API_KEY",
"createdAt": "2026-05-31T11:15:33.426Z",
"updatedAt": "2026-05-31T11:15:33.426Z",
"items": [
{
"id": "cmptoobso0008xnlvz938stgm",
"productId": null,
"description": "SaaS subscription — monthly",
"quantity": 1,
"unitPrice": "45000",
"lineTotal": "45000",
"position": 0
}
],
"payments": [],
"collections": []
},
"payPage": {
"url": "https://swiftpayiq.com/pay?slug=1tinjslkqp"
}
}/invoices/{id}Fetch an invoice
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)invoices:read
Parameters
idstringpathrequiredResource id.
Responses
Invoice
/invoices/{id}Update an invoice
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)invoices:write
Partially updates an invoice. Only DRAFT invoices can be edited. Sending items replaces the full line-item list.
Parameters
idstringpathrequiredResource id.
Request body
customerIdstringoptionalcustomerNamestringoptionalcustomerPhonestringoptionalA valid Iraqi phone number in E.164 form.
itemsobject[]optionaltaxAmountstringoptionalAn amount in IQD (no decimals), serialized as an integer string to preserve precision.
discountstringoptionalAn amount in IQD (no decimals), serialized as an integer string to preserve precision.
discountPercentagestringoptionaldueDatestring (date-time)optionalnotesstringoptionalResponses
Updated invoice
/invoices/{id}Delete an invoice draft
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)invoices:write
Deletes a DRAFT invoice. Issued or paid invoices cannot be deleted — cancel them instead.
Parameters
idstringpathrequiredResource id.
Responses
Deleted
/invoices/{id}/cancelCancel an invoice
AuthYour secret API key as a Bearer token: Authorization: Bearer spi_live_… (or spi_test_…)invoices:write
Cancels an issued or sent invoice. Returns the updated record with status: CANCELLED.
Parameters
idstringpathrequiredResource id.
Responses
Cancelled invoice
Partner Connections
Connecting a merchant to your platform. You call this once per merchant, with your own partner key, to receive that merchant's API key. Everything after that uses the merchant key against the Payment Links and Invoices endpoints.
/partner/connections/exchangeSwap a merchant's one-time code for their API key
AuthYour partner key as a Bearer token: Authorization: Bearer spp_live_…
The second half of the connection handover.
The merchant authorises your platform inside their SwiftPayIQ portal and is redirected back to you carrying a short-lived code. That code grants nothing on its own — your server swaps it here, authenticated with your partner key, and receives the merchant's own API key in return.
The merchant key is returned exactly once. It is stored hashed and can never be retrieved again, so persist it before you respond to the request. Lose it and the merchant has to authorise you again.
Single use, and codes expire five minutes after they are issued.
A code does not always come back
If the merchant connects a business they already had — rather than one created through the setup page you sent them to — the link is recorded as pending review and waits for a SwiftPayIQ admin to approve it. Their consent is real and dated; what is missing is our decision.
In that case no code is issued, and the merchant returns to your redirect address without a ?code= parameter. This is deliberate: an established merchant's business is not handed to a platform on the strength of one button press.
Handle the no-code return. Treat it as "waiting on SwiftPayIQ", not as a failure and not as a finished connection. Send the merchant back through the setup page and you will get partner.link_already_pending_review — the request is already on file.
Request body
codestringrequiredThe one-time code from the merchant redirect.
Responses
Connected. Store apiKey now — it is never shown again.
/partner/connections/pendingMerchants an admin approved that you have not collected yet
AuthYour partner key as a Bearer token: Authorization: Bearer spp_live_…
The other half of the review flow.
When a merchant connects a business they already had, the link waits for a SwiftPayIQ admin. That decision happens out of band, days after the merchant consented, so there is no redirect to carry a code back to you. This is where your server finds out.
Each entry carries a fresh one-time code. Swap it at /partner/connections/exchange exactly as you would one that arrived by redirect.
Codes are minted per call and live five minutes, so call this when you are ready to exchange rather than on a slow timer. An entry disappears once you hold a key for that merchant, so a run that fails halfway is safe to repeat — you will simply be offered the ones you did not finish.
An empty array means nothing is waiting. Poll it a few times a day, or when the PARTNER_CONNECTION_READY notification reaches your SwiftPayIQ inbox.
Responses
Approved merchants awaiting collection. Empty when there are none.
Partner Merchants
Onboarding a merchant from your own signup flow. Creates an unclaimed account and returns a link for the merchant to claim it themselves.
/partner/merchantsCreate a referred merchant from your own signup flow
AuthYour partner key as a Bearer token: Authorization: Bearer spp_live_…
For a platform whose merchants sign up on your side rather than ours.
Creates an unverified account, a DRAFT business and a pending link, and returns a claim URL to send the merchant.
What it does not do: issue a password, send a one-time code, or earn you anything yet. The merchant opens the claim URL, sets their own password and verifies their own phone — and that verification is the only thing that activates the link and starts commission. You cannot consent on their behalf.
An existing SwiftPayIQ phone number is refused rather than claimed: invite that merchant through the connection flow instead.
Request body
phonestringrequiredThe merchant's own phone, E.164.
namestringrequiredemailstringoptionalattestedConsentbooleanrequiredYou are asserting the merchant asked you to do this. Recorded on the consent trail against your partner account, with the key on the audit entry. Required.
businessobjectrequiredResponses
Created. Send claimUrl to the merchant — nothing happens until they use it.
/partner/onboarding-sessionsSend a merchant to a setup page we host
AuthPartnerApiKey
For a platform that would rather not build sign-up, one-time codes and document upload itself.
Returns a single-use URL. Open it for the merchant in a new tab; every screen that touches a password or a one-time code stays on our pages, so the merchant's credentials never pass through your servers.
The page reads the merchant's own records each time rather than storing a step number, so somebody who stops halfway through uploading documents and comes back later lands on exactly the same screen.
Register a return address first. Without one this call fails with partner.no_redirect_uri — a journey that ends nowhere is not a finished journey. Ask our team to set it for your account.
When the merchant finishes we send them to that address with ?code=, the same one-time code described on POST /partner/connections/exchange, swapped the same way from your server.
Two ways the journey can end
A business created through this page is yours to connect: the merchant presses connect and comes back to you with ?code=.
A business the merchant already had goes to review instead. They come back with no ?code=, and a SwiftPayIQ admin decides. Build for both endings — the second one is the normal case for any merchant who was already trading with us.
Request body
phonestringrequiredThe merchant's own phone, E.164. The one-time code goes here, which is what stops you completing this on their behalf.
prefillobjectoptionalAnything you already know, to save them typing: trade name, governorate. Convenience only — they can change all of it and none of it is trusted.
Responses
Link created.
Webhooks
We post these to the URL registered for your account. Reply 2xx quickly and do the work afterwards — a slow handler looks the same to us as a broken one.
payment.succeededpayment.succeeded
Sent when a payment completes successfully. This is the event most integrations care about — fulfil the order when you receive it (after verifying the signature).
How webhooks are delivered
SwiftPayIQ sends an HTTP POST to the endpoint URL you configured in the dashboard, with a JSON body of the shape:
{ "event": "payment.succeeded", "data": { /* … */ } }Headers on every delivery:
| Header | Value |
|---|---|
Content-Type | application/json |
X-SwiftPay-Signature | t=<unix-seconds>,v1=<hex-hmac> |
Your endpoint should respond with any 2xx status as quickly as possible. A non-2xx response (or a timeout — the request is aborted after 10 seconds) is treated as a failure and retried.
Verifying the signature
Every delivery is signed with the signing secret of your webhook (shown when you create or rotate it in the dashboard; it starts with whsec_). Verify it before trusting the payload:
- Read the
X-SwiftPay-Signatureheader and parse thet(timestamp) andv1(signature) values. - Reject the request if
tis more than 5 minutes away from your current time (replay protection). - Build the signed string:
"{t}.{rawRequestBody}"— use the raw, unparsed request body bytes exactly as received. - Compute
HMAC-SHA256(signingSecret, signedString)and hex-encode it. - Compare it to
v1using a constant-time comparison. If they match, the request is authentic.
// Node.js (Express) example
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = parts.t, v1 = parts.v1;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));
}> Capture the raw request body for signature verification — re-serializing parsed JSON will change the bytes and the signature will not match.
Retries
If your endpoint doesn't return 2xx, delivery is retried up to 6 attempts total (5 retries) with increasing back-off: 1 min → 5 min → 30 min → 2 hours → 8 hours (about 10.5 hours end-to-end). A 4xx response other than 408/429 is treated as a permanent failure and is not retried. Make your handler idempotent — the same event may arrive more than once. You can inspect delivery history and re-send from the dashboard.
Payload
eventpayment.succeeded | payment.failed | payment.refunded | invoice.paid | payment_link.paidrequiredThe event type.
dataobjectrequiredThe payment that triggered the event. Amounts are IQD integer strings.
{
"event": "payment.succeeded",
"data": {
"paymentId": "9b2c0e1a-1f3d-4a55-8c2e-1a2b3c4d5e6f",
"businessId": "3a1d…",
"isLive": true,
"amount": "50000",
"status": "SUCCEEDED",
"provider": "ALQASEH",
"gatewayTxnId": "alq_8f21c…",
"invoiceId": null,
"paymentLinkId": "7c9e…",
"customerId": null,
"customerPhone": "+9647801234567"
}
}payment.failedpayment.failed
Sent when a payment attempt fails. Same delivery, signing, and retry rules as payment.succeeded.
Payload
eventpayment.succeeded | payment.failed | payment.refunded | invoice.paid | payment_link.paidrequiredThe event type.
dataobjectrequiredThe payment that triggered the event. Amounts are IQD integer strings.
{
"event": "payment.failed",
"data": {
"paymentId": "…",
"status": "FAILED",
"amount": "50000",
"isLive": true,
"provider": "ALQASEH",
"customerPhone": "+9647801234567",
"invoiceId": null,
"paymentLinkId": "7c9e…",
"customerId": null,
"gatewayTxnId": "alq_…",
"businessId": "3a1d…"
}
}payment.refundedpayment.refunded
Sent when a previously successful payment is refunded. Same delivery, signing, and retry rules as payment.succeeded.
Payload
eventpayment.succeeded | payment.failed | payment.refunded | invoice.paid | payment_link.paidrequiredThe event type.
dataobjectrequiredThe payment that triggered the event. Amounts are IQD integer strings.
{
"event": "payment.refunded",
"data": {
"paymentId": "…",
"status": "REFUNDED",
"amount": "50000",
"isLive": true,
"provider": "ALQASEH",
"customerPhone": "+9647801234567",
"invoiceId": null,
"paymentLinkId": "7c9e…",
"customerId": null,
"gatewayTxnId": "alq_…",
"businessId": "3a1d…"
}
}invoice.paidinvoice.paid
Sent when an invoice is fully paid. Only fired for payments linked to an invoice — data.invoiceId is populated. Same delivery, signing, and retry rules as payment.succeeded.
Payload
eventpayment.succeeded | payment.failed | payment.refunded | invoice.paid | payment_link.paidrequiredThe event type.
dataobjectrequiredThe payment that triggered the event. Amounts are IQD integer strings.
{
"event": "invoice.paid",
"data": {
"paymentId": "…",
"status": "SUCCEEDED",
"amount": "85000",
"isLive": true,
"provider": "ALQASEH",
"customerPhone": "+9647801234567",
"invoiceId": "f01a…",
"paymentLinkId": null,
"customerId": null,
"gatewayTxnId": "alq_…",
"businessId": "3a1d…"
}
}payment_link.paidpayment_link.paid
Sent when a payment link is paid. Only fired for payments linked to a payment link — data.paymentLinkId is populated. Same delivery, signing, and retry rules as payment.succeeded.
Payload
eventpayment.succeeded | payment.failed | payment.refunded | invoice.paid | payment_link.paidrequiredThe event type.
dataobjectrequiredThe payment that triggered the event. Amounts are IQD integer strings.
{
"event": "payment_link.paid",
"data": {
"paymentId": "…",
"status": "SUCCEEDED",
"amount": "50000",
"isLive": true,
"provider": "ALQASEH",
"customerPhone": "+9647801234567",
"invoiceId": null,
"paymentLinkId": "7c9e…",
"customerId": null,
"gatewayTxnId": "alq_…",
"businessId": "3a1d…"
}
}