https://waestaos.waestaltd.comWaesta OS API: start here
Four small JSON APIs let your own systems (Benofy, Webook, EduMaster, a client's website...) work with Waesta OS. Together they link a system fully: it reports its health, checks its subscription, raises support tickets, and sells or sends SMS, with no manual steps.
| API | What it is for | Key | Full guide |
|---|---|---|---|
| Bulk SMS | Send SMS from a customer's prepaid credits, check balance and delivery, buy more credits (mobile money) and get a callback when they arrive | wsk_... | SMS_API.md |
| Support tickets | Raise and follow support tickets, get webhooks on replies and resolution | wcp_... | SUPPORT_API.md |
| Systems and licence | Report health and version (heartbeat), check the subscription status, learn when a new version is out. Feeds the Operations board and incident alerts | wcp_... (same key as support) | SYSTEMS_API.md |
| Website inquiry form | The contact form on waestaltd.com (used by the embed script) | none (Turnstile) | Inquiry form |
Machine-readable description for Postman, Insomnia, Swagger UI and code generators: openapi.json (OpenAPI 3.1). Use *Download OpenAPI file* at the top of this page: its default server is already this system's address. In Postman: *Import → File*, then add your key.
The examples use the address of this system. In a copy read outside Waesta OS they show YOUR-SYSTEM where the address goes: replace it with yours (for example os.waestaltd.com).
1. Quick start (5 minutes)
Get a key. Keys are created in the system and shown once:
- SMS: *SMS → Customers → the customer → API keys* (or the customer does it in the portal under *SMS credits → Connect your system*).
- Support, heartbeat and licence (one key for all three): *Customer care → Systems → the system → Connect this system (API)*. Link it to the client's subscription there so the licence check works.
What to wire up first in every system we build: a heartbeat job (health and version), a licence check (is the subscription current?), the support ticket call behind your "Report a problem" button with its webhook receiver, and the SMS API if it sends or sells SMS. SYSTEMS_API.md has an integration checklist.
Try it (replace the key):
# SMS balance
curl https://waestaos.waestaltd.com/api/v1/sms/balance \
-H "Authorization: Bearer wsk_YOUR_KEY"
# Send an SMS
curl -X POST https://waestaos.waestaltd.com/api/v1/sms/send \
-H "Authorization: Bearer wsk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"to":["0772345678"],"message":"Your code is 4821"}'
# Tell Waesta OS this system is alive (send every 1 to 5 minutes from a scheduled job)
curl -X POST https://waestaos.waestaltd.com/api/v1/systems/heartbeat \
-H "Authorization: Bearer wcp_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"status":"ok","version":"2.4.1","errors_24h":0}'
# Is this system's subscription current?
curl https://waestaos.waestaltd.com/api/v1/subscription -H "Authorization: Bearer wcp_YOUR_KEY"
# Raise a support ticket
curl -X POST https://waestaos.waestaltd.com/api/v1/support/tickets \
-H "Authorization: Bearer wcp_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"subject":"Report will not export","message":"Export does nothing.","external_ref":"BEN-4471","reporter":{"name":"Grace","email":"[email protected]"}}'
A working call answers {"ok": true, ...}. Anything else answers {"ok": false, "error": "what to fix"} with an HTTP status from the table below.
2. Rules that apply to every API
Authentication
Send the key in a header on every call:
Authorization: Bearer wsk_... (SMS) or Authorization: Bearer wcp_... (Support)
If your host strips the Authorization header, send X-Api-Key: <key> instead. Keys are different per API: an SMS key does not work on the support or systems API. The support key (wcp_) also serves the systems and licence API.
- Keep keys on your server. Never put one in a web page, a mobile app or a public repository.
- Use one key per system and environment (for example "Benofy production", "Benofy test") so you can revoke one without touching the others.
- If a key leaks, revoke it in the system and create a new one. Revoked keys are refused immediately.
Requests and responses
- HTTPS only. Send
Content-Type: application/jsonand a JSON body forPOST. Responses are JSON. - Times are ISO 8601 in UTC (for example
2026-09-28T08:00:00+00:00). - Field names are
snake_case. Unknown fields are ignored; missing optional fields use safe defaults.
Success and errors
Every reply has ok. On failure error is a sentence you can log or show to a developer.
| HTTP | Meaning | What to do |
|---|---|---|
| 200 / 201 | Worked (201 = something was created) | Read the body |
| 401 | Missing, wrong or revoked key | Check the key and the header |
| 402 | SMS only: not enough credits (reply includes balance) | Top up, then retry |
| 403 | Not allowed from this place | Check the allowed website list |
| 404 | No such record for your key | Check the id; keys only see their own records |
| 422 | Something in the request is wrong | Fix it (the message says what) and resend |
| 429 | Too many requests | Wait and retry with backoff |
| 503 | Temporarily unavailable, or the feature is not switched on | Retry later |
{ "ok": false, "error": "There is no valid phone number to send to." }
Rate limits
- 120 requests per minute per key, and 600 per minute per network address.
- 30 wrong keys from one address in 10 minutes are blocked for a while.
- Bulk work belongs in one call with many recipients (SMS: up to 1,000 numbers), not in many small calls.
Retrying safely (idempotency)
Networks fail. Make retries harmless:
| API | How |
|---|---|
| SMS send | Header Idempotency-Key: <your unique id>. The same key returns the first result and is not charged again. |
| Support ticket | Field external_ref (your own id for the problem). The same reference returns the existing ticket (replayed: true). |
Retry 429, 503 and network errors with a growing delay (1s, 5s, 30s...). Do not blindly retry 4xx errors: fix the request.
Webhooks (support API)
When we reply, resolve, close or re-open a ticket your system can be called. Every call is signed:
X-Waesta-Signature: sha256=<HMAC-SHA256 of the raw body with your webhook secret>
X-Waesta-Event: ticket.resolved
X-Waesta-Delivery: 1234 (unique id: ignore repeats)
Verify the signature before trusting a call, and answer with any 2xx. Details and a receiver example: SUPPORT_API.md.
3. Examples in other languages
PHP
function waesta(string $method, string $path, string $key, ?array $body = null): array {
$ch = curl_init('https://waestaos.waestaltd.com' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$reply = json_decode((string) curl_exec($ch), true) ?: ['ok' => false, 'error' => 'No valid reply'];
$reply['_status'] = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
return $reply;
}
$r = waesta('POST', '/api/v1/sms/send', $smsKey, ['to' => ['0772345678'], 'message' => 'Hello']);
Node.js (18+, run on your server)
async function waesta(method, path, key, body) {
const res = await fetch('https://waestaos.waestaltd.com' + path, {
method, headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
return { status: res.status, ...(await res.json()) };
}
const r = await waesta('POST', '/api/v1/support/tickets', supportKey,
{ subject: 'Cannot log in', message: 'Error on the login page', external_ref: 'WEB-88' });
Python
import requests
def waesta(method, path, key, body=None):
r = requests.request(method, "https://waestaos.waestaltd.com" + path, json=body, timeout=20,
headers={"Authorization": f"Bearer {key}"})
return {"status": r.status_code, **r.json()}
r = waesta("GET", "/api/v1/sms/balance", sms_key)
Verify a webhook (Node.js / Express)
const crypto = require('crypto');
app.post('/hooks/waesta', express.raw({ type: '*/*' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.WAESTA_HOOK_SECRET).update(req.body).digest('hex');
const given = req.get('X-Waesta-Signature') || '';
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) return res.sendStatus(401);
const event = JSON.parse(req.body); // event.event, event.ticket.external_ref ...
res.sendStatus(204);
});
4. Going live checklist
- ☐ Key is stored in the server's environment or secret store, not in code.
- ☐ Requests use HTTPS and a timeout (15 to 20 seconds).
- ☐ SMS: you send an
Idempotency-Keyand handle402(low credits) by alerting your team. - ☐ Support: you send
external_refand store the returnedticket_nonext to your own record. - ☐ Heartbeat: a scheduled job sends it every 1 to 5 minutes with
version; the licence answer is cached andentitled: falseis handled kindly (see SYSTEMS_API.md). - ☐ Webhooks: the receiver verifies the signature, answers fast with
2xx, and ignores repeatedX-Waesta-Deliveryids. - ☐ You log every non-2xx reply including its
errortext. - ☐ Test with a test key first, then create a separate production key.
5. Website inquiry form
The contact form on waestaltd.com posts to the system so every message becomes an inquiry, a prospect client and a lead. You normally just paste the two-line embed code from *Website inquiries* into the page; this section is for developers who build their own form.
GET /api/inquiry/config returns the Turnstile site key and the list of services.
POST /api/inquiry (form-encoded or JSON), only from the allowed website origins (INQUIRY_ALLOWED_ORIGINS):
| Field | Required | Notes |
|---|---|---|
name | yes | 2 to 120 characters, no links or markup |
email | yes | valid address |
message | yes | 10 to 3,000 characters, at most 2 links |
consent | yes | must be 1: agreement to be contacted |
phone, company, service | no | service must be one from the config list, otherwise it becomes "Other" |
marketing | no | 1 if the visitor agreed to greetings and offers |
cf-turnstile-response | yes | the Cloudflare Turnstile token from the widget |
_t | yes | the time the form was shown, in milliseconds (a form submitted in under 3 seconds is refused) |
company_website | must be empty | hidden trap field for bots |
Answers {"ok": true, "reference": "INQ-00012"}. Limits: 20 attempts per hour and 5 accepted inquiries per hour per address, and 3 per day per email.
6. Changes
| Date | Change |
|---|---|
| 2026-10 | Systems and licence API: POST /api/v1/systems/heartbeat and GET /api/v1/subscription, with the Operations board, incident alerts and licence linking. Public developer portal at /developers. |
| 2026-09 | Bulk SMS API (/api/v1/sms/*), support tickets API and webhooks (/api/v1/support/*), website inquiry endpoint. |