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: "status": "DELIVRD" 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
Create one in the X Lead dashboard. 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
}' 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.
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. |
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- Submission or delivery status of the request
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 the final status
{ "rc": 100, "rm": "OK", "ref_id": "145737475", "status": "DELIVRD" } 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.
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
Tell us the use case and expected volume; we open the account and configure routes suited to your destinations.
- 2
Create an API key
Generate the key in the dashboard, then 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 a test key first?
If you need to run integration tests against the API, contact us for a test API key and a small allocation of test points — no contract required first.
Request a test key →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's 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 + status so a redelivered callback does not double-decrement stock or double-send an email.
How should I handle the non-DELIVRD statuses? +
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 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, contact us for a test API key and a small allocation of test points.
Ready to get started?
Contact us — send soon after a quick account setup. Free trial credit lets you integrate first and confirm the platform fits your use case.