SMSHub API · v1

SMSHub API documentation

The SMSHub API lets your application send SMS to Ethio Telecom numbers in Ethiopia. It is a single JSON endpoint authenticated with a secret key. This guide covers everything from creating an account to handling errors in production.

Base URLhttps://sms.hizcore.com
Send endpointPOST /api/sms
Test endpointPOST /api/test — validates without sending or charging
AuthenticationAuthorization: Bearer sk_live_…
FormatJSON request and response bodies, UTF-8
TransportHTTPS only

How SMSHub works

SMSHub is an SMS gateway for Ethiopia. Your application sends one HTTPS request per message; SMSHub checks it, adds your signature, uses one credit and hands it to Ethio Telecom. There is nothing to install — any language that can make an HTTP request works.

  1. 1
    Your server sends a request

    POST /api/sms with your secret key in the Authorization header and a small JSON body: msisdn (the phone number) and text.

  2. 2
    SMSHub checks it

    API key, account and KYC status, rate limit, number format, message length and content moderation. A request that fails any check gets a clear error and costs nothing.

  3. 3
    Signature & credit

    Your SMS signature is appended and one credit is reserved from your balance.

  4. 4
    Delivery to Ethio Telecom

    If the carrier accepts the message you get 200 with "message_status": "sent". If it refuses, you get 502 and the credit is refunded automatically.

  5. 5
    Track it

    Every message — from the API or the dashboard — appears in Dashboard → Message history, where you can search, filter and export it.

Building something new? Use /api/test while you develop: it runs every check above and shows the final text, but never sends and never charges.

Quick start

  1. Create an account and verify your phone number with the code we text you.
  2. Submit your identity documents under Dashboard → Verification. Sending unlocks when an administrator approves them, and 15 free credits are added.
  3. Copy your secret key from Dashboard → API access and store it as an environment variable, for example SMSHUB_API_KEY.
  4. Validate your request against /api/test, then send for real with /api/sms:
Terminal
curl -X POST 'https://sms.hizcore.com/api/sms' \
  -H "Authorization: Bearer $SMSHUB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"msisdn":"0911234567","text":"Hello from SMSHub"}'

A 200 response with "message_status": "sent" means the carrier accepted the message and one credit was used.

Account registration & verification

Every SMSHub account belongs to a real person or business with a working Ethio Telecom number.

  1. Register with your full name, email, phone number (09xxxxxxxx) and a password of at least 10 characters (common passwords are rejected).
  2. Verify your phone. We send a 6-digit code that expires after 10 minutes. After 5 incorrect attempts the code is cancelled and you must request a new one (available once per minute).
  3. Complete KYC (see below). Until KYC is approved you can sign in and explore the dashboard, but sending, API access and free credits are locked.
Signed up with a referral link? The person who invited you earns 10 SMS once your phone is verified.

KYC requirements

Identity verification is how we keep scammers off the network. An administrator reviews every submission manually — usually within one business day.

DocumentSides requiredNotes
National ID (Fayda / Kebele)Front and backMust be valid and legible.
PassportPhoto page onlyEthiopian or foreign passport.
Driver's licenseFront and backEthiopian license.
Other government IDFront and backReviewed case by case.
  • Accepted formats: JPG, PNG or PDF, up to 5 MB per file.
  • The legal name must match the document. Business accounts should submit the ID of the account owner.
  • If a submission is rejected you'll see the reason on the Verification page and can resubmit immediately.
  • Documents are stored in access-restricted storage and used only for verification and legal compliance.

Authenticating requests

Send your secret key in the Authorization header of every request using the Bearer scheme:

HTTP
POST /api/sms HTTP/1.1
Host: sms.hizcore.com
Authorization: Bearer sk_live_4f9c…
Content-Type: application/json

Requests without a valid key receive 401. Keys belonging to accounts that are unverified, not KYC-approved or suspended receive 403. Repeated invalid keys from one IP address are throttled with 429. See Errors.

Keep your key on the server. Anyone with your key can send messages and spend your credits. Never embed it in a mobile app, browser JavaScript or a public repository.

