Developers

Huupa SMS API

1. Authentication

An admin creates keys on CRM → Developers. A key is shown once; we keep only a hash of it. Send it on every request:

Authorization: Bearer hk_1a2b3c4d_...

A missing, wrong or revoked key gets 401. More than 120 requests a minute on one key gets 429 with Retry-After. If your plan is not active, sends get 403 plan_inactive (reads still work while a plan is paused).

2. Send one SMS

curl https://huupa.jbklutseventures.com/api/v1/messages \ -H "Authorization: Bearer $HUUPA_KEY" \ -H "Content-Type: application/json" \ -d '{"to": "0244123456", "message": "Your order #1042 is ready for pickup.", "idempotency_key": "order-1042-ready"}'
FieldWhat it is
toA Ghana mobile number: 0244123456, 233244123456 or +233 24 412 3456.
messageUp to 3 pages. A page is 160 characters (153 each once split), or 70 (67) if the text has an emoji or a character like ɛ. 1 credit per page. {{first_name}} works if we know the name.
categorytransactional (default: receipts, codes, reminders) or marketing. Marketing needs the person's recorded consent and your own approved sender ID, is sent only 08:00–19:00 Monday to Saturday Ghana time (a text outside that waits), and gets an opt-out link added.
senderOptional. One of your approved sender IDs. Leave it out to use your default. Without an approved ID, transactional texts go out as HUUPA.
idempotency_keyOptional but recommended. Send the same key again and you get the first message back (200), and nothing is sent twice.
scheduled_atOptional ISO 8601 time, up to 30 days ahead, e.g. 2026-10-01T09:30:00Z.

A new message answers 201:

{"id": 5821, "status": "sent", "to": "233244123456", "segments": 1, "credits": 1, "sender": "YOURBRAND", "category": "transactional", "idempotency_key": "order-1042-ready", "reason": null, ...}

Status moves queued → sent → delivered or failed. scheduled means it is waiting for its time or for the marketing window.

3. Send to many

curl https://huupa.jbklutseventures.com/api/v1/messages/bulk \ -H "Authorization: Bearer $HUUPA_KEY" -H "Content-Type: application/json" \ -d '{"to": ["0244123456", "0501234567"], "message": "Our shop is closed on Monday for the holiday.", "idempotency_key": "holiday-closure"}'

Up to 1,000 numbers. Every number and the credits for all of them are checked first: if any number is invalid or credit is short, nothing is sent. Answers 202 with one entry per number.

4. Check messages, contacts and credit

CallWhat it returns
GET /api/v1/messages/{id}One message and its status.
GET /api/v1/messages?status=failed&page=1&per_page=50Your messages, newest first (at most 100 a page).
POST /api/v1/contactsAdd or update a contact: phone, email, first_name, last_name, company. To record marketing consent send "consent": {"sms_marketing": true, "evidence": "Ticked the box on our checkout, 2026-09-22"}. The evidence is required: it is the record Ghana's Data Protection Act asks for.
GET /api/v1/balanceCredits left, this month's included texts, and credits expiring soon.
GET /api/v1/sender-idsYour approved sender IDs and your default.
GET /api/v1/transactionsYour credit ledger: purchases, sends, refunds.

5. Errors

Every refusal is JSON: {"error": "<code>", "message": "<what to do>"}.

CodeMeaning
invalid_toNot a Ghana mobile number.
too_long · empty_message · unknown_fieldFix the text.
sender_not_approvedThat sender ID is not approved for you.
sender_neededMarketing needs your own approved sender ID.
insufficient_credit (402)Buy SMS credits on your Plan page.
no_consent · suppressed · unsubscribed (422)The person has not agreed to marketing, has opted out, or the number cannot receive. The message is recorded but not sent.
contact_limit · plan_inactive (403)Your plan's limits.

6. Webhooks

Add an https:// URL on the Developers page and choose events: message.sent, message.delivered, message.failed. We POST JSON like this:

{"id": "evt_90211", "type": "message.delivered", "created_at": "2026-09-22 10:14:03", "data": {"id": 5821, "status": "delivered", "to": "233244123456", ...}}

Each request carries X-Huupa-Event, X-Huupa-Delivery, X-Huupa-Timestamp and X-Huupa-Signature. Check the signature with the secret shown when you created the webhook:

# Python import hashlib, hmac expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() ok = hmac.compare_digest(expected, request.headers["X-Huupa-Signature"])

Use the raw request body, exactly as received. Reject a timestamp older than five minutes. Answer with any 2xx quickly; a slow or failed answer is retried after 1 min, 5 min, 30 min, 2 h. After 20 failures in a row we turn the webhook off and say so on the Developers page. We do not follow redirects.