x
loader
J o y n d P a y

API Documentation

API Documentation

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.

Overview

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:

  • Initiate a payment — Send us the amount, currency, and two URLs (callback and IPN). We return a checkout link (redirect_url) and an order_id.
  • Verify payment status — Call our verify endpoint with the 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.
  • Receive callbacks (IPN) — We send a POST request to your 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.

Fees (live only)

When you use the Live API (from Business API key), the customer pays:

  1. Payment processing fee — A fee per transaction (varies by payment method and currency).
  2. Platform fee2.3% + 1 per transaction

Total charge = amount + payment processing fee + platform fee. Sandbox (test) does not apply the platform fee.

1. Get your API keys

Every API request must include your Public Key and Secret Key. Here is how to get them:

  1. Log in to your account at Log In.
  2. Go to DashboardBusiness Api key (or open https://joyndpay.com/user/api when logged in)).
  3. Click Generate API Key if you do not have one yet.
  4. Copy your Public Key and Secret Key. You will send these in the request headers for every API call.

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.

2. Authentication

All API requests must include your keys in the HTTP headers. Use the exact header names below (case-sensitive):

HeaderDescription
ApiKeyYour Public Key from the Business API key page.
SecretKeyYour Secret Key from the same page.
Content-Typeapplication/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.

How to connect (step-by-step)

Follow these steps to integrate payments:

  1. Get your API keys — From DashboardBusiness Api key, copy your Public Key and Secret Key. Use Sandbox for testing.
  2. Call Initiate payment — Send a 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).
  3. Use the response — From the response, take data.redirect_url (the checkout link) and data.order_id (the reference for this payment).
  4. Send the customer to pay — Choose one:
    • With redirect — Send the customer to 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.
    • Without redirect — Open 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 checkout page (where the customer pays)

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
Option A: Redirect to checkout

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.

Option B: No redirect — keep the user on your page

If you do not want the user to leave your site or app:

  1. Do not set window.location or redirect() to the checkout URL.
  2. Open the checkout in a new tab: window.open(data.data.redirect_url, '_blank') — or show a "Pay now" link or button that points to redirect_url.
  3. Your original page stays open. To know when payment is done:
    • Polling — Call 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 — We POST to your 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.

Key concepts

Before you start, understand these terms:

order_id
A unique reference you give to each payment (or we generate one for you). You use it to check payment status and to recognise the order in the IPN callback. Keep it between 10 and 20 characters if you provide it yourself.
redirect_url
The URL we return after you call Initiate payment. Send your customer to this URL so they can pay by Card or Mobile money on our secure checkout page.
callback_url
The page where we send the customer after they finish payment (success or failure). For example your "Thank you" or "Order complete" page.
ipn_url
(Instant Payment Notification) A URL on your server that we call with a POST request when the payment status changes. Use it to update your database and fulfil orders. Must be HTTPS in live mode.
Sandbox vs Live
Sandbox keys are for testing: no real money is charged. Live keys process real payments. Always test with Sandbox before switching to Live.

Payment without redirect

Use 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.

When to use it

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).

Step-by-step
  1. Call POST /api/initiate/payment and get data.redirect_url and data.order_id.
  2. Open the checkout in a new tab: window.open(data.data.redirect_url, '_blank'). Do not use window.location or the user will leave your page.
  3. On your page, every 2–3 seconds call POST /api/verify/payment with body { "order_id": data.data.order_id } and the same ApiKey and SecretKey headers.
  4. The response looks like: { "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.

Laravel (no redirect)
// 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
React (no redirect)
// 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);
};
Python (no redirect)
# 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"

3. Initiate payment

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.

Request headers
  • ApiKey — Your Public Key
  • SecretKey — Your Secret Key
  • Content-Type: application/json
Request body (JSON)

Send a JSON object with the following fields. Required fields must be present and valid.

FieldTypeRequiredDescription
currencystringYesISO currency code, e.g. USD, UGX. Must be a currency we support for the checkout.
amountnumberYesThe amount the customer should pay. Must be greater than zero.
order_idstringOptionalYour 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_urlstringYesFull 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_urlstringYesFull URL where we redirect the customer after they finish payment (e.g. your thank-you or order-complete page).
meta.customer_namestringNoCustomer name (optional, for your reference).
meta.customer_emailstringNoCustomer email (optional).
meta.descriptionstringNoShort description of the order (optional, max 500 characters).
Response (success)

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.

Response (error)

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.

4. Verify payment

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:

  • After the customer returns to your callback_url — call Verify with the order_id to confirm whether the payment succeeded before showing a success message.
  • In the no-redirect flow — poll this endpoint every 2–3 seconds with the order_id until data.status is success or failed, then stop polling and update your UI.
Request headers

ApiKey, SecretKey, Content-Type: application/json

Request body (JSON)
{ "order_id": "YOUR_ORDER_ID" }

Use the same order_id you received from Initiate payment or that you sent when creating the payment.

Response
{
  "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.

5. IPN (Instant Payment Notification)

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:

  • Respond with 200 quickly so we know you received the notification.
  • Process the payload asynchronously if needed (e.g. queue a job) and update your database.
  • Make your handler idempotent: we may send the same notification more than once; use order_id and status to avoid duplicate fulfilment.
  • In live mode use HTTPS for ipn_url.

You can also call Verify payment from your IPN handler to double-check the status before updating your database.

Testing and going live

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:

  • Switch to your Live API keys from the Business API key page.
  • Ensure ipn_url and callback_url use HTTPS.
  • Handle errors: check the status and error fields in responses and show a clear message to the user.
  • On your thank-you or callback page, call Verify payment with the 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.

Code examples: Laravel

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']

Code examples: React

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);
  }
};

Code examples: Python

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'))

Example: cURL

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"
  }'

Example: JavaScript (fetch)

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;
}

The Website uses cookies

This website uses cookies to improve user experience. By using our website you consent to all cookies in accordance with our Cookie Policy. see more Accept Close