Managing API keys

  • Each account has one active secret key, prefixed sk_live_.
  • Create, view, copy and rotate it under Dashboard → API access. The key is never embedded in the page — it is loaded only when you press Show or Copy.
  • Requests are matched against a SHA-256 fingerprint of your key, and the viewable copy is encrypted with AES-256-GCM using a secret kept outside the database — a leaked database alone does not expose keys.
  • Rotating creates a new key and disables the old one immediately. Deploy the new key to all your services first-thing after rotating.
  • Keys are revoked automatically when an account is suspended for a serious policy violation. A reinstated account must create a new key.
  • Rotate your key if it may have been exposed, when a team member with access leaves, and periodically as good practice.

Keeping your API key private

Your secret key works like the password to your balance: anyone who has it can send messages as you and spend your credits. Treat it the same way you treat a database password.

Do

  • Store it in an environment variable or a secrets manager, e.g. SMSHUB_API_KEY.
  • Call the API only from your own server.
  • Rotate it immediately if it may have leaked, and when someone with access leaves.
  • Redact it from logs, error reports and screenshots.

Don't

  • Put it in a mobile app, browser JavaScript or an HTML page.
  • Commit it to Git — add .env to .gitignore.
  • Paste it into chats or support tickets — SMSHub support never needs it.
  • Share one key across servers you don't control.

Loading the key from the environment

# .env — keep this file out of Git (add it to .gitignore)
SMSHUB_API_KEY=sk_live_your_key
Leaked a key? Rotate it under Dashboard → API access. The old key stops working immediately, so deploy the new one to your servers right after.

Send an SMS

POSThttps://sms.hizcore.com/api/sms

Sends one message to one recipient and uses one credit. Your SMS signature is appended automatically (see Sender IDs & signatures).

Headers

HeaderRequiredValue
AuthorizationYesBearer sk_live_…
Content-TypeYesapplication/json (form-encoded bodies are also accepted)

Example

curl -X POST 'https://sms.hizcore.com/api/sms' \
  -H "Authorization: Bearer sk_live_your_key" \
  -H 'Content-Type: application/json' \
  -d '{"msisdn":"0911234567","text":"Your verification code is 482193. It expires in 5 minutes."}'

Request parameters

FieldTypeRequiredDescription
msisdnstringYesRecipient Ethio Telecom number. Accepted formats: 0911234567, 251911234567, +251911234567. Spaces, dashes and brackets are ignored. Normalised to 09xxxxxxxx.
track_linksbooleanNoDefault false. When true, ?utm_source=smshub is added to every link in text before sending (see Links & UTM tracking).
textstringYesMessage body, UTF-8. Leading and trailing whitespace is trimmed. Maximum 160 characters (GSM-7) or 70 characters (Unicode, e.g. Amharic) including your signature.

Unknown fields are ignored. Request bodies larger than 64 KB are rejected. Safaricom Ethiopia numbers (07xxxxxxxx) and international numbers are rejected with invalid_msisdn.

Responses

Every response is JSON and includes a request_id (also sent as the X-Request-Id header). Include it when contacting support.

Success — 200

Response
{
  "status": "success",
  "data": {
    "msisdn": "0911234567",
    "message_status": "sent",
    "credits_used": 1,
    "credits_remaining": 4999,
    "signature_applied": true,
    "phone_in_signature": false
  },
  "request_id": "req_8f3a1c09d2e4b7a1"
}
FieldDescription
data.message_statussent — accepted by the carrier for delivery.
data.credits_usedAlways 1 for a successful send.
data.credits_remainingBalance after this message.
data.signature_appliedWhether your signature was appended.
data.phone_in_signatureWhether your phone number fit in the signature.

SMS character limits

SMSHub sends each message as a single SMS segment. The limit depends on the characters in the final text, including your signature:

