Developers

Lynk API

Send SMS and email, use Lynk AI, and take Mobile Money and card payments from your own app — with one REST API and JSON.

OpenAPI spec (JSON) · Get an API key

Getting started

All requests go to:

https://www.lynkinfra.com/api/v1
  1. Create a free Lynk account.
  2. Messaging and AI: create a key in Developer / API. Payments: create keys in Lynk Pay → Developers.
  3. Send JSON with Content-Type: application/json and your key in the Authorization header.
Your first request
curl -X POST https://www.lynkinfra.com/api/v1/sms \
  -H "Authorization: Bearer lynk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"to": "0241234567", "message": "Hello from my app"}'

Authentication

Send your key as a Bearer token. Keep keys on your server — never in a website or mobile app.

Authorization: Bearer <your key>
KeyLooks likeUsed for
Account keylynk_…SMS, email, WhatsApp and Lynk AI
Lynk Pay secret keysk_test_… / sk_live_…Payments. Test keys never move real money.

Permissions. When you create an account key, you choose what it can use: sms, email, whatsapp, ai (chat) and images. A key used for anything else gets 403. Give each app its own key with only what it needs.

Errors

Every error has an HTTP status and a message you can show or log.

StatusMeaning
400 / 422Something in the request is missing or invalid.
401The API key is missing or wrong.
402Not enough wallet balance or messaging allowance. Top up and try again.
403The key doesn't have permission for this service.
404Not found (e.g. a payment reference).
409Lynk Pay can't take this payment yet (e.g. no approved payout account, or that payment method is off).
503The service is temporarily unavailable. Try again later.
500 / 502Something went wrong on our side. Retry with backoff.

Error bodies by API:

Messaging and /ai
{ "error": "Insufficient wallet balance" }
OpenAI-compatible endpoints
{ "error": { "message": "…", "type": "invalid_request_error", "code": "model_not_found" } }
Lynk Pay
{ "status": false, "message": "…", "code": "VALIDATION_ERROR" }

Send SMS

POST/smsneeds sms

Sends one message to one or many numbers. Each part (160 characters, or 70 with emoji) per recipient uses one SMS from your allowance.

FieldTypeDescription
to*string | string[]A number or a list. Ghana numbers like 0241234567 or +233241234567; other countries with their country code.
message*stringThe text. Up to 6 parts.
senderstringYour approved sender ID. Without it, Lynk's default sender is used.
Request
curl -X POST https://www.lynkinfra.com/api/v1/sms \
  -H "Authorization: Bearer lynk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"to": ["0241234567", "0201234567"], "message": "Your order is on its way!", "sender": "MYSHOP"}'
Response
{
  "id": "cm…",
  "status": "SENT",
  "sender": "MYSHOP",
  "recipients": 2,
  "parts": 1,
  "unitsUsed": 2,
  "rejected": 0,
  "invalidNumbers": []
}

Numbers that couldn't be read are listed in invalidNumbers and aren't charged; numbers the network rejects are counted in rejected and refunded.

Send email

POST/emailneeds email

Sends a plain-text email under your business name. Replies go to your reply-to address. With a verified sending domain, it comes from your own address. Uses one email from your allowance.

FieldTypeDescription
to*stringRecipient email address.
subject*stringSubject line.
body*stringPlain-text message.
Request
curl -X POST https://www.lynkinfra.com/api/v1/email \
  -H "Authorization: Bearer lynk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"to": "ama@example.com", "subject": "Your receipt", "body": "Thanks for your order!"}'
Response
{ "id": "cm…", "status": "SENT", "unitsUsed": 1 }

Send WhatsApp

POST/whatsappneeds whatsapp

Sends a WhatsApp message, charged per message from your wallet. Returns 503 while WhatsApp isn't available on your account.

FieldTypeDescription
to*stringPhone number with country code.
message*stringThe text.
Response
{ "id": "cm…", "status": "SENT", "costCents": 12 }

Lynk AI: chat

POST/aineeds ai

Ask Lynk AI a question. Lynk picks a suitable model, removes personal details before sending (based on your privacy setting), and charges your wallet for what the answer uses.

