The SMS API, written for engineers.
REST + JSON · x-api-key auth · two-tier DLR callbacks
One POST sends the message. Two callbacks tell you it was
submitted and then that it was delivered — we report those separately because
they were never the same thing. OFCA Sender IDs, DNC filtering and the per-region field
differences are all spelled out below.
After the product overview instead? See the SMS service page — 100+ HK securities firms, dual-route failover, # Sender ID handled for you.
POST /v1/sms/send
x-api-key: YOUR_API_KEY
{
"sender": "XLEAD",
"country_code": "852",
"mobile": "91234567",
"content": "Your login code is 482910"
}
→ { "rc": 100, "ref_id": "145737475" }
→ callback 1: "status": "SMD"
→ callback 2: "delivery_status": "SS" One POST and your first SMS is on its way
No SDK, no OAuth dance. Grab an API key, paste the snippet below, change the number, run it.
- 1
Get an API key
Apply for one in the X Lead dashboard. Once it is activated, every request carries it in the x-api-key header.
- 2
Call the send endpoint
POST one JSON body — one request per recipient. The response returns a ref_id immediately.
- 3
Receive callbacks
Set a Callback URL in the dashboard and we push submission plus carrier delivery status back to you.
Send one Hong Kong SMS
curl -X POST https://api.connect.xleadfunnel.com/v1/sms/send \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"sender": "XLEAD",
"country_code": "852",
"mobile": "91234567",
"content": "Your login code is 482910",
"send_also_ofca_registrants": false
}' Send one Taiwan SMS (+886)
Taiwan takes no sender and no send_also_ofca_registrants; every other field is the same.
curl -X POST https://api.connect.xleadfunnel.com/v1/sms/send \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"country_code": "886",
"mobile": "0912345678",
"content": "Your login code is 482910"
}' Taiwan SMS API quickstart: from sign-up to your first SMS to +886 →
Synchronous response
{
"rc": 100,
"rm": "OK",
"ref_id": "145737475"
} ref_id identifies this request — both callbacks carry the same ref_id, so use it to reconcile.
Taiwan SMS API: integrate first, compliance next
Hong Kong steps stay in Quickstart and Getting started above. For Taiwan numbers the payload differs: no sender field in the API, and plain-text OTP to your own phone is usually enough for an integration test.
- 1
Open an account
Sign up online for trial credit — no credit card.
- 2
Apply for an API key
Request one in the dashboard; call the send endpoint with x-api-key once it is active.
- 3
Send your first +886
Set country_code to 886 and mobile to a Taiwan number; start with plain-text OTP content.
- 4
Before production
Messages with URLs or phone numbers need Taiwan commercial-SMS white-list registration; marketing copy must meet local labelling rules.
Step-by-step: sign-up to your first +886 SMS → Sender labelling and sample copy → Sign up free and apply for a key →
Authentication, endpoints and parameters
One header handles auth; three endpoints cover the whole send chain: send, usage, balance.
Authentication
- Base URL
https://api.connect.xleadfunnel.com- Auth
Header: x-api-key: YOUR_API_KEY- Encoding
UTF-8- Request format
application/json (POST)
Endpoints
- POST
/v1/sms/sendSend one SMS — one request per recipient. - GET
/v1/sms/get-usage-reportUsage summary for a date range (date_from / date_to, YYYY-MM-DD). - GET
/v1/acct/get-points-balanceRemaining points balance. SMS and MMS draw on the same pool.
POST /v1/sms/send parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
country_code | string | Required | Region dialling code, 1–3 digits, no leading zeros. Hong Kong is 852; Taiwan is 886. |
mobile | string | Required | Recipient's mobile number, up to 10 digits, a leading zero is accepted. |
content | string | Optional | Message body, UTF-8. |
sender | string | Optional | Sender name, 3–11 alphanumeric characters. Hong Kong requires a pre-registered Sender ID; Taiwan does not take this field. |
send_also_ofca_registrants | boolean | Optional | Defaults to false — do not send to numbers on the OFCA Do-Not-Call register. Hong Kong only. |
Response fields
| Parameter | Type | Description |
|---|---|---|
rc | integer | 100 = OK; -100 = invalid input |
rm | string | Return message, carries the reason on failure |
ref_id | string | Identifier for this request; both callbacks echo it back |
Region differences: one field, two rules
Whether `sender` is required is decided by the destination regulator — which is why one hard-coded payload will not work across regions.
A pre-registered Sender ID is mandatory or the request is rejected. send_also_ofca_registrants controls whether the OFCA DNC register is bypassed.
No sender field needed. Leave it out — supplying one can cause validation to fail.
Two tiers of reporting — submission first, delivery second
"Submitted" is not "delivered". We split them into two separate callbacks so you can tell a bad number on your side from a failure at the carrier.
Set a Callback URL in your dashboard first — otherwise none of these fire.
Callback payload
rc- Return code
rm- Return message with detail, where available
ref_id- The reference id we returned on your original API call
status- 1st callback: submission status of the request
delivery_status- 2nd callback: SS = delivered, SF = not delivered
desc_code- Present only when delivery_status = SF — the reason it was not delivered
Example callbacks
// 1st — submitted to carrier, this one is charged
{ "rc": 100, "rm": "OK", "ref_id": "145737475", "status": "SMD" }
// 1st — validation failed, not charged
{ "rc": -100, "rm": "...", "ref_id": "145737476", "status": "IM" }
// 2nd — carrier reported delivered
{ "rc": 100, "rm": "...", "ref_id": "145737475", "delivery_status": "SS" }
// 2nd — not delivered, desc_code is the reason
{ "rc": 100, "rm": "...", "ref_id": "145737477",
"delivery_status": "SF", "desc_code": "UNDELIV" } 1st callback · send request report
Whether your request passed validation and reached the carrier. This tier decides whether you are charged.
rc = 100 message will be charged SMD- A valid send — message submitted to the carrier for delivery
rc = -100 message will not be charged IM- Incorrect country code or mobile number
OFCA- On the OFCA DNC register and filtered (Hong Kong only)
IUL- On the unsend list — message did not go out
IP- Insufficient points
IS- Invalid sender, or the Sender ID is not registered on your account
2nd callback · delivery report (DLR)
When the carrier reports the final handset status, we relay it to you unchanged. This tier reports in delivery_status: SS is DELIVRD (delivered); SF is not delivered, with desc_code carrying one of the reasons below.
rc = 100DELIVRD- Carrier delivered to the recipient device
EXPIRED- Not delivered — processing exceeded the validity period
UNDELIV- Undelivered — unreachable destination, network issue, or device switched off
REJECTD- Rejected — invalid content, blacklisted, or policy restriction
UNKNWN- The carrier cannot determine the final delivery status
Why we publish every failure code
Some providers return "submitted" and bill you regardless. We split submission from delivery and pass each failure code through untouched because you need to reconcile: requests accepted, requests submitted, messages actually delivered — all three numbers have to line up.
How Honest Delivery works →Wire it up in whatever you already use
The same POST, six ways. Each one uses the language's standard HTTP client — no SDK of ours to install.
cURL
curl -X POST https://api.connect.xleadfunnel.com/v1/sms/send \
-H "Content-Type: application/json" \
-H "x-api-key: $XLEAD_API_KEY" \
-d '{
"sender": "XLEAD",
"country_code": "852",
"mobile": "91234567",
"content": "Your login code is 482910"
}' Node.js
const res = await fetch('https://api.connect.xleadfunnel.com/v1/sms/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.XLEAD_API_KEY,
},
body: JSON.stringify({
sender: 'XLEAD',
country_code: '852',
mobile: '91234567',
content: 'Your login code is 482910',
}),
});
const { rc, rm, ref_id } = await res.json();
if (rc !== 100) throw new Error(`send failed: ${rm}`);
console.log('queued as', ref_id); Python
import os, requests
res = requests.post(
'https://api.connect.xleadfunnel.com/v1/sms/send',
headers={'x-api-key': os.environ['XLEAD_API_KEY']},
json={
'sender': 'XLEAD',
'country_code': '852',
'mobile': '91234567',
'content': 'Your login code is 482910',
},
timeout=10,
)
data = res.json()
if data['rc'] != 100:
raise RuntimeError(f"send failed: {data['rm']}")
print('queued as', data['ref_id']) PHP
<?php
$ch = curl_init('https://api.connect.xleadfunnel.com/v1/sms/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . getenv('XLEAD_API_KEY'),
],
CURLOPT_POSTFIELDS => json_encode([
'sender' => 'XLEAD',
'country_code' => '852',
'mobile' => '91234567',
'content' => 'Your login code is 482910',
], JSON_UNESCAPED_UNICODE),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($data['rc'] !== 100) {
throw new RuntimeException('send failed: ' . $data['rm']);
}
echo 'queued as ' . $data['ref_id']; Java
var body = """
{"sender":"XLEAD","country_code":"852",
"mobile":"91234567","content":"Your login code is 482910"}
""";
var req = HttpRequest.newBuilder()
.uri(URI.create("https://api.connect.xleadfunnel.com/v1/sms/send"))
.header("Content-Type", "application/json")
.header("x-api-key", System.getenv("XLEAD_API_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println(res.body()); // {"rc":100,"rm":"OK","ref_id":"..."} C# / .NET
using var http = new HttpClient();
http.DefaultRequestHeaders.Add(
"x-api-key", Environment.GetEnvironmentVariable("XLEAD_API_KEY"));
var payload = new {
sender = "XLEAD",
country_code = "852",
mobile = "91234567",
content = "Your login code is 482910"
};
var res = await http.PostAsJsonAsync("https://api.connect.xleadfunnel.com/v1/sms/send", payload);
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
if (data.GetProperty("rc").GetInt32() != 100)
throw new Exception(data.GetProperty("rm").GetString()); In production, read the API key from an environment variable rather than hard-coding it.
3 steps from account to production
No long contract before you can try it. Most clients have a test send working in the first week, with Sender ID registration running in the background.
- 1
Open an account
Sign up online — it comes with free SMS trial credit and needs no credit card. For larger volumes, or a # Sender ID registration for Hong Kong, tell us the use case and expected volume and we configure routes suited to your destinations.
- 2
Apply for an API key
Apply for the key in the dashboard; once it is activated, send it on every request in the x-api-key header. Server-side only.
- 3
Set up the API and test in 3 minutes
Send a one-click test from the dashboard to confirm routing and presentation, then follow the Quickstart above to send your first message through the API — about 3 minutes in total.
Once you are sending, follow up with
- +
Set a Callback URL (optional)
When you want the two-tier DLR callbacks, enter your endpoint in the dashboard — without it no DLRs reach you.
- +
Register a # Sender ID (for your brand)
Sending to Hong Kong requires a registered sender name — it is also your brand in the inbox. We handle the naming checks, documents and secure network; this runs in parallel with your integration.
Want to test the API first?
Once you have signed up you can apply for an API key in the dashboard and, when it is activated, send to your own phone on the trial credit — no contract required first. Taiwan (+886) takes no sender, so you can test as soon as the key is active. Hong Kong needs a Sender ID registered on your account; if you do not have one yet, contact us to arrange a test.
Integration FAQ
Can one request go to multiple recipients? +
No. /v1/sms/send is one request per recipient. Loop on your side for a broadcast — each send returns its own ref_id, so reconciliation and retries stay per-message. For large scheduled broadcasts the dashboard sender is a better fit than a hand-rolled loop.
Does rc = 100 mean the customer received it? +
No. The synchronous rc = 100 only means we accepted and queued the request. Submission is confirmed by the 1st callback (status = SMD means it reached the carrier); actual arrival on the handset is the 2nd callback reporting delivery_status = SS (that is, DELIVRD). Conflating these three is the single most common reporting bug we see.
Why is sender required for Hong Kong but not Taiwan? +
Different rules. Hong Kong's OFCA runs a sender-registration scheme, so commercial messages must use a registered sender name — sender is mandatory and must be a Sender ID registered on your account. Taiwan has no equivalent scheme and does not take the field. Build the payload per destination region.
What should send_also_ofca_registrants be set to? +
It defaults to false, meaning numbers on the OFCA Do-Not-Call register are not sent to — marketing traffic should keep that default. Only consider true where the message falls outside what the register governs (for example a transactional notice under an existing relationship with the recipient), and the responsibility for that call sits with the sender. When unsure, leave it false.
My callbacks never arrive — what do I check? +
In order: is a Callback URL saved in the dashboard; is that URL reachable from the public internet and returning 2xx; is it HTTPS with a valid certificate; is a firewall dropping it. Also make your handler idempotent — key on ref_id plus the status (status on the 1st callback, delivery_status on the 2nd) so a redelivered callback does not double-decrement stock or double-send an email.
How should I handle the non-DELIVRD statuses? +
These arrive in the 2nd callback's desc_code, which is present only when delivery_status is SF. UNDELIV is usually a powered-off or out-of-coverage handset — retry later. EXPIRED means it never got through within the validity window; confirm the number is still live before retrying. REJECTD is a content or policy problem — resending the same body will just be rejected again, so fix the content or check the sender setup. UNKNWN means the carrier could not determine the outcome; do not treat it as an outright failure and blindly resend — track the rate first.
How do I reconcile my numbers? +
Use ref_id to join three counts: requests you sent, 1st callbacks that returned SMD (this is the billing basis), and 2nd callbacks that returned delivery_status = SS (DELIVRD). /v1/sms/get-usage-report returns sms_count and points_used for a date range so you can cross-check against your own records.
Is there a test environment? +
Once your account is open, an admin can fire a one-click test send from the dashboard to confirm the Sender ID and routing before any code is written. For API-level integration testing, sign up, apply for an API key in the dashboard and, once it is activated, send to your own phone on the trial credit: Taiwan (+886) takes no sender, so it can be tested straight away; Hong Kong needs a Sender ID registered on your account, and if you do not have one yet you can contact us to arrange a test.
Free API access. No credit card. Test before you commit.
Sign up for free SMS trial credit and API access — no credit card required. Self-serve account, so you can confirm the platform fits your use case first; enterprise teams can also contact us for assisted onboarding.