EncodingUsed whenLimit
GSM-7Only Latin letters, digits and common punctuation160 characters
Unicode (UCS-2)Any Amharic (Ge'ez) character, emoji or other non-GSM symbol70 characters
  • A single Amharic character switches the whole message to Unicode — including the signature.
  • Your signature adds a blank line plus - Your Name (and optionally | 09xxxxxxxx). The phone number is dropped automatically if it doesn't fit.
  • Use /api/test to see the exact final text, character count and encoding before sending.

Test mode

POSThttps://sms.hizcore.com/api/test

Accepts exactly the same headers and body as /api/sms and runs every check — authentication, account state, rate limit, number format, length, moderation and signature — but never contacts the carrier and never uses credits. The dashboard's API page includes a test console.

Test response
{
  "status": "success",
  "data": {
    "mode": "test",
    "message_status": "test_passed",
    "would_send": true,
    "msisdn": "0911234567",
    "final_text": "Hello from SMSHub\n\n- Abebe Bekele | 0911000000",
    "characters": 44,
    "character_limit": 160,
    "encoding": "gsm7",
    "credits_used": 0,
    "provider_contacted": false,
    "sms_sent": false
  },
  "request_id": "req_0c9d8e7f6a5b4c3d"
}
Moderation applies in test mode too. Test messages don't add strikes, but hate speech and child-exploitation content still suspend the account.

OTP & verification codes

SMSHub delivers the message; your application creates and checks the code. That keeps codes under your control and works with any sign-up, login or payment-confirmation flow.

  1. 1
    Generate

    Create a 6-digit code with a cryptographically secure random generator (random_int, crypto.randomInt, secrets).

  2. 2
    Store a hash

    Save a hash of the code with an expiry (e.g. 5 minutes) and an attempt counter. Never store or log the plain code.

  3. 3
    Send it

    POST /api/sms with a short message such as “Your Abay Market code is 604218. It expires in 5 minutes. Do not share it.”

  4. 4
    Verify

    Compare in constant time, check the expiry and attempts, then delete the code so it can be used only once.

  5. 5
    Limit

    Allow a resend after about 60 seconds and lock the code after about 5 wrong attempts.

Writing the message

  • Keep it within 160 characters (Latin) or 70 (Amharic), including your signature — see character limits.
  • Say who it's from and how long it's valid, and add “Do not share it”.
  • Never ask the recipient to reply with or send the code — moderation blocks that as phishing (422 message_rejected).
  • Test your template with /api/test to see the exact final text.

Example: send and verify

<?php
// Your own table, e.g.: otp_codes(phone PK, code_hash, expires_at, attempts)

function startOtp(PDO $db, string $phone): void {
    $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    $db->prepare('REPLACE INTO otp_codes (phone, code_hash, expires_at, attempts)
                  VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE), 0)')
       ->execute([$phone, password_hash($code, PASSWORD_DEFAULT)]);

    $ch = curl_init('https://sms.hizcore.com/api/sms');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('SMSHUB_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'msisdn' => $phone,
            'text'   => "Your login code is {$code}. It expires in 5 minutes. Do not share it.",
        ]),
    ]);
    $body   = json_decode((string) curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status !== 200) {
        throw new RuntimeException(($body['error_type'] ?? 'network_error') . ' (' . ($body['request_id'] ?? '-') . ')');
    }
}

function checkOtp(PDO $db, string $phone, string $input): bool {
    $st = $db->prepare('SELECT code_hash, attempts FROM otp_codes WHERE phone = ? AND expires_at > NOW()');
    $st->execute([$phone]);
    $row = $st->fetch();
    if (!$row || $row['attempts'] >= 5) return false;            // expired or locked
    if (!password_verify($input, $row['code_hash'])) {
        $db->prepare('UPDATE otp_codes SET attempts = attempts + 1 WHERE phone = ?')->execute([$phone]);
        return false;
    }
    $db->prepare('DELETE FROM otp_codes WHERE phone = ?')->execute([$phone]);   // single use
    return true;
}
Each code is one normal SMS: one credit, the same 200 response and the same errors as any other message.

Errors

Errors use standard HTTP status codes and a consistent body. Branch on error_type; message is human-readable and may change.