FieldTypeDescription
promptstringA single question. Send this or messages.
messagesobject[]A conversation: [{ "role": "user" | "assistant", "content": "…" }], ending with the user. Up to 100 messages.
modestringAUTO (default), QUALITY, FAST, ECONOMY or PRIVACY.
Request
curl -X POST https://www.lynkinfra.com/api/v1/ai \
  -H "Authorization: Bearer lynk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Write a short SMS announcing our weekend sale"}'
Response
{
  "id": "cm…",
  "completion": "Weekend sale! 20% off everything…",
  "model": "…",
  "usage": { "inputTokens": 18, "outputTokens": 42 },
  "charged": { "amountCents": 1 },
  "privacy": { "protectedCount": 0 }
}

Lynk AI: OpenAI-compatible

POST/chat/completionsneeds ai

GET/modelsneeds ai or images

Already using an OpenAI library? Point it at Lynk and use a Lynk key. The model is a Lynk mode:

FieldTypeDescription
lynk-automodelBest model for each request (default).
lynk-qualitymodelThe most capable available model.
lynk-fastmodelThe quickest available model.
lynk-economymodelThe lowest-cost available model.
lynk-privacymodelOnly privacy-approved models.
Node.js (openai package)
import OpenAI from "openai";

const lynk = new OpenAI({ apiKey: process.env.LYNK_API_KEY, baseURL: "https://www.lynkinfra.com/api/v1" });

const reply = await lynk.chat.completions.create({
  model: "lynk-auto",
  messages: [
    { role: "system", content: "You write friendly SMS for a bakery." },
    { role: "user", content: "Announce fresh bread at 7am." },
  ],
});
console.log(reply.choices[0].message.content);
Python (openai package)
from openai import OpenAI

lynk = OpenAI(api_key="lynk_your_key", base_url="https://www.lynkinfra.com/api/v1")
reply = lynk.chat.completions.create(
    model="lynk-auto",
    messages=[{"role": "user", "content": "Announce fresh bread at 7am."}],
)
print(reply.choices[0].message.content)

system and developer messages become instructions. Streaming isn't supported yet. The response also has a lynk object with served_by and charged_cents.

Lynk AI: images

POST/images/generationsneeds images

Creates one image per request, returned as base64. Charged per image.

FieldTypeDescription
prompt*stringWhat to draw. Up to 4,000 characters.
modelstringlynk-auto (default) or lynk-quality.
sizestring1024x1024 (default), 1024x1536 or 1536x1024.
Request
curl -X POST https://www.lynkinfra.com/api/v1/images/generations \
  -H "Authorization: Bearer lynk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "A bright photo of fresh bread on a wooden table", "size": "1024x1024"}'
Response
{
  "created": 1790000000,
  "data": [{ "b64_json": "iVBORw0KGgo…" }],
  "output_format": "png",
  "lynk": { "image_id": "cm…", "charged_cents": 5 }
}

Lynk Pay: create a payment

POST/pay/payments

Starts a Mobile Money or card payment and returns a link to send your customer to. Amounts are in pesewas (GH₵100.00 = 10000). Use a Lynk Pay secret key; test keys create test payments.

FieldTypeDescription
amount*integerWhat you want to receive, in pesewas.
channel*stringMOBILE_MONEY or CARD.
email*stringCustomer's email, for their receipt.
currencystringGHS (the only currency for now).
fee_bearerstringmerchant (fee comes out of your payout) or customer (added on top). Defaults to your account setting.
callback_urlstringWhere to send the customer afterwards. We add ?reference=…&status=….
referencestringYour own order ID. Sending the same one again returns the same payment instead of charging twice.
descriptionstringShown to the customer.

For safe retries you can also send an Idempotency-Key header.

Request
curl -X POST https://www.lynkinfra.com/api/v1/pay/payments \
  -H "Authorization: Bearer sk_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "amount": 10000,
    "channel": "MOBILE_MONEY",
    "email": "customer@example.com",
    "fee_bearer": "customer",
    "callback_url": "https://yourshop.com/orders/1042/paid"
  }'
Response
{
  "status": true,
  "message": "Payment initialized",
  "data": {
    "reference": "LNK-3F9A1C2B7D4E",
    "authorization_url": "https://…",
    "amount": 10304,
    "fee": 304,
    "net_amount": 10000,
    "fee_bearer": "customer"
  }
}

amount is what the customer pays, fee is Lynk Pay's fee, and net_amount is what you receive. Redirect the customer to authorization_url.

