Taiwan SMS API quickstart — from sign-up to your first SMS to +886 (cURL, Node.js, Python, PHP)

On this page
  1. Before you start: two things about sending to Taiwan
  2. Step 1: Sign up
  3. Step 2: Apply for an API key
  4. Step 3: Check your points balance to confirm the key works
  5. Step 4: Send your first SMS
  6. Step 5: Confirm it was delivered
  7. Nothing arrived? Check this table first
  8. Before you go live

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, mobile and content. Leave sender out.
  • A synchronous rc: 100 only 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’s delivery_status is SS.
  • Keep test content plain text: in Taiwan, commercial SMS containing a URL or phone number must be white-listed first.
  • An IM on the 1st callback is usually a number-format problem and IP is 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:

FieldRequiredNotes
country_codeYesDialling code, 1–3 digits, no leading zeros and no +. Taiwan is 886.
mobileYesRecipient number, up to 10 digits. The leading zero is optional — 0912345678 and 912345678 both deliver.
contentNoMessage 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_codeMeaning
EXPIREDNot delivered within the validity period
UNDELIVUndelivered — unreachable destination, a network issue or the device switched off
REJECTDRejected — invalid content, blacklisted or a policy restriction
UNKNWNThe 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:

StatusCauseWhat to do
IMIncorrect country code or mobile numberCheck that country_code is 886 (no +, no leading zeros) and that mobile is no longer than 10 digits with no spaces or symbols
IPInsufficient pointsCheck the balance with the Step 3 endpoint, then top up in the dashboard
IULThe number is on the unsend listThe number was previously marked do-not-send; test with a different one
ISInvalid senderDo 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:

  1. 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.
  2. 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.
  3. Normalise numbers when you store them. What sits in the database should be a clean country_code and mobile pair, 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.