Error response
{
  "status": "error",
  "error_type": "invalid_msisdn",
  "message": "Invalid msisdn. SMSHub delivers to Ethio Telecom numbers only…",
  "request_id": "req_1b2c3d4e5f6a7b8c"
}
HTTPerror_typeMeaningWhat to do
400invalid_jsonBody isn't valid JSON.Check serialisation and Content-Type.
401missing_authorizationNo Bearer token was sent.Add the Authorization header.
401invalid_api_keyKey is wrong, rotated or revoked.Rotate the key in the dashboard and deploy the new one.
402insufficient_creditsBalance is zero.Buy credits; retrying won't help.
403account_not_verifiedPhone not verified.Finish phone verification.
403kyc_not_approvedKYC pending or rejected.Check Dashboard → Verification.
403account_suspendedAccount suspended.Contact support. Do not retry.
405method_not_allowedNot a POST request.Use POST.
422invalid_msisdnNumber isn't a valid Ethio Telecom number.Validate numbers before sending.
422missing_texttext is empty.Provide a message.
422message_too_longOver 160 / 70 characters.Shorten the text. See limits.
422signature_too_longFits alone but not with your signature.Shorten the text or use a shorter signature name.
422message_rejectedBlocked by moderation. Includes a reference (MOD-…).Revise the content. Do not retry the same text.
429rate_limitedOver 60 requests/minute, or many invalid keys from one IP.Wait for Retry-After seconds.
502provider_errorCarrier didn't accept the message. Credit refunded.Retry with exponential backoff.
Only 429 and 502 (and network timeouts) are safe to retry automatically. Retrying other errors will fail the same way.

HTTP status codes

Check the HTTP status first, then error_type for the exact reason (see Errors). Every JSON response carries a request_id — log it and quote it to support.

StatusMeaningTypical error_typeRetry?How to handle
200OK — accepted by the carrier (or validated on /api/test)——Store request_id; one credit was used.
400Bad Request — the body isn't valid JSONinvalid_jsonNoFix serialisation; send Content-Type: application/json.
401Unauthorized — key missing, wrong, rotated or revokedmissing_authorization, invalid_api_keyNoCheck the Authorization: Bearer header; copy or rotate the key.
402Payment Required — not enough creditsinsufficient_creditsAfter top-upBuy credits in Billing, then send again.
403Forbidden — this account can't sendaccount_not_verified, kyc_not_approved, account_suspendedNoFinish verification, or contact support.
404Not Found — wrong URL (an HTML page, not JSON)—NoUse POST https://sms.hizcore.com/api/sms or /api/test.
405Method Not Allowedmethod_not_allowedNoSend a POST.
422Validation error — the request was understood but rejectedinvalid_msisdn, missing_text, message_too_long, signature_too_long, message_rejectedNoFix the number or text. Don't resend the same content.
429Too Many Requests — over 60/minute, or many invalid keys from one IPrate_limitedYesWait Retry-After seconds, then continue at a steady pace.
500Internal Server Error — unexpected error on our side—YesRetry with exponential backoff; send the X-Request-Id to support if it persists.
502Bad Gateway — the carrier didn't accept the message; credit refundedprovider_errorYesRetry with exponential backoff.
503Service Unavailable — scheduled maintenance or a temporary outagemaintenanceYesWait Retry-After seconds (maintenance sends 900).

Handling errors in code

async function sendSms(msisdn, text) {
  const res = await fetch('https://sms.hizcore.com/api/sms', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SMSHUB_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ msisdn, text }),
  });
  const body = await res.json().catch(() => ({}));
  if (res.ok) return body.data;                    // 200 — accepted by the carrier

  const err = new Error(`${res.status} ${body.error_type || 'error'}: ${body.message || res.statusText}`);
  err.retryable  = [429, 500, 502, 503].includes(res.status);   // safe to try again later
  err.retryAfter = Number(res.headers.get('Retry-After') || 0); // seconds (429 / 503)
  err.requestId  = body.request_id;                             // quote this to support
  throw err;
}

Rate limits

Each account can make 60 requests per minute, shared across /api/sms, /api/test and dashboard sending. Every authenticated response includes:

HeaderDescription
X-RateLimit-LimitRequests allowed per window (60).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix time when the window resets.
Retry-AfterOn 429 only: seconds to wait.

For large sends, queue messages on your side and send at a steady pace. Need a higher limit? Contact us on Telegram.

Sender IDs & signatures

Messages are delivered from SMSHub's shared route, so the sender shown on the recipient's phone is set by the carrier. To tell recipients who a message is from, SMSHub appends your SMS signature to every message:

Delivered message
Your appointment is confirmed for 10:00 AM.