Lynk Pay: check a payment

GET/pay/payments/{reference}

Always confirm a payment on your server before delivering an order — don't rely on the redirect alone.

Request
curl https://www.lynkinfra.com/api/v1/pay/payments/LNK-3F9A1C2B7D4E \
  -H "Authorization: Bearer sk_live_your_key"
Response
{
  "status": "success",
  "reference": "LNK-3F9A1C2B7D4E",
  "amount": 10304,
  "fee": 304,
  "net_amount": 10000,
  "fee_bearer": "customer",
  "currency": "GHS",
  "refunded_amount": 0,
  "refundable_amount": 10000
}

status is pending, success, failed, cancelled or refunded. Mobile Money can take a minute or two while the customer approves.

Lynk Pay: refunds

POST/pay/payments/{reference}/refund

Sends money back to your customer for a successful payment — all of it or part. The customer gets back what they paid, less any fee they covered (fees aren't refundable). Live refunds are taken from your Lynk wallet, so keep it topped up.

FieldTypeDescription
amountintegerIn pesewas. Leave out to refund everything still refundable (see refundable_amount).
reasonstringShown to the customer.
Request
curl -X POST https://www.lynkinfra.com/api/v1/pay/payments/LNK-3F9A1C2B7D4E/refund \
  -H "Authorization: Bearer sk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "reason": "Booking cancelled"}'
Response
{
  "status": true,
  "message": "Refund started",
  "data": { "id": "cm…", "reference": "LNK-3F9A1C2B7D4E", "amount": 5000, "currency": "GHS", "status": "pending" }
}

Refunds usually complete within a few business days; you get a payment.refunded webhook when it's done, or refund.failed (and the money returns to your wallet). Errors: 402 INSUFFICIENT_FUNDS (top up your wallet), 409 REFUND_NOT_ALLOWED (e.g. more than the refundable amount), 404 NOT_FOUND.

Lynk Pay: webhooks

Add your webhook URL in Lynk Pay → Developers (it must be a public https:// address). We POST these events:

FieldTypeDescription
payment.successeventThe customer paid. Deliver the order.
payment.failedeventThe payment didn't go through.
payment.refundedeventA refund reached the customer.
refund.failedeventA refund couldn't be completed; the money is back in your wallet.
payment.success body
{
  "event": "payment.success",
  "data": {
    "reference": "LNK-3F9A1C2B7D4E",
    "amount": 10304,
    "fee": 304,
    "net_amount": 10000,
    "fee_bearer": "customer",
    "currency": "GHS",
    "status": "success",
    "customer": { "email": "customer@example.com" }
  }
}

Check the signature on every webhook: X-Lynk-Signature is the HMAC-SHA256 (hex) of the raw request body, using your webhook secret. Reply with any 2xx within 10 seconds. If you don't, we send it again — so skip any X-Lynk-Delivery id you've already handled.

Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const seen = new Set(); // use your database in production

app.post("/webhooks/lynk", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto.createHmac("sha256", process.env.LYNK_WEBHOOK_SECRET).update(req.body).digest("hex");
  const given = req.get("X-Lynk-Signature") ?? "";
  if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  const delivery = req.get("X-Lynk-Delivery");
  if (!seen.has(delivery)) {
    seen.add(delivery);
    const { event, data } = JSON.parse(req.body.toString());
    if (event === "payment.success") {
      // Mark the order with data.reference as paid.
    }
  }
  res.sendStatus(200);
});
PHP
<?php
$body = file_get_contents("php://input");
$expected = hash_hmac("sha256", $body, getenv("LYNK_WEBHOOK_SECRET"));
if (!hash_equals($expected, $_SERVER["HTTP_X_LYNK_SIGNATURE"] ?? "")) {
  http_response_code(401);
  exit;
}
$payload = json_decode($body, true);
if ($payload["event"] === "payment.success") {
  // Mark the order with $payload["data"]["reference"] as paid.
}
http_response_code(200);

Limits

  • SMS: up to 1,000 recipients and 6 parts per request.
  • AI chat: up to 100 messages per request, each up to 20,000 characters.
  • AI images: one image per request, prompt up to 4,000 characters.
  • Request bodies are JSON; amounts are in the smallest currency unit (pesewas).

Need help? Contact support with the endpoint, status code and error message — never your API key.