Taiwan SMS API quickstart — from sign-up to your first SMS to +886 (cURL, Node.js, Python, PHP)
Published
On this page
Key takeaways
- Sign up, apply for an API key in the dashboard, and once it is activated you can test on your own — sign-up comes with free SMS trial credit and needs no credit card.
- A send to a Taiwan number takes three fields,
country_code,mobileandcontent. Leavesenderout.- A synchronous
rc: 100only means the platform accepted the request. Submission and delivery reports reach you only after you set a Callback URL in the dashboard, and a message counts as delivered only when the 2nd callback’sdelivery_statusisSS.- Keep test content plain text: in Taiwan, commercial SMS containing a URL or phone number must be white-listed first.
- An
IMon the 1st callback is usually a number-format problem andIPis insufficient points. Neither is charged.
This guide is for engineers wiring SMS into their own system. The goal is narrow: using your own Taiwan mobile number, receive a first SMS sent through the API and confirm that it was actually delivered. There is no SDK to install, and both the account and the API key are applied for online.
You need three things: a Taiwan handset that can receive SMS, a terminal, and any language that can make an HTTP request.
Before you start: two things about sending to Taiwan
No sender name. Business SMS in Taiwan is shown with a numeric sender, so a request to 886 does not take the sender field. Leave it out — supplying one can cause validation to fail. Hong Kong is the opposite: a registered sender name is mandatory there.
Keep test content plain text. Since 18 November 2024, a business sending commercial SMS in Taiwan that contains a URL, a short link or a phone number must first be white-listed with a carrier or an SMS provider, and messages from unregistered senders are not sent (CNA report, in Chinese). Text-only content is unaffected, so one-time passcodes and test messages go straight out. For the full comparison of the two markets, see What is SMS? How text messaging works, character limits, pricing and OTP use (Hong Kong & Taiwan).
Step 1: Sign up
Create an account on the X Lead sign-up page. Sign-up comes with free SMS trial credit and needs no credit card.
UFOSEND’s sending platform and API are provided by X Lead, which is why the account, the dashboard and the API all sit on xleadfunnel.com.
Step 2: Apply for an API key
Log in to the dashboard and apply for an API key. It is ready to use once we have reviewed and activated the application. Every request carries it in the x-api-key header. There is no OAuth flow and no token to refresh.
The key carries your account’s sending rights, so keep it server-side — never in front-end code or a public repository. The examples below read it from an environment variable:
export XLEAD_API_KEY="paste your API key here"
Step 3: Check your points balance to confirm the key works
Before sending anything, call an endpoint that costs no points to confirm the key and your network path are fine:
curl https://api.connect.xleadfunnel.com/v1/acct/get-points-balance \
-H "x-api-key: $XLEAD_API_KEY"
The response returns the points left on the account. SMS and MMS draw on the same pool:
{
"rc": 100,
"rm": "Remaining points for sending SMS & MMS",
"points": "380.242"
}
An rc of 100 means the key is valid.
Step 4: Send your first SMS
The endpoint is POST /v1/sms/send, one request per recipient. Replace mobile with your own number:
curl -X POST https://api.connect.xleadfunnel.com/v1/sms/send \
-H "Content-Type: application/json" \
-H "x-api-key: $XLEAD_API_KEY" \
-d '{
"country_code": "886",
"mobile": "0912345678",
"content": "My first SMS sent through the API"
}'
The rules for the three fields:
| Field | Required | Notes |
|---|---|---|
country_code | Yes | Dialling code, 1–3 digits, no leading zeros and no +. Taiwan is 886. |
mobile | Yes | Recipient number, up to 10 digits. The leading zero is optional — 0912345678 and 912345678 both deliver. |
content | No | Message body, UTF-8. |
A successful call returns immediately:
{
"rc": 100,
"rm": "OK",
"ref_id": "145737475"
}
ref_id identifies this request. Store it — the callbacks that follow carry the same value.
The same request in other languages
Each of these uses the language’s built-in or most common HTTP client.
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({
country_code: '886',
mobile: '0912345678',
content: 'My first SMS sent through the API',
}),
});
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={
'country_code': '886',
'mobile': '0912345678',
'content': 'My first SMS sent through the API',
},
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([
'country_code' => '886',
'mobile' => '0912345678',
'content' => 'My first SMS sent through the API',
], 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 and C# versions are in the SMS API documentation — drop sender from the Hong Kong example and change country_code to 886.
Step 5: Confirm it was delivered
A buzzing phone is the quickest check, but nobody watches a handset once the system is live. rc: 100 only means the platform accepted and queued the request. Two reports follow, both pushed to the Callback URL you set in the dashboard. Without that setting, neither report is sent.
1st callback: submission. When the request passes validation and is handed to the carrier you receive SMD. This tier decides whether you are charged:
{
"ref_id": "120731617",
"rc": 100,
"rm": "A valid send, message has been submitted to carrier for delivery",
"status": "SMD",
"num_sms_count": 1,
"used_points": 0.36,
"remaining_points": 99.64
}
num_sms_count, used_points and remaining_points are the number of messages billed, the points deducted and the points left. The values above are an example; yours will differ. Chinese content is Unicode-encoded, which limits a single message to 70 characters before it is split and billed per segment, so check that num_sms_count matches what you expected.
2nd callback: delivery. Once the carrier reports the final status of the recipient’s handset, the platform pushes a second callback. This one carries the result in delivery_status, where SS means delivered:
{
"ref_id": "120731617",
"rc": 100,
"rm": "Carrier has successfully delivered to the recipient's device",
"delivery_status": "SS"
}
SF means not delivered, and in that case a desc_code gives the reason:
{
"ref_id": "120731617",
"rc": 100,
"rm": "Undelivered due to unreachable destination, network issues, or recipient",
"delivery_status": "SF",
"desc_code": "UNDELIV"
}
desc_code | Meaning |
|---|---|
EXPIRED | Not delivered within the validity period |
UNDELIV | Undelivered — unreachable destination, a network issue or the device switched off |
REJECTD | Rejected — invalid content, blacklisted or a policy restriction |
UNKNWN | The carrier cannot determine the final status |
Note that the two tiers use different field names: read status on the 1st callback and delivery_status on the 2nd. On a first integration, log the whole payload as it arrives and confirm the fields before writing any branching logic. A minimal receiver looks like this:
import express from 'express';
const app = express();
app.use(express.json());
app.post('/sms-callback', (req, res) => {
console.log('callback', JSON.stringify(req.body));
res.sendStatus(200);
});
app.listen(3000);
The Callback URL has to be an HTTPS address reachable from the public internet. The same callback can arrive more than once, so in production key on ref_id plus the status (status on the 1st callback, delivery_status on the 2nd) and make the handler safe to run twice. For what to do with each status, see SMS DLR status codes explained — what SMD, DELIVRD, UNDELIV and REJECTD actually mean.
Nothing arrived? Check this table first
If the 1st callback returns rc: -100, the request was not sent and is not charged:
| Status | Cause | What to do |
|---|---|---|
IM | Incorrect country code or mobile number | Check that country_code is 886 (no +, no leading zeros) and that mobile is no longer than 10 digits with no spaces or symbols |
IP | Insufficient points | Check the balance with the Step 3 endpoint, then top up in the dashboard |
IUL | The number is on the unsend list | The number was previously marked do-not-send; test with a different one |
IS | Invalid sender | Do not send the sender field to Taiwan |
If the 1st callback returned SMD but the phone stayed silent, read the 2nd callback: when delivery_status is SF, look at desc_code. For UNDELIV, confirm the handset is switched on and has signal. REJECTD is usually a content problem, so confirm the message has no URL or phone number in it.
If no callback arrives at all, the SMS is rarely the problem. Either the Callback URL has not been set in the dashboard, or your address cannot be reached from outside.
Before you go live
Once the test works, three things remain before production:
- If the content needs a link or a phone number, get white-listed first. The domain has to match what was registered. A third-party URL shortener will not match, so use your own domain.
- Rate-limit the OTP endpoint. Once a send endpoint is public it can be used to pump traffic. See Six pitfalls in OTP SMS implementation — from code design to SMS bombing defence.
- Normalise numbers when you store them. What sits in the database should be a clean
country_codeandmobilepair, not raw user input cleaned up at send time.
Full definitions of every endpoint, parameter and status code are in the SMS API documentation. If you do not have an account yet, sign up free, apply for an API key and test with your own number once it is activated. For larger volumes or special requirements, talk to us.