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
🧪 Sandbox vs Production
Throughout this documentation, all endpoints are shown with the /sandbox/ prefix — this is the test mode, it does not trigger real charges.

To switch to production, replace /sandbox/ with /live/ in every URL and use your live API key (pk_live_…) instead of the test key (pk_test_…).

Example:
• Test    → {BASE_URL}/sandbox/payment  + pk_test_…
• Production → {BASE_URL}/live/payment      + pk_live_…

Test phone number: 98745632

Required Headers

The following headers are required on every request (sandbox and production):

HeaderValueDescription
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
)
VariableDescription
SKYour secret key (secret_key from the dashboard)
AKYour public API key (auth-apiKey header value)
METHODHTTP method in uppercase: GET or POST
URLFull URL including query string for GET requests · Base URL only (no query string) for POST requests
BODYRaw JSON request body — empty string "" for GET requests
TIMESTAMPExact value sent in the auth-timestamp header (UNIX seconds)

🔐 Security Notes

  • Never expose your SK in client-side code (front-end, mobile app).
  • The auth-timestamp must 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 the sandbox header.

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

HeaderTypeDescription
AcceptStringrequired — Must be application/json.
auth-apiKeyStringrequired — Your public API key.
auth-timestampStringrequired — Current UNIX timestamp.
auth-tokenStringrequired — Your access token.
auth-signatureStringrequired — HMAC-SHA512 hash. For GET, use the full URL (with query string) as the URL component.
sandboxStringrequired — 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

HeaderTypeDescription
AcceptStringrequired — Must be application/json.
auth-apiKeyStringrequired — Your public API key.
auth-timestampStringrequired — Current UNIX timestamp (used in signature).
auth-tokenStringrequired — Your access token.
auth-signatureStringrequired — HMAC-SHA512 hash of the request.
sandboxStringrequired — sandbox only — Value: true.

📨 Request Body

FieldTypeDescription
customer_phoneStringrequired Customer's phone number (without country code). Sandbox: 98745632
amountNumberrequired Amount to charge in FCFA (positive integer).
country_codeStringrequired ISO 2-letter country code in lowercase. e.g. ml for Mali.
notifiableBooleanrequired true to send a push/SMS notification to the customer.
langStringoptional Notification language: fr (default) or en.
return_urlStringoptional Redirect URL after payment confirmation.
versionStringoptional 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)

ValueStatusDescription
0CreatedTransaction created, pending initiation.
1PendingNotification sent, awaiting customer confirmation.
2CompletedPayment confirmed and funds received.
3FailedPayment 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

HeaderDescription
AcceptMust be application/json.
auth-apiKeyYour public API key (pk_test_… or pk_live_…).
auth-timestampCurrent UNIX timestamp.
auth-tokenYour access token.
auth-signatureHMAC-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

HeaderDescription
AcceptMust be application/json.
Content-TypeMust be application/json.
auth-apiKeyYour public API key (pk_test_… or pk_live_…).
auth-timestampCurrent UNIX timestamp.
auth-tokenYour access token.
auth-signatureHMAC-SHA512 hash of the request.

📨 Request Body

FieldTypeDescription
nameStringrequired Plan name (max 255 chars).
priceNumericrequired Amount in FCFA. Minimum: 100.
frequency_codeStringrequired Billing cycle — one of weekly, monthly, yearly.
vat_rate_idStringrequired 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

HeaderTypeDescription
AcceptStringrequired — Must be application/json.
auth-apiKeyStringrequired — Your public API key.
auth-timestampStringrequired — Current UNIX timestamp.
auth-tokenStringrequired — Your access token.
auth-signatureStringrequired — HMAC-SHA512 hash of the request.
sandboxStringrequired — sandbox only — Value: true.

📨 Request Body

FieldTypeDescription
phoneStringrequired Customer's phone number. Sandbox: 98745632
plan_idStringrequired Plan identifier from Retrieve Subscription Plans.
frequency_codeStringrequired Billing frequency — one of weekly, monthly, yearly.
start_atStringrequired 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

HeaderTypeDescription
AcceptStringrequired — Must be application/json.
auth-apiKeyStringrequired — Your public API key.
auth-timestampStringrequired — Current UNIX timestamp.
auth-tokenStringrequired — Your access token.
auth-signatureStringrequired — HMAC-SHA512 hash of the request.
sandboxStringrequired — sandbox only — Value: true.

📨 Request Body

FieldTypeDescription
phoneStringrequired 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"
  }
}

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.

Sandbox vs Live
All webhook endpoints exist in two variants:
{BASE_URL}/sandbox/… — use with your sandbox API key (pk_test_…) for testing.
{BASE_URL}/live/… — use with your live API key (pk_live_…) in production.
Replace /sandbox/ with /live/ and your key with pk_live_… when going to production.

⚡ How It Works

  1. Register an endpoint URL via the Dashboard or the API.
  2. Choose the events to listen to (payment.success, subscription.created, …).
  3. Jɛmɛnipay sends a signed POST request to your URL when an event fires.
  4. Your server validates the signature and responds 200 OK.
  5. On failure, Jɛmɛnipay retries automatically — 3 attempts with exponential backoff (1 min → 5 min → 30 min).
  6. If a fallback URL is configured, it is tried immediately before the retry queue.

📋 Available Events

EventTriggered when
payment.successA transaction is validated (state = 2).
payment.failedA transaction is failed or cancelled (state = 3 / 4).
subscription.createdA new subscription is created.
subscription.renewedA subscription is renewed.
subscription.disabledA 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.

FieldTypeDescription
urlStringrequired Your endpoint URL. Must be a valid HTTPS URL (max 500 chars).
eventsArrayrequired List of event types to subscribe to. At least one required.
fallback_urlStringoptional Secondary URL tried immediately if url returns a non-2xx response.
descriptionStringoptional 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

ConditionMeaning
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

AttemptDelay before retry
1st retry1 minute
2nd retry5 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 OK as 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_id or external_reference to deduplicate events — retries may send the same event more than once.
  • Register a fallback_url on 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.