- Bethel Clinic | 0911000000
  • By default the signature is your ID-verified legal name (changing your account name doesn't change it). You also choose whether your phone number is included.
  • The signature is always on — it can't be switched off or changed directly. To use a custom signature of up to 11 characters, submit a request under Dashboard → Settings → Request Custom Signature. An administrator approves or rejects it and you're notified either way. If it's approved, pay the one-time 150 ETB fee through Chapa and the signature is activated as soon as the payment is confirmed. Names of banks, wallets, telecoms or government bodies aren't allowed.
  • Custom alphanumeric sender IDs (e.g. BETHEL) require registration with the carrier and aren't self-service yet. Contact us on Telegram if you need one.

Pricing & credits

SMSHub is prepaid. One credit sends one single-segment SMS to one recipient. Credits never expire.

PackagePricePer SMS
500 SMS307.50 ETB0.615 ETB
2,000 SMS1,025 ETB0.513 ETB
5,000 SMS2,460 ETB0.492 ETB
10,000 SMS4,817.50 ETB0.482 ETB
25,000 SMS11,787.50 ETB0.472 ETB
50,000 SMS22,550 ETB0.451 ETB
  • When credits are used: when the carrier accepts a message. If the carrier rejects it (502), the credit is refunded immediately.
  • Not charged: validation errors, moderation blocks, rate-limited requests and anything sent to /api/test.
  • Payments: through Chapa (telebirr, CBE Birr, M-Pesa, Visa, Mastercard). Every payment is verified server-to-server with Chapa — reference, currency and amount must match — and credited exactly once, even if you close the browser.
  • Free credits: 15 credits are added once, when your KYC is approved.

Webhooks & callbacks

SMSHub does not currently send delivery-receipt webhooks to your server. The API response is the source of truth:

  • 200 with message_status: "sent" — accepted by the carrier.
  • 502 provider_error — not accepted; the credit was refunded.

Store the request_id and status from each response in your own database. The full history is also available under Dashboard → Message history and can be exported as CSV.

Payment webhooks from Chapa are handled internally by SMSHub: they are signature-checked, de-duplicated and re-verified with Chapa before credits are added — you don't need to configure anything.

Security

  • Transport: all traffic uses HTTPS. Plain HTTP is not supported.
  • Secrets: keep API keys in environment variables or a secrets manager. Never log full keys.
  • Least exposure: call the API from your backend only. If a key leaks, rotate it immediately.
  • Browser protection: a strict Content-Security-Policy, HSTS, CSRF tokens on every form, host-only secure session cookies, and automatic sign-out after 2 hours of inactivity or 7 days.
  • API keys: matched by SHA-256 fingerprint and stored encrypted (AES-256-GCM) for viewing. Suspended accounts have their keys revoked automatically.
  • Accounts: passwords (minimum 10 characters, common passwords rejected) are hashed with bcrypt and upgraded automatically; verification and reset codes are stored hashed, single-use, expire after 10 minutes and lock after repeated failures. Changing your password signs out every other device.
  • Payments: Chapa secret keys never reach the browser; every credit is preceded by a server-side verification with Chapa.
  • OTP messages: include a line such as “Do not share this code” and keep codes short-lived on your side.
  • Reporting: found a vulnerability? Email support@hizcore.com. Please don't test against other users' accounts.

Moderation policy

Every outgoing message — dashboard, API and test — is screened in English, Amharic and transliterated Amharic before a credit is reserved. Screening looks at context, not just keywords:

  • Allowed: “Your code is 482193. Do not share it with anyone.”  Blocked: “Your account is locked. Send us your PIN to unlock it.”
  • An insult word on its own is allowed (e.g. “ውሻዬ ታሟል” — my dog is sick); the same word aimed at the recipient is not.
  • News, education and counter-speech are routed to human review rather than automatic suspension.
CategoryEnforcement
Hate speechImmediate suspension. All sessions ended, API keys revoked, account locked.
Child sexual exploitationImmediate suspension. All sessions ended, API keys revoked, account locked.
Scam or phishingMessage blocked and held for review. One strike.
Threat of violenceMessage blocked. One strike.
Harassment, sexual contentMessage blocked. One strike.
Profanity (undirected)Message blocked. No strike.

Strikes expire after 90 days; 3 active strikes suspend sending until an administrator reviews the account. Every blocked message gets a MOD- reference you can quote to support to request a review. Where required or appropriate under Ethiopian law, information about serious violations may be reported to the Ethiopian Federal Police and/or INSA. Read the full Acceptable Use Policy.

Examples

Sending a one-time password (PHP)

otp.php
<?php
function sendOtp(string $phone): string {
    $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);

    $ch = curl_init('https://sms.hizcore.com/api/sms');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('SMSHUB_API_KEY'),
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'msisdn' => $phone,
            'text'   => "Your login code is {$code}. It expires in 5 minutes. Do not share it.",
        ]),
    ]);
    $body   = json_decode((string) curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status !== 200) {
        throw new RuntimeException(($body['error_type'] ?? 'network_error') . ': ' . ($body['message'] ?? ''));
    }
    // Store a hash of $code with a 5-minute expiry — never the plain code.
    return $code;
}

