API reference
The programmatic surface: base URL, authentication, the send endpoint, public pricing, the response envelope, and errors.
Darkel’s public API is intentionally small. Your backend sends messages and can read live pricing; everything else (connecting a number, templates, top-ups, team) is done in the dashboard. This page is the precise contract for the endpoints you call from code.
Base URL & versioning
https://api.darkel.ng- One base URL for sandbox and live — the API key decides the environment (
dk_test_vsdk_live_). - Versioned endpoints are prefixed with
/v1. Treat the version as part of the path. - All requests and responses are JSON. Send
Content-Type: application/jsonon requests with a body.
Authentication
Send your API key in the X-Api-Key header. The key identifies the workspace; there is no separate account ID to send. Full details in Authentication.
X-Api-Key: dk_live_YOUR_API_KEYResponse envelope
Successful responses are wrapped in a standard envelope. Your data is always under data.
{
"success": true,
"statusCode": 201,
"message": "Request successful",
"data": { /* the resource — e.g. the message record */ }
}Errors use the same envelope idea with success: false and a human-readable message:
{
"success": false,
"statusCode": 400,
"message": "to must be a valid E.164 phone number"
}Send a message
/v1/whatsapp/messagesX-Api-KeySend an approved template message from the key’s workspace.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient in E.164, e.g. +2348012345678 |
template | object | Yes | The template to send (below) |
template.name | string | Yes | Exact name of an approved template |
template.languageCode | string | No | Defaults to en_US; must match the template |
template.components | array | Conditional | Variable values; required when the template has placeholders |
const res = await fetch("https://api.darkel.ng/v1/whatsapp/messages", {
method: "POST",
headers: {
"X-Api-Key": process.env.DARKEL_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+2348012345678",
template: {
name: "order_confirmation",
languageCode: "en_US",
components: [
{ type: "body", parameters: [{ type: "text", text: "Ada" }] },
],
},
}),
});
const { data } = await res.json();
if (!res.ok) throw new Error("Send failed");
console.log("Queued:", data.id);{
"success": true,
"statusCode": 201,
"message": "Request successful",
"data": {
"id": "665f1c0b8a3e2d0012a4b9f1",
"to": "+2348012345678",
"direction": "outbound",
"templateName": "order_confirmation",
"status": "pending",
"pricing": {
"country": "NG",
"expectedCategory": "marketing",
"chargedKobo": 7998
},
"createdAt": "2026-06-27T09:30:00.000Z"
}
}The response is returned the moment the message is accepted, so status is usually pending or sent. Track the final outcome in the dashboard — see Delivery & errors.
The pricing object
A message sent on a shared Darkel number carries a pricing object. A message sent on your own number is not charged by Darkel. The price is Meta’s price for the message type and recipient country, in naira — see the price list.
| Field | Description |
|---|---|
pricing.country | Recipient country, from the country calling code |
pricing.expectedCategory | The message type the price was calculated for at send time |
pricing.chargedKobo | Amount currently held for the message, in kobo |
pricing.billedCategory | Once Meta confirms: the type Meta actually billed |
pricing.finalKobo | Once Meta confirms: the final amount for the message, in kobo |
Components by template type
- No variables — omit
componentsentirely. - Body variables — one
{ "type": "body", "parameters": [...] }entry; eachparameterfills{{1}},{{2}}… in order. - Authentication (OTP) — a single body parameter carrying the code, e.g.
[{ "type": "body", "parameters": [{ "type": "text", "text": "483920" }] }].
Get product pricing
/products/:keyNone (public)Read live pricing for an active product. No key required.
curl https://api.darkel.ng/products/whatsapp{
"success": true,
"statusCode": 200,
"message": "Request successful",
"data": {
"key": "<product-key>",
"name": "<product name>",
"description": "<short description>",
"monthlyFee": <monthly access fee in kobo>,
"yearlyFee": <yearly access fee in kobo>
}
}Amounts are in kobo (₦1 = 100 kobo) — read the live values from this endpoint rather than hard-coding them. Returns 404 if the product key does not exist or is inactive. More in Billing & pricing.
Get WhatsApp message prices
/whatsapp/pricesNone (public)Read the current per-message price list. No key required.
curl https://api.darkel.ng/whatsapp/prices{
"success": true,
"statusCode": 200,
"message": "Request successful",
"data": {
"koboPerUsd": <naira per US dollar, in kobo, or null>,
"updatedAt": "<ISO timestamp, or null>",
"countries": [
{
"country": "NG",
"name": "Nigeria",
"prices": { "marketing": <kobo>, "utility": <kobo>, "...": <kobo> }
}
]
}
}Amounts are in kobo. countries is empty while pricing is not configured. A message is charged at the price for its type and the recipient’s country, converted at the published rate. More in Billing & pricing.
Status codes
| Code | Meaning |
|---|---|
200 / 201 | Success |
400 | Validation or business-rule error (bad number, sandbox recipient, low balance, not set up) |
401 | Missing, invalid or revoked API key |
403 | WhatsApp is not enabled for the workspace |
404 | Resource not found (e.g. unknown product key) |
429 | Too many requests — slow down and retry |
For Meta-side failures that occur after a message is accepted, see the error-code table in Delivery & errors.