Getting Started
The Jɛmɛnipay API enables you to initiate Mobile Money payments, manage recurring subscriptions, generate payment links, and track transaction states in real time.
To access the API you need an access token, a public API key, and a secret key. Sign in to your dashboard to retrieve your credentials.
BASE_URL: https://jemeni.net/api
Required Headers
The following headers are required on every request (sandbox and production):
| Header | Value | Description |
|---|---|---|
Accept |
application/json |
required — Any request missing this header is rejected with 403. |
Content-Type |
application/json |
required — For requests with a body (POST). |
auth-apiKey |
Your public API key | required — API key from your dashboard. |
auth-token |
Your access token | required — User token linked to your account. |
auth-timestamp |
UNIX timestamp (seconds) | required — Must be within ±5 minutes of server time. Prevents replay attacks. |
auth-signature |
HMAC-SHA512 hash | required — See Signature Generation below. |
sandbox |
true |
required — sandbox only — Missing or false → request rejected with 403. |
Signature Generation
The signature is an HMAC-SHA512 — not a plain SHA512. It is computed by concatenating the following components without any separator:
HMAC-SHA512(
message = SK + AK + METHOD + URL + BODY + TIMESTAMP,
key = SK
)
| Variable | Description |
|---|---|
SK | Your secret key (secret_key from the dashboard) |
AK | Your public API key (auth-apiKey header value) |
METHOD | HTTP method in uppercase: GET or POST |
URL | Full URL including query string for GET requests · Base URL only (no query string) for POST requests |
BODY | Raw JSON request body — empty string "" for GET requests |
TIMESTAMP | Exact value sent in the auth-timestamp header (UNIX seconds) |
🔐 Security Notes
- Never expose your
SKin client-side code (front-end, mobile app). - The
auth-timestampmust be fresh (±5 min) — requests outside this window are rejected. - All requests must be sent over HTTPS.
- In production, replace
pk_test_keys with your live keys and remove thesandboxheader.
Check Transaction Status
Retrieves the current state of a transaction by its ID. Use this endpoint for polling or to verify the final outcome of a payment.
📎 Endpoint
GET {BASE_URL}/sandbox/payments/{id}
📦 Example Request (cURL)
curl -X GET "{BASE_URL}/sandbox/payments/01jz0p4y6g5jxe0abeazw12j55" \
-H "Accept: application/json" \
-H "sandbox: true" \
-H "auth-apiKey: pk_test_37........bf76b6dc218" \
-H "auth-timestamp: 1700000000" \
-H "auth-token: your_access_token" \
-H "auth-signature: your_signature_hash"
🧾 Required Headers
| Header | Type | Description |
|---|---|---|
Accept | String | required — Must be application/json. |
auth-apiKey | String | required — Your public API key. |
auth-timestamp | String | required — Current UNIX timestamp. |
auth-token | String | required — Your access token. |
auth-signature | String | required — HMAC-SHA512 hash. For GET, use the full URL (with query string) as the URL component. |
sandbox | String | required — sandbox only — Value: true. |
📤 Example Response
{
"status": "success",
"message": "Transaction retrieved successfully.",
"data": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"amount": "9000.00",
"state": 2,
"customer": {
"id": "01jxk881aztej3mnd5aj4yt7hk",
"first_name": "Kadiatou",
"last_name": "Maiga",
"phone": "98745632",
"country_code": "ml"
},
"client": {
"id": "01jwvrvjswcex8n3ajv0njfpx6",
"name": "KTM SHOP",
"status": 1
},
"payment": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"amount": "9000.00",
"due_at": "2025-06-30 16:09:05",
"status": "completed",
"payment_reference": "JP-ABC123456"
}
}
}
Refer to the Transaction State Codes table in the Initiate a Transaction section for the meaning of each state value.
Initiate a Transaction
Triggers a payment request to a customer's Mobile Money account. The customer receives a notification and must confirm the payment on their phone.
📎 Endpoint
POST {BASE_URL}/sandbox/payments
📦 Example Request (cURL)
curl -X POST "{BASE_URL}/sandbox/payments" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "sandbox: true" \
-H "auth-apiKey: pk_test_37........bf76b6dc218" \
-H "auth-timestamp: 1700000000" \
-H "auth-token: your_access_token" \
-H "auth-signature: your_signature_hash" \
--data '{
"customer_phone": "98745632",
"amount": 9000,
"country_code": "ml",
"notifiable": true,
"lang": "fr",
"return_url": "https://your-site.ml/payment/callback"
}'
🧾 Required Headers
| Header | Type | Description |
|---|---|---|
Accept | String | required — Must be application/json. |
auth-apiKey | String | required — Your public API key. |
auth-timestamp | String | required — Current UNIX timestamp (used in signature). |
auth-token | String | required — Your access token. |
auth-signature | String | required — HMAC-SHA512 hash of the request. |
sandbox | String | required — sandbox only — Value: true. |
📨 Request Body
| Field | Type | Description | |
|---|---|---|---|
customer_phone | String | required | Customer's phone number (without country code). Sandbox: 98745632 |
amount | Number | required | Amount to charge in FCFA (positive integer). |
country_code | String | required | ISO 2-letter country code in lowercase. e.g. ml for Mali. |
notifiable | Boolean | required | true to send a push/SMS notification to the customer. |
lang | String | optional | Notification language: fr (default) or en. |
return_url | String | optional | Redirect URL after payment confirmation. |
version | String | optional | API version (default: v1). |
📤 Example Response
{
"status": "success",
"message": "Payment initiated successfully.",
"data": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"amount": 9000,
"state": 1,
"reference": "6862a8914c1d5bbabd99cf54",
"created_at": "2025-06-30 15:09:05",
"customer": {
"id": "01jxk881aztej3mnd5aj4yt7hk",
"first_name": "Kadiatou",
"last_name": "Maiga",
"phone": "98745632",
"country_code": "ml"
},
"client": {
"id": "01jwvrvjswcex8n3ajv0njfpx6",
"name": "KTM SHOP",
"client_type_code": "shop"
},
"payment": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"amount": "9000.00",
"status": "pending",
"payment_reference": "JP-ABC123456"
}
}
}
🔢 Transaction State Codes (state)
| Value | Status | Description |
|---|---|---|
| 0 | Created | Transaction created, pending initiation. |
| 1 | Pending | Notification sent, awaiting customer confirmation. |
| 2 | Completed | Payment confirmed and funds received. |
| 3 | Failed | Payment declined or expired. |
Your Plans
Manage the subscription plans for your merchant account. Use the returned id
and frequency_code when creating a subscription.
GET List Plans
{BASE_URL}/sandbox/plans · {BASE_URL}/live/plans
Returns all active plans for your account.
curl -X GET "{BASE_URL}/sandbox/plans" \
-H "Accept: application/json" \
-H "auth-apiKey: pk_test_fce1••••••c821c60e" \
-H "auth-timestamp: 1700000000" \
-H "auth-token: your_access_token" \
-H "auth-signature: your_signature_hash"
🧾 Required Headers
| Header | Description |
|---|---|
Accept | Must be application/json. |
auth-apiKey | Your public API key (pk_test_… or pk_live_…). |
auth-timestamp | Current UNIX timestamp. |
auth-token | Your access token. |
auth-signature | HMAC-SHA512 hash of the request. |
📤 Example Response
{
"status": "Success",
"message": "List of subscription plans",
"data": [
{
"id": "01jz0p4y6g5jxe0abeazw12j55",
"name": "Basic Plan",
"price": "2000.00",
"frequency_code": "monthly",
"archived": false
}
]
}
POST Create a Plan
{BASE_URL}/sandbox/plans · {BASE_URL}/live/plans
Create a new subscription plan for your account.
curl -X POST "{BASE_URL}/sandbox/plans" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "auth-apiKey: pk_test_fce1••••••c821c60e" \
-H "auth-timestamp: 1700000000" \
-H "auth-token: your_access_token" \
-H "auth-signature: your_signature_hash" \
--data '{
"name": "Premium Plan",
"price": 5000,
"frequency_code": "monthly",
"vat_rate_id": "01jwvrvjswcex8n3ajv0njfpx6"
}'
🧾 Required Headers
| Header | Description |
|---|---|
Accept | Must be application/json. |
Content-Type | Must be application/json. |
auth-apiKey | Your public API key (pk_test_… or pk_live_…). |
auth-timestamp | Current UNIX timestamp. |
auth-token | Your access token. |
auth-signature | HMAC-SHA512 hash of the request. |
📨 Request Body
| Field | Type | Description | |
|---|---|---|---|
name | String | required | Plan name (max 255 chars). |
price | Numeric | required | Amount in FCFA. Minimum: 100. |
frequency_code | String | required | Billing cycle — one of weekly, monthly, yearly. |
vat_rate_id | String | required | Tax rate identifier (ULID) from your account's tax rates. |
📤 Example Response
{
"status": "Success",
"message": "Plan créé avec succès.",
"data": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"name": "Premium Plan",
"price": "5000.00",
"frequency_code": "monthly",
"archived": false
}
}
Create a Subscription
Creates a recurring subscription for a customer based on a plan and billing frequency. Retrieve available plans first via Retrieve Subscription Plans.
📎 Endpoint
POST {BASE_URL}/sandbox/subscriptions
curl -X POST "{BASE_URL}/sandbox/subscriptions" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "sandbox: true" \
-H "auth-apiKey: pk_test_fce1••••••c821c60e" \
-H "auth-timestamp: 1700000000" \
-H "auth-token: your_access_token" \
-H "auth-signature: your_signature_hash" \
--data '{
"phone": "98745632",
"plan_id": "01jz0p4y6g5jxe0abeazw12j55",
"frequency_code": "monthly",
"start_at": "2025-07-01"
}'
🧾 Required Headers
| Header | Type | Description |
|---|---|---|
Accept | String | required — Must be application/json. |
auth-apiKey | String | required — Your public API key. |
auth-timestamp | String | required — Current UNIX timestamp. |
auth-token | String | required — Your access token. |
auth-signature | String | required — HMAC-SHA512 hash of the request. |
sandbox | String | required — sandbox only — Value: true. |
📨 Request Body
| Field | Type | Description | |
|---|---|---|---|
phone | String | required | Customer's phone number. Sandbox: 98745632 |
plan_id | String | required | Plan identifier from Retrieve Subscription Plans. |
frequency_code | String | required | Billing frequency — one of weekly, monthly, yearly. |
start_at | String | required | Subscription start date — format: YYYY-MM-DD. |
📤 Example Response
{
"status": "success",
"message": "Subscription created successfully",
"data": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"client_id": "01jwvrvjswcex8n3ajv0njfpx6",
"plan_id": "01jz0p4y6g5jxe0abeazw12j55",
"customer_id": "01jxk881aztej3mnd5aj4yt7hk",
"status": "active",
"start_at": "2025-07-01 00:00:00",
"expire_at": "2026-01-01 00:00:00",
"archived": false,
"renewal_date": null,
"renewed": false
}
}
Cancel a Subscription
Disables the active subscription linked to a customer's phone number. The subscription is archived immediately and no further charges will be made.
📎 Endpoint
DELETE {BASE_URL}/sandbox/subscriptions
curl -X DELETE "{BASE_URL}/sandbox/subscriptions" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "sandbox: true" \
-H "auth-apiKey: pk_test_••••••••••••••••••••" \
-H "auth-timestamp: 1700000000" \
-H "auth-token: your_access_token" \
-H "auth-signature: your_signature_hash" \
--data '{
"phone": "98745632"
}'
🧾 Required Headers
| Header | Type | Description |
|---|---|---|
Accept | String | required — Must be application/json. |
auth-apiKey | String | required — Your public API key. |
auth-timestamp | String | required — Current UNIX timestamp. |
auth-token | String | required — Your access token. |
auth-signature | String | required — HMAC-SHA512 hash of the request. |
sandbox | String | required — sandbox only — Value: true. |
📨 Request Body
| Field | Type | Description | |
|---|---|---|---|
phone | String | required | Customer's phone number. Sandbox: 98745632 |
📤 Example Response
{
"status": "success",
"message": "Customer subscription disabled successfully",
"data": {
"id": "01jxk881aztej3mnd5aj4yt7hk",
"first_name": "Kadiatou",
"last_name": "Maiga",
"email": "kmaiga@ktminnov.com",
"phone": "98745632",
"country_code": "ml"
}
}
Generate a Payment Link
Jemeni.NET allows you to receive payments without needing to integrate the API. You can create secure payment links and share them directly with your customers.
🔧 How It Works
- Step 1 - Generate the Link: Log into your dashboard and create a new payment link for your customer.
- Step 2 - Share the Link: Send the link via SMS, email, WhatsApp, or any messaging platform.
- Step 3 - Receive Payment: The customer clicks the link, fills out payment info, and completes the transaction securely.
✅ Benefits
- No technical integration required.
- Perfect for freelancers, small businesses, or quick one-time charges.
- Real-time payment tracking via your dashboard.
Use cases: Remote sales, invoice payments, social commerce, donation campaigns, and more.
Ready to start? Log in to generate your first payment link today.
Send a Payment Notification
The payment notification system lets you trigger payment requests directly to a customer's Jɛmɛnipay account, without using a public payment link.
🔧 How It Works
- Step 1 - Initiate a Notification: From your system or dashboard, send a payment request tied to the customer's phone number.
- Step 2 - Customer is Notified: The customer receives an instant notification within their Jɛmɛnipay app or channel.
- Step 3 - Direct Payment: The customer can pay securely, without the need to click on any external link.
✅ Benefits
- No link sharing required.
- Secure and personalized experience via Jɛmɛnipay interface.
- Ideal for in-app payments, recurring charges, or enterprise scenarios.
Use case examples: utility bill reminders, subscription renewals, loan installments, or payment follow-ups.
Ready to try it? Log in to your dashboard and start sending payment notifications to your customers.
Webhook Integration
Webhooks allow your server to receive real-time HTTP notifications when key events occur on Jɛmɛnipay
(successful payments, failed transactions, subscription changes, etc.).
Each delivery is signed with X-Jemeni-Signature (HMAC-SHA512) so you can verify authenticity.
⚡ How It Works
- Register an endpoint URL via the Dashboard or the API.
- Choose the events to listen to (
payment.success,subscription.created, …). - Jɛmɛnipay sends a signed
POSTrequest to your URL when an event fires. - Your server validates the signature and responds
200 OK. - On failure, Jɛmɛnipay retries automatically — 3 attempts with exponential backoff (1 min → 5 min → 30 min).
- If a fallback URL is configured, it is tried immediately before the retry queue.
📋 Available Events
| Event | Triggered when |
|---|---|
payment.success | A transaction is validated (state = 2). |
payment.failed | A transaction is failed or cancelled (state = 3 / 4). |
subscription.created | A new subscription is created. |
subscription.renewed | A subscription is renewed. |
subscription.disabled | A subscription is disabled / cancelled. |
Webhook API Endpoints
Each endpoint is shown with its sandbox URL (/sandbox/…).
Replace with /live/… and use a pk_live_… key in production.
curl -X POST "{BASE_URL}/sandbox/webhooks" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "auth-apiKey: pk_test_fce1••••••c821c60e" \
--data '{
"url": "https://votre-serveur.com/webhooks",
"fallback_url": "https://backup.votre-serveur.com/webhooks",
"events": ["payment.success", "payment.failed"],
"description": "Notifications paiements production"
}'
POST Register a Webhook
{BASE_URL}/sandbox/webhooks
Register a new webhook endpoint for your account. Returns a secret key shown only once.
| Field | Type | Description | |
|---|---|---|---|
url | String | required | Your endpoint URL. Must be a valid HTTPS URL (max 500 chars). |
events | Array | required | List of event types to subscribe to. At least one required. |
fallback_url | String | optional | Secondary URL tried immediately if url returns a non-2xx response. |
description | String | optional | Label for your reference (max 255 chars). |
📤 Example Response
{
"status": "Success",
"message": "Webhook enregistré avec succès.",
"data": {
"webhook": {
"id": "01jz0p4y6g5jxe0abeazw12j55",
"url": "https://votre-serveur.com/webhooks",
"fallback_url": "https://backup.votre-serveur.com/webhooks",
"events": ["payment.success", "payment.failed"],
"description": "Notifications paiements production",
"is_active": true,
"created_at": "2026-03-19T10:00:00.000000Z"
},
"secret": "a3f9c1e2...d84b"
}
}
⚠️ Save the secret immediately. It is shown only once and is used to verify all incoming webhook signatures.
GET List Webhooks
{BASE_URL}/sandbox/webhooks
List all registered webhooks for your account.
📦 Example Request (cURL)
curl -X GET "{BASE_URL}/sandbox/webhooks" \
-H "Accept: application/json" \
-H "auth-apiKey: pk_test_fce1••••••c821c60e"
📤 Example Response
{
"status": "Success",
"message": "Liste des webhooks",
"data": [
{
"id": "01jz0p4y6g5jxe0abeazw12j55",
"url": "https://votre-serveur.com/webhooks",
"fallback_url": null,
"events": ["payment.success", "payment.failed"],
"description": "Notifications prod",
"is_active": true,
"created_at": "2026-03-19T10:00:00.000000Z"
}
]
}
DELETE Delete a Webhook
{BASE_URL}/sandbox/webhooks/{id}
Permanently delete a webhook. All associated logs are also removed.
curl -X DELETE "{BASE_URL}/sandbox/webhooks/01jz0p4y6g5jxe0abeazw12j55" \
-H "Accept: application/json" \
-H "auth-apiKey: pk_test_fce1••••••c821c60e"
📤 Example Response
{ "status": "Success", "message": "Webhook supprimé.", "data": null }
POST Toggle Active / Inactive
{BASE_URL}/sandbox/webhooks/{id}/toggle
Toggles the webhook's is_active flag. Inactive webhooks receive no deliveries.
📤 Example Response
{ "status": "Success", "message": "Statut mis à jour.", "data": { "is_active": false } }
POST Send a Test Delivery
{BASE_URL}/sandbox/webhooks/{id}/test
Sends a synthetic payment.success payload to the webhook URL immediately. Useful to validate your endpoint before going live.
📤 Example Response
{
"status": "Success",
"message": "Payload de test envoyé.",
"data": { "http_status": 200, "response_body": "OK" }
}
GET Delivery Logs
{BASE_URL}/sandbox/webhooks/{id}/logs
Returns the last 50 delivery attempts for a webhook, ordered by most recent.
📤 Example Response
{
"status": "Success",
"message": "Historique des livraisons",
"data": [
{
"id": "01jz1a2b3c4d5e6f7g8h9i0j1k",
"event_type": "payment.success",
"http_status": 200,
"attempts": 1,
"delivered_at": "2026-03-19T10:05:00.000000Z",
"next_retry_at": null,
"created_at": "2026-03-19T10:04:58.000000Z"
}
]
}
📋 Log Status Reference
| Condition | Meaning |
|---|---|
delivered_at not null | ✅ Delivered successfully. |
next_retry_at not null | ⏳ Pending — scheduled for retry. |
delivered_at null, next_retry_at null, attempts > 0 | ❌ Permanently failed — all retries exhausted. |
POST Retry a Failed Delivery
{BASE_URL}/sandbox/webhooks/logs/{logId}/retry
Re-queues a permanently failed log entry for immediate re-delivery.
📤 Example Response
{ "status": "Success", "message": "Relance en cours...", "data": null }
Webhook Events & Security
📨 Payload — payment.success
{
"event": "payment.success",
"data": {
"transaction_id": "01jz0p4y6g5jxe0abeazw12j55",
"reference": "REF-20260319-001",
"amount": "5000",
"state": 2,
"state_label": "Validated",
"client_id": "01jwvrvjswcex8n3ajv0njfpx6",
"customer_id": "01jxk881aztej3mnd5aj4yt7hk",
"payment_id": "01jxk881aztej3mnd5aj4yt7hk",
"notifiable_url": "https://votre-serveur.com/notify",
"external_reference": "ORDER-9876",
"updated_at": "2026-03-19T10:05:00.000000Z"
}
}
📨 Payload — subscription.created
{
"event": "subscription.created",
"data": {
"subscription_id": "01jz0p4y6g5jxe0abeazw12j55",
"client_id": "01jwvrvjswcex8n3ajv0njfpx6",
"plan_id": "01jxk881aztej3mnd5aj4yt7hk",
"status": "active",
"start_at": "2026-03-19T00:00:00.000000Z",
"expire_at": "2027-03-19T00:00:00.000000Z"
}
}
🔐 Signature Verification
Every webhook request contains the header X-Jemeni-Signature.
Compute the expected HMAC-SHA512 of the raw request body using your webhook secret and compare with hash_equals.
<?php
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha512', $payload, YOUR_WEBHOOK_SECRET);
$received = $_SERVER['HTTP_X_JEMENI_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($payload, true);
// Process $data['event'] ...
http_response_code(200);
echo 'OK';
🔁 Retry Policy
| Attempt | Delay before retry |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry (final) | 30 minutes |
If a fallback_url is configured, it is attempted immediately when the primary URL
returns a non-2xx response — before the retry queue. A successful fallback marks the delivery as done.
✅ Best Practices
- Always respond with
200 OKas fast as possible — process the payload asynchronously. - Use
hash_equals(not===) to avoid timing attacks when comparing signatures. - Store your webhook secret securely (environment variable, secret manager).
- Use
transaction_idorexternal_referenceto deduplicate events — retries may send the same event more than once. - Register a
fallback_urlon a different host or cloud region for high availability.
HTTP Codes
The Jɛmɛnipay API uses the following error codes:
HTTP Code Descriptions for API Documentation
2xx Success
- 200 OK: The request was successful, and the response contains the requested data.
- 201 Created: The request was successful, and a new resource was created.
- 202 Accepted: The request has been accepted for processing but is not yet complete.
- 204 No Content: The request was successful, but there is no content to return.
3xx Redirection
- 301 Moved Permanently: The resource has been permanently moved to a new URL.
- 302 Found: The resource is temporarily located at a different URL.
- 304 Not Modified: The resource has not been modified since the last request.
4xx Client Errors
- 400 Bad Request: The server could not understand the request due to invalid syntax.
- 401 Unauthorized: Authentication is required, or it has failed.
- 403 Forbidden: The server understands the request but refuses to authorize it.
- 404 Not Found: The requested resource could not be found.
- 429 Too Many Requests: The client has sent too many requests in a given amount of time (rate limiting).
5xx Server Errors
- 500 Internal Server Error: The server encountered an unexpected condition.
- 501 Not Implemented: The server does not support the functionality required to fulfill the request.
- 502 Bad Gateway: The server received an invalid response from the upstream server.
- 503 Service Unavailable: The server is currently unable to handle the request due to maintenance or overload.
- 504 Gateway Timeout: The server did not receive a timely response from the upstream server.
Custom Error Ranges
3000-3999 (Custom Redirection Codes)
Reserved for API-specific redirection behaviors.
4000-4999 (Custom Client Errors)
- 4001 Invalid Input Data: The input data provided is invalid.
- 4002 Resource Constraint Exceeded: A request exceeded a defined limit.
5000-5999 (Custom Server Errors)
- 5001 Database Connection Failed: The server could not connect to the database.
- 5002 Service Dependency Timeout: A dependent service did not respond in time.