Retrying safely (Node.js)

send.js
async function sendSms(msisdn, text, attempt = 1) {
  const res = await fetch('https://sms.hizcore.com/api/sms', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SMSHUB_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ msisdn, text }),
  });
  const body = await res.json();

  // Only 429 and 502 are worth retrying.
  if ((res.status === 429 || res.status === 502) && attempt < 4) {
    const wait = res.status === 429
      ? Number(res.headers.get('Retry-After') || 1) * 1000
      : 2 ** attempt * 500;
    await new Promise(r => setTimeout(r, wait));
    return sendSms(msisdn, text, attempt + 1);
  }
  if (!res.ok) throw new Error(`${body.error_type}: ${body.message} (${body.request_id})`);
  return body.data;
}

Amharic message (Python)

amharic.py
import os, requests

res = requests.post(
    'https://sms.hizcore.com/api/sms',
    headers={'Authorization': f"Bearer {os.environ['SMSHUB_API_KEY']}"},
    json={'msisdn': '+251911234567', 'text': 'ትዕዛዝዎ ተልኳል። ዛሬ ይደርሳል።'},  # 70-char Unicode limit
    timeout=30,
)
print(res.status_code, res.json())

Troubleshooting

SymptomLikely causeFix
401 missing_authorization but the header is setYour web server strips Authorization (common on Apache/CGI).Check the header reaches us using /api/test; contact support with the request_id.
401 invalid_api_keyKey was rotated or revoked, or has extra whitespace.Copy the current key from Dashboard → API access (or rotate it) and update your server.
403 kyc_not_approvedVerification is pending or was rejected.Check Dashboard → Verification.
422 message_too_long for a short Amharic textAmharic uses the 70-character limit.Shorten the text or split it across messages.
422 signature_too_longMessage plus signature exceeds the limit.Shorten the message or the signature name, or untick “include my phone number”.
422 message_rejectedContent matched a moderation rule.Revise the message. If you think it's a mistake, open a ticket with the MOD- reference.
Paid but credits not addedChapa confirmation is delayed or you closed the page early.Billing → Check status. Credits are added automatically once Chapa confirms. Still missing? Contact support with the payment reference.
Payment shows “Under review”Chapa reported an amount or reference that doesn't match the order.Nothing is lost — a person checks it. Contact support with the reference if it's urgent.
Recipient says they didn't get itPhone off, out of coverage or number inactive.Confirm the number and that the response was 200; message history shows what was accepted.
429 rate_limitedMore than 60 requests in a minute.Queue and pace sends; honour Retry-After.

Frequently asked questions

Can I send to Safaricom numbers?

Not yet. Only Ethio Telecom 09xxxxxxxx numbers are supported.

Can I send to many recipients in one request?

No — one request sends one message. Loop over recipients on your side, respecting the rate limit.

Do credits expire?

No.

Is there a sandbox?

Yes: /api/test runs every validation without sending or charging. It uses your live key.

Why did my OTP get blocked?

Most likely it asked the recipient to send or reply with the code. Tell users to enter the code in your app and not to share it.

Can I send without my signature?

No. The signature is always on. You can request a custom signature name under Settings → Request Custom Signature; if an administrator approves it, a one-time 150 ETB fee activates it.

Can I get a refund for unused credits?

Contact support. Credits on accounts suspended for serious violations may be forfeited under the Terms of Use.

Do you store message content?

Yes, in your message history, so you can audit what was sent. Blocked messages are retained for moderation review.

Still stuck? Message us on Telegram, email support@hizcore.com or call 0941907682.