Connect your website or app to accept payments. Use the API to create payment links and verify transactions.
This guide explains every step in detail so you can integrate payments quickly and correctly.
The Payment API lets you accept Card and Mobile money payments on your website or app without building your own checkout. Here is what you can do:
redirect_url) and an order_id.order_id to check if the payment is pending, success, or failed. Use this when the user returns to your site or when you poll in the no-redirect flow.ipn_url when the payment status changes so your server can update the order and fulfil it.Base URL for all API requests:
https://joyndpay.com/api
Example: Initiate payment is POST https://joyndpay.com/api/initiate/payment, Verify payment is POST https://joyndpay.com/api/verify/payment.
When you use the Live API (from Business API key), the customer pays:
Total charge = amount + payment processing fee + platform fee. Sandbox (test) does not apply the platform fee.
Every API request must include your Public Key and Secret Key. Here is how to get them:
Sandbox vs Live: Use Sandbox keys for testing — no real money is charged. Use Live keys only when you are ready to accept real payments. Always test with Sandbox first.
Security: Never expose your Secret Key in frontend code or in public repositories. Call the API from your backend, or ensure the Secret Key is only used in server-side requests.
All API requests must include your keys in the HTTP headers. Use the exact header names below (case-sensitive):
| Header | Description |
|---|---|
ApiKey | Your Public Key from the Business API key page. |
SecretKey | Your Secret Key from the same page. |
Content-Type | application/json (required for POST requests with a JSON body). |
Example: ApiKey: your_public_key_here, SecretKey: your_secret_key_here. If the keys are wrong or missing, the API will return an error.
Follow these steps to integrate payments:
POST request to https://joyndpay.com/api/initiate/payment with headers ApiKey, SecretKey, Content-Type: application/json and a JSON body containing currency, amount, ipn_url, callback_url and optionally order_id (if you omit order_id we generate one and return it).data.redirect_url (the checkout link) and data.order_id (the reference for this payment).data.redirect_url in the same tab (e.g. window.location.href = redirect_url or redirect($url)). They complete payment on our page, then we send them to your callback_url.data.redirect_url in a new tab with window.open(redirect_url, '_blank'). Your page stays open. Poll POST /api/verify/payment with order_id every 2–3 seconds, or rely on your ipn_url callback, until data.status is success or failed.After payment, we also POST to your ipn_url so your server can update the order. You can confirm status anytime by calling Verify payment with the same order_id.
The redirect_url we return points to our hosted checkout page. On that page the customer can choose to pay by Card or Mobile money and enter their details. You do not need to build a payment form — we provide it.
Checkout URL format:
https://joyndpay.com/make/payment/{mode}/{utr}
{mode} — live for real payments, test for sandbox. Matches the API key you used.{utr} — A unique transaction ID. It is returned in data.id from Initiate payment (you usually just pass data.redirect_url as-is to the customer).Example:
https://joyndpay.com/make/payment/live/5e5964d8-15d0-42bd-80b2-b9828801594b
Send the customer to data.redirect_url in the same window — for example window.location.href = data.data.redirect_url in JavaScript or return redirect($url) in Laravel. The customer pays on our page; when they are done we redirect them to your callback_url. This is the simplest approach and works for most websites and apps.
If you do not want the user to leave your site or app:
window.location or redirect() to the checkout URL.window.open(data.data.redirect_url, '_blank') — or show a "Pay now" link or button that points to redirect_url.POST /api/verify/payment with body { "order_id": "..." } every 2–3 seconds. When the response has data.status === "success" or "failed", stop polling and update your UI.ipn_url when status changes. Your server can update the order and then notify the frontend (e.g. via a poll that reads from your database or via WebSocket).See the section Payment without redirect below for full code examples.
Before you start, understand these terms:
order_idredirect_urlcallback_urlipn_urlUse this flow when you want the user to stay on your page (e.g. single-page app or when you do not want to navigate away). The checkout opens in a new tab; you detect when payment is done by polling the Verify payment endpoint or by handling the IPN callback.
Choose "no redirect" when: the user should not leave your site, you want to show a loading state on your page while they pay in another tab, or you need to update the same page when payment completes. Choose "redirect" when: a full redirect to our checkout and then to your thank-you page is acceptable (simplest integration).
POST /api/initiate/payment and get data.redirect_url and data.order_id.window.open(data.data.redirect_url, '_blank'). Do not use window.location or the user will leave your page.POST /api/verify/payment with body { "order_id": data.data.order_id } and the same ApiKey and SecretKey headers.{ "status": "success", "data": { "order_id": "...", "status": "pending"|"success"|"failed" } }. When data.data.status is "success" or "failed", stop polling and update your UI (e.g. show a success message or an error).Alternatively you can rely on your ipn_url callback: when we POST to it, your server updates the order and can notify the frontend (e.g. via a separate poll that reads from your database).
Example code: open checkout in new tab and poll until success or failed.
// 1. Initiate payment, return redirect_url + order_id to frontend (do NOT redirect)
$response = \Illuminate\Support\Facades\Http::withHeaders([
'ApiKey' => 'YOUR_PUBLIC_KEY',
'SecretKey' => 'YOUR_SECRET_KEY',
])->post('https://joyndpay.com/api/initiate/payment', [
'currency' => 'UGX',
'amount' => 10000,
'order_id' => $orderId, // optional; we return one if omitted
'ipn_url' => route('your.ipn.route'),
'callback_url' => route('your.thankyou.page'),
]);
$data = $response->json();
return response()->json([
'redirect_url' => $data['data']['redirect_url'] ?? null,
'order_id' => $data['data']['order_id'] ?? $orderId,
]);
// 2. Frontend: window.open(redirect_url, '_blank') — user pays in new tab
// 3. Frontend: poll POST /api/verify/payment with { "order_id": order_id } every 2–3s until data.data.status is "success" or "failed"
// Or: rely on ipn_url callback and have frontend poll your backend
// User stays on your page; checkout opens in new tab. Poll verify/payment until success or failed.
const [orderId, setOrderId] = useState('');
const [paymentStatus, setPaymentStatus] = useState('pending'); // 'pending' | 'success' | 'failed'
const payNow = async () => {
const res = await fetch('https://joyndpay.com/api/initiate/payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'ApiKey': 'KEY', 'SecretKey': 'SECRET' },
body: JSON.stringify({
currency: 'UGX',
amount: 10000,
ipn_url: 'https://yoursite.com/ipn',
callback_url: 'https://yoursite.com/thank-you',
}),
});
const data = await res.json();
if (data.status !== 'success' || !data.data?.redirect_url) {
console.error(data.error || data.message); return;
}
const oid = data.data.order_id;
setOrderId(oid);
window.open(data.data.redirect_url, '_blank'); // New tab — user stays here
const poll = setInterval(async () => {
const v = await fetch('https://joyndpay.com/api/verify/payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'ApiKey': 'KEY', 'SecretKey': 'SECRET' },
body: JSON.stringify({ order_id: oid }),
});
const r = await v.json();
if (r.status === 'success' && r.data?.status) {
if (r.data.status === 'success' || r.data.status === 'failed') {
clearInterval(poll);
setPaymentStatus(r.data.status);
}
}
}, 2500);
};
# Backend: initiate and return redirect_url + order_id to frontend (do NOT redirect)
import requests
r = requests.post('https://joyndpay.com/api/initiate/payment',
headers={'ApiKey': 'KEY', 'SecretKey': 'SECRET', 'Content-Type': 'application/json'},
json={'currency': 'UGX', 'amount': 10000, 'ipn_url': 'https://yoursite.com/ipn', 'callback_url': 'https://yoursite.com/thank-you'})
data = r.json()
if data.get('status') == 'success':
redirect_url = data['data']['redirect_url']
order_id = data['data']['order_id']
# Return to frontend: {"redirect_url": redirect_url, "order_id": order_id}
# Frontend must: window.open(redirect_url, '_blank') then poll POST /api/verify/payment with {"order_id": order_id} every 2–3s until data.data.status is "success" or "failed"
POST /api/initiate/payment
This endpoint creates a payment and returns a checkout URL. You then send your customer to that URL so they can pay by Card or Mobile money.
When testing you can omit order_id — we will generate one and return it in data.order_id. Use that value when calling Verify payment and in your IPN handler. For production you can send your own unique reference (10–20 characters) so you can match the payment to an order in your database.
ApiKey — Your Public KeySecretKey — Your Secret KeyContent-Type: application/jsonSend a JSON object with the following fields. Required fields must be present and valid.
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | Yes | ISO currency code, e.g. USD, UGX. Must be a currency we support for the checkout. |
amount | number | Yes | The amount the customer should pay. Must be greater than zero. |
order_id | string | Optional | Your unique order reference. If you omit it we generate one (e.g. ORD-XXXXXXXXXXXX) and return it in the response. If you send it, use 10–20 characters and ensure it is unique per payment. |
ipn_url | string | Yes | Full URL we will call with a POST request when the payment status changes (e.g. https://yoursite.com/webhooks/payment). In live mode this must be HTTPS. Your server should respond with HTTP 200. |
callback_url | string | Yes | Full URL where we redirect the customer after they finish payment (e.g. your thank-you or order-complete page). |
meta.customer_name | string | No | Customer name (optional, for your reference). |
meta.customer_email | string | No | Customer email (optional). |
meta.description | string | No | Short description of the order (optional, max 500 characters). |
When the request is successful, you receive:
{
"status": "success",
"data": {
"id": "uuid",
"order_id": "YOUR_ORDER_ID",
"currency": "UGX",
"amount": 10000,
"redirect_url": "https://joyndpay.com/make/payment/live/...",
"callback_url": "https://yoursite.com/thank-you",
"ipn_url": "https://yoursite.com/ipn"
}
}
Important: Send your customer to data.redirect_url to complete the payment. Use data.order_id when calling Verify payment or when handling the IPN callback.
If the request is invalid or the keys are wrong, you may receive:
{
"status": "error",
"error": ["Error message describing what went wrong"]
}
Common causes: invalid or missing API keys, missing required fields, invalid URLs, or order_id already used. Check the error array and fix the request.
POST /api/verify/payment
Use this endpoint to check the status of a payment. You need the order_id that was returned from Initiate payment (or that you sent when initiating).
When to call Verify payment:
callback_url — call Verify with the order_id to confirm whether the payment succeeded before showing a success message.order_id until data.status is success or failed, then stop polling and update your UI.ApiKey, SecretKey, Content-Type: application/json
{ "order_id": "YOUR_ORDER_ID" }
Use the same order_id you received from Initiate payment or that you sent when creating the payment.
{
"status": "success",
"data": {
"order_id": "YOUR_ORDER_ID",
"status": "pending" | "success" | "failed",
"amount": 10000,
"currency": "UGX"
}
}
Meaning of data.status:
pending — Payment not yet completed. Keep polling or wait for the IPN callback.success — Payment completed successfully. You can fulfil the order.failed — Payment failed or was not completed. Do not fulfil the order.The top-level status indicates whether the verify request itself succeeded. The data.status field is the payment outcome.
We send a POST request to the ipn_url you provided when the payment status changes. This lets your server update the order and fulfil it without relying only on the customer returning to your site.
When we call your IPN: We POST to your URL when the payment completes (success or failure). Your endpoint should respond with HTTP 200 as soon as you have received and processed the payload.
What we send: The payload may include order_id, status, trx_id and other fields. Use order_id to find the order in your database and status to know the outcome.
Best practices:
order_id and status to avoid duplicate fulfilment.ipn_url.You can also call Verify payment from your IPN handler to double-check the status before updating your database.
Testing (Sandbox): Use your Sandbox API keys. No real money is charged. Create a payment and open the checkout URL — you can complete a test payment to see the full flow. Use Verify payment and your ipn_url to confirm your integration.
Going live:
ipn_url and callback_url use HTTPS.status and error fields in responses and show a clear message to the user.order_id before showing success — do not trust the redirect alone.Keep your Secret Key secure: use it only on the server, never in frontend code or in public repositories.
Redirect flow: initiate payment then redirect the customer to the checkout URL. Replace API keys and URLs with your own.
$response = \Illuminate\Support\Facades\Http::withHeaders([
'ApiKey' => 'YOUR_PUBLIC_KEY',
'SecretKey' => 'YOUR_SECRET_KEY',
])->post('https://joyndpay.com/api/initiate/payment', [
'currency' => 'UGX',
'amount' => 10000,
'order_id' => 'ORD-' . uniqid(),
'ipn_url' => 'https://yoursite.com/ipn',
'callback_url' => 'https://yoursite.com/thank-you',
]);
$data = $response->json();
if (($data['status'] ?? '') === 'success' && !empty($data['data']['redirect_url'])) {
// Redirect customer to checkout page (Card or Mobile money)
return redirect()->away($data['data']['redirect_url']);
}
// Handle error: $data['error'] or $data['message']
Redirect flow: call Initiate payment then send the user to the checkout URL. Replace API keys and URLs.
const API_BASE = 'https://joyndpay.com/api';
const apiKey = 'YOUR_PUBLIC_KEY';
const secretKey = 'YOUR_SECRET_KEY';
const initiatePayment = async () => {
const res = await fetch(`${API_BASE}/initiate/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ApiKey': apiKey,
'SecretKey': secretKey,
},
body: JSON.stringify({
currency: 'UGX',
amount: 10000,
order_id: 'ORD-' + Date.now(),
ipn_url: 'https://yoursite.com/ipn',
callback_url: 'https://yoursite.com/thank-you',
}),
});
const data = await res.json();
if (data.status === 'success' && data.data?.redirect_url) {
// Redirect to checkout page: https://joyndpay.com/make/payment/live/{utr}
window.location.href = data.data.redirect_url;
} else {
console.error(data.error || data.message);
}
};
Redirect flow: initiate payment and redirect the user to the returned checkout URL. Works with Flask, Django, or any Python HTTP client.
import requests
API_BASE = 'https://joyndpay.com/api'
API_KEY = 'YOUR_PUBLIC_KEY'
SECRET_KEY = 'YOUR_SECRET_KEY'
response = requests.post(
f'{API_BASE}/initiate/payment',
headers={
'ApiKey': API_KEY,
'SecretKey': SECRET_KEY,
'Content-Type': 'application/json',
},
json={
'currency': 'UGX',
'amount': 10000,
'order_id': 'ORD-' + str(__import__('time').time()),
'ipn_url': 'https://yoursite.com/ipn',
'callback_url': 'https://yoursite.com/thank-you',
},
)
data = response.json()
if data.get('status') == 'success' and data.get('data', {}).get('redirect_url'):
# Redirect user to checkout page: https://joyndpay.com/make/payment/live/{utr}
redirect_url = data['data']['redirect_url']
print('Send customer to:', redirect_url)
# In Flask: return redirect(redirect_url)
# In Django: return HttpResponseRedirect(redirect_url)
else:
print('Error:', data.get('error') or data.get('message'))
Test Initiate payment from the command line. Replace YOUR_PUBLIC_KEY and YOUR_SECRET_KEY with your Sandbox or Live keys.
curl -X POST "https://joyndpay.com/api/initiate/payment" \
-H "ApiKey: YOUR_PUBLIC_KEY" \
-H "SecretKey: YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "UGX",
"amount": 10000,
"order_id": "ORD-'$(date +%s)'",
"ipn_url": "https://yoursite.com/ipn",
"callback_url": "https://yoursite.com/thank-you"
}'
Redirect flow in plain JavaScript. Replace keys and URLs; on success redirect the user to data.data.redirect_url.
const res = await fetch('https://joyndpay.com/api/initiate/payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ApiKey': 'YOUR_PUBLIC_KEY',
'SecretKey': 'YOUR_SECRET_KEY'
},
body: JSON.stringify({
currency: 'UGX',
amount: 10000,
order_id: 'ORD-' + Date.now(),
ipn_url: 'https://yoursite.com/ipn',
callback_url: 'https://yoursite.com/thank-you'
})
});
const data = await res.json();
if (data.status === 'success') {
window.location.href = data.data.redirect_url;
}