Simple, powerful SMS API for sending messages, receiving replies, and managing delivery reports
Migrating from Bulletin Connect? See our simple 3-step migration guide
| Function | Endpoint |
|---|---|
| Get Access Token | https://api.websms.co.nz/api/connexus/auth/token |
| Send SMS | https://api.websms.co.nz/api/connexus/sms/out |
| Webhooks (Incoming SMS & Delivery Reports) | Configure in Members Area |
| Query Incoming Messages | https://api.websms.co.nz/api/connexus/mo/query |
| Query Delivery Status | https://api.websms.co.nz/api/connexus/sms/status/query |
| Billing Report (by rateCode) | https://api.websms.co.nz/api/connexus/billing/query |
| Query Blocked Numbers | https://api.websms.co.nz/api/connexus/unsubscribes/query |
| Check Balance | https://api.websms.co.nz/api/connexus/sms/balance |
| Send OTP/2FA Code | https://api.websms.co.nz/api/connexus/sms/otp |
| Appointment Reminder | https://api.websms.co.nz/api/connexus/sms/appointment |
| Number Validation (IPMS) | https://api.websms.co.nz/api/connexus/number/lookup |
| Message Class (Transactional / Marketing) | Add messageClass=transactional or messageClass=marketing to every send |
| Sandbox / Testing Mode | Add sandbox=true to any send request |
The Connexus API supports two authentication methods:
Simple authentication using your WebSMS email and password. Best for quick testing and simple integrations.
curl -X POST https://api.websms.co.nz/api/connexus/sms/out \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword" \
-d "to=6421234567" \
-d "body=Hello" \
-d "messageClass=transactional"
More secure authentication using client credentials. Best for production applications.
Go to API Keys in your dashboard to create a new key. You'll receive:
cid_xxx...) - Public identifiercsk_xxx...) - Keep this secret!Exchange your client credentials for an access token:
curl -X POST https://api.websms.co.nz/api/connexus/auth/token \
-d "client_id=cid_abc123..." \
-d "client_secret=csk_xyz789..."
{
"access_token": "wst_abc123...",
"token_type": "Bearer",
"expires_in": 86400
}
Include the access token in the Authorization header:
curl -X POST https://api.websms.co.nz/api/connexus/sms/out \
-H "Authorization: Bearer wst_abc123..." \
-d "to=6421234567" \
-d "body=Hello World" \
-d "messageClass=transactional"
| URL | https://api.websms.co.nz/api/connexus/auth/token |
| Method | POST |
| Content-Type | application/x-www-form-urlencoded or application/json |
| HTTP Code | Error | Description |
|---|---|---|
| 400 | invalid_request |
Missing client_id or client_secret |
| 401 | invalid_client |
Invalid credentials, key disabled, or key expired |
| 405 | method_not_allowed |
Only POST method is accepted |
To help protect recipients from phishing and spam, all links in outbound messages are checked against your account's list of approved domains. Messages containing links to a domain that hasn't been whitelisted will be held until the domain is approved.
Whitelisting is done within our portal — open the Request whitelist form, enter your domain, and submit. Once approved, messages containing links to that domain are sent immediately.
example.com), so you only need to request each domain once — not every individual URL.This is a quiet-hours restriction applied for you so that sending stays within the Appropriate Timing requirement of our Terms of Service — messages should reach recipients at reasonable hours. Nothing is lost or rejected: the API still accepts the request and returns a message ID you can track as normal.
{
"success": true,
"status": "queued",
"message_id": "1158493",
"to": "6421234567",
"from": "552",
"parts": 1,
"queue_reason": "nighttime",
"queue_message": "Message queued for delivery at 7am NZ time"
}
Check for status: "queued" rather than status: "accepted" if your integration needs to distinguish the two. Delivery reports for a queued message arrive after it is released, not at submission time.
64. International destinations are never night-queued, so send those according to the recipient's own local time./api/connexus/sms/otp bypass the queue entirely and go out immediately, day or night — a login code is useless the next morning.messageClass does not change this.status: "accepted" and no further changes are needed on your side.
On our shared shortcodes we handle opt-outs for you: an inbound STOP (or any of the accepted opt-out keywords) blocks that number on your account automatically, and you can read the list back via Query Blocked Numbers.
Send your first SMS in seconds:
curl -X POST https://api.websms.co.nz/api/connexus/sms/out \
-H "Authorization: Bearer wst_your_token..." \
-d "to=6421234567" \
-d "body=Hello World" \
-d "messageClass=transactional"
curl -X POST https://api.websms.co.nz/api/connexus/sms/out \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword" \
-d "to=6421234567" \
-d "body=Hello World" \
-d "messageClass=transactional"
{
"success": true,
"status": "accepted",
"message_id": "1158493",
"to": "6421234567",
"from": "552",
"parts": 1,
"route": "smsc_2degrees",
"porting": {
"ported": false,
"carrier": "2degrees",
"source": "redis"
}
}
The route and porting fields are included for NZ numbers when carrier information is available.
Send the same message to multiple recipients in a single API call using JSON body with to as an array:
HTTP 400. Split larger sends into multiple requests.
curl -X POST https://api.websms.co.nz/api/connexus/sms/out \
-H "Authorization: Bearer wst_your_token..." \
-H "Content-Type: application/json" \
-d '{
"to": ["6421111111", "6422222222", "6423333333"],
"body": "Your appointment is tomorrow at 2pm",
"messageClass": "transactional"
}'
{
"success": true,
"messages": [
{
"success": true,
"status": "accepted",
"message_id": "1188236",
"to": "6421111111",
"from": "552",
"parts": 1,
"route": "smsc_vodafone",
"porting": {"ported": true, "carrier": "One NZ", "source": "redis"}
},
{
"success": true,
"status": "accepted",
"message_id": "1188237",
"to": "6422222222",
"from": "552",
"parts": 1,
"route": "smsc_vodafone"
},
{
"success": true,
"status": "accepted",
"message_id": "1188238",
"to": "6423333333",
"from": "552",
"parts": 1,
"route": "smsc_2degrees"
}
],
"summary": {
"total": 3,
"sent": 3,
"failed": 0
}
}
Each message in the messages array has the same format as a single SMS response. The summary provides totals for easy tracking.
| Parameter | Required | Description | Example |
|---|---|---|---|
userId |
Required | Your WebSMS account email | user@domain.co.nz |
password |
Required | Your WebSMS account password | mypassword |
to |
Required | Recipient phone number (international format). For bulk sends, use JSON body with to as an array. |
6421234567 or ["6421111111","6422222222"] |
body |
Required | Message content. Sent as GSM-7 — see Message encoding, as characters outside that set are not delivered as typed. | Hello World |
messageClass |
Required | Classifies the message so it can be routed over the correct shortcode. Must be transactional (one-to-one, user-initiated messages such as OTPs, alerts and notifications) or marketing (promotional or bulk campaign traffic). See Message Class for details. |
transactional |
from |
Optional | Sender ID | 552 |
messageId |
Optional | Unique message identifier (max 36 chars) | MSG123456 |
rateCode |
Optional | Message source identifier (max 95 chars) | CAMPAIGN1 |
contentType |
Optional | Message type | text/plain |
fragmentationLimit |
Optional | Maximum SMS parts (1–11, or 0 for no limit of your own). 11 parts is 1683 characters. A longer message is trimmed to fit, and the response then carries truncated: true. |
11 |
sandbox |
Optional | If true, validates and returns a demo response without sending the SMS or billing your account. Useful for integration testing. |
true |
Messages are sent as GSM-7. Before sending, we map the
common typographic characters to their plain equivalents — curly
quotes become ', en/em dashes become -,
… becomes ..., and accented and macronised
vowels are folded to unaccented letters (Māori is sent as
Maori).
Any remaining character outside the ASCII range — emoji, currency
symbols such as €, and non-Latin scripts — is
replaced with ?. This endpoint does not send UCS-2/Unicode.
Use sandbox=true and read sandbox_message back to
see exactly what would go out.
A single SMS holds 160 characters. Longer messages are split into parts of
153 characters each, up to the fragmentationLimit —
11 parts, or 1683 characters. Each part is billed
separately, so a 1683-character message costs 11× a single SMS. The
parts field in the response tells you what you were charged for.
| 204 | Success - Message sent |
| 400 | Bad Request - Missing parameters |
| 401 | Unauthorized - Invalid credentials |
| 403 | Forbidden - Insufficient funds |
| 500 | Server Error |
| Standard SMS (160 chars) | $0.10 +GST |
| Multi-part SMS (per part) | $0.10 +GST |
| Unicode SMS (70 chars) | $0.10 +GST |
| GST applies to NZ customers only | |
Every send must declare a messageClass so we can route it over the appropriate shortcode. Choose the value that matches the intent of the message:
| Value | Use for | Examples |
|---|---|---|
transactional |
One-to-one, user-initiated or service messages the recipient is expecting. | OTP / 2FA codes, booking confirmations, delivery updates, account alerts, appointment reminders |
marketing |
Promotional or bulk campaign traffic sent to a list. | Sales and offers, newsletters, product announcements, event invites |
Classifying marketing traffic correctly improves deliverability and keeps your transactional messages on high-trust routes. Misclassifying marketing traffic as transactional may be treated as a compliance issue.
Add sandbox=true to any request to /sms/out, /sms/otp, or /sms/appointment to validate your integration without actually sending an SMS. In sandbox mode WebSMS will:
400/401/403 errors for bad inputs or unauthorised shortcodes)fragmentationLimit truncation, so you can confirm what would have been sentSANDBOX_ message ID and "sandbox": trueNothing is queued, sent, logged to your message history, or billed to your account. Balance is not checked, so sandbox requests work even with a zero balance.
sandbox_ is returned only in sandbox mode — don't expect it in a real send response. Use it as a signal that the call did not actually deliver anything.
curl -X POST https://api.websms.co.nz/api/connexus/sms/out \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword" \
-d "to=6421234567" \
-d "body=Hello from sandbox" \
-d "messageClass=transactional" \
-d "sandbox=true"
{
"success": true,
"status": "sandbox",
"message_id": "SANDBOX_671e4d8a9f3c2",
"to": "6421234567",
"from": "2190",
"parts": 1,
"sandbox": true,
"sandbox_message": "Hello from sandbox",
"sandbox_encoding": "GSM-7",
"sandbox_would_charge": 0.092
}
sandbox_would_charge is GST-inclusive and reflects your account's rate (or the $0.08 template rate for OTP/appointment, whichever is lower). sandbox_message is useful for spotting unintended character conversions (e.g. a smart apostrophe that becomes ?, or a long message clipped by fragmentationLimit).
Your webhook receives POST requests with JSON payloads. Check the type field to determine which event occurred:
"type": "SMS" - Incoming SMS message"type": "dlr" - Delivery status report{
"type": "SMS",
"messageId": "987654",
"connexusMessageId": "MO6a1b2c3d4e5f",
"from": "+6421234567",
"to": "shortcode",
"body": "Reply text",
"timestamp": 1234567890,
"encoding": "text/plain",
"network": "carrier_name"
}
Replies to an outbound message also include replyTo and replyToConnexusId referencing the original message, if available. If you supplied a custom messageId when sending the original message, the reply also includes it as replyToCustomerMessageId.
{
"type": "dlr",
"messageId": "987654",
"connexusMessageId": "MSG6a1b2c3d4e5f",
"status": "DELIVRD",
"statusCode": 1,
"timestamp": 1234567890,
"details": {
"smsc": "carrier_name",
"smscid": "abc12345-6789-def0-1234-56789abcdef0"
}
}
If you supplied a custom messageId when sending, the report also includes it as customerMessageId.
| statusCode | status | Description |
|---|---|---|
1 | DELIVRD | Message delivered successfully |
2 | UNDELIV | Message undeliverable |
4 | QUEUED | Message expired/queued |
8 | ACCEPTD | Message accepted by network |
16 | UNDELIV | Message undeliverable |
-1 | BLOCKED | Message blocked before carrier submission. The report includes an additional blockedReason field: unsubscribed (recipient has opted out) or invalid_number. This is a terminal status — no further delivery reports will follow. |
<?php
// webhook.php - handles both incoming SMS and delivery reports
$data = json_decode(file_get_contents('php://input'), true);
if (($data['type'] ?? '') === 'SMS') {
// Incoming SMS message
$from = $data['from'];
$message = $data['body'];
error_log("Incoming SMS from {$from}: {$message}");
// Optionally send a reply
// sendReply($from, "Thanks for your message!");
} elseif (($data['type'] ?? '') === 'dlr') {
// Delivery status report
$messageId = $data['messageId'];
$status = $data['status'];
if ($status === 'DELIVRD') {
error_log("Message {$messageId} delivered successfully");
} elseif ($status === 'UNDELIV') {
error_log("Message {$messageId} failed to deliver");
}
}
// Always return 200 OK
http_response_code(200);
echo json_encode(['success' => true]);
?>
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const data = req.body;
if (data.type === 'SMS') {
// Incoming SMS
console.log(`SMS from ${data.from}: ${data.body}`);
} else if (data.type === 'dlr') {
// Delivery report
console.log(`Message ${data.messageId}: ${data.status}`);
}
res.json({ success: true });
});
app.listen(3000);
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
data = request.json
if data.get('type') == 'SMS':
# Incoming SMS
print(f"SMS from {data['from']}: {data['body']}")
elif data.get('type') == 'dlr':
# Delivery report
print(f"Message {data['messageId']}: {data['status']}")
return jsonify({'success': True})
if __name__ == '__main__':
app.run(port=3000)
curl -X POST https://api.websms.co.nz/api/connexus/mo/query \
-H "Authorization: Bearer wst_your_token..." \
-d "from=6421234567" \
-d "to=551" \
-d "start_date=2025-01-01" \
-d "end_date=2025-01-31" \
-d "limit=100" \
-d "page=1"
curl -X POST https://api.websms.co.nz/api/connexus/mo/query \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword" \
-d "from=6421234567" \
-d "to=551" \
-d "start_date=2025-01-01" \
-d "end_date=2025-01-31" \
-d "limit=100" \
-d "page=1"
| Parameter | Required | Description | Example |
|---|---|---|---|
userId |
Required | Your WebSMS account email | user@domain.co.nz |
password |
Required | Your WebSMS account password | yourpassword |
from |
Optional | Filter by sender number | 6421234567 |
to |
Optional | Filter by recipient (shortcode) | 551 |
callback_status |
Optional | Filter by webhook callback HTTP status | 200 |
start_date |
Optional | Start of date range (Y-m-d or Y-m-d H:i:s) | 2025-01-01 |
end_date |
Optional | End of date range (Y-m-d or Y-m-d H:i:s) | 2025-01-31 |
limit |
Optional | Number of results per page (default: 100, max: 200) | 100 |
page |
Optional | Page number for pagination (default: 1) | 1 |
{
"status": "success",
"messages": [
{
"messageId": "1234",
"from": "6421234567",
"to": "551",
"body": "Reply message text",
"receivedTime": "2025-01-15 14:30:00",
"timestamp": 1736948400,
"relatedMessageId": "5678", // Optional: only if reply to sent message
"callbackUrl": "https://...", // Optional: only if webhook configured
"callbackStatus": "200", // Optional: only if webhook called
"callbackResponse": "OK" // Optional: only if webhook responded
}
],
"pagination": {
"page": 1,
"limit": 100,
"totalRecords": 250,
"totalPages": 3,
"hasNextPage": true,
"hasPreviousPage": false
},
"filters": {
"from": "6421234567",
"to": "551",
"callback_status": null,
"start_date": "2025-01-01 00:00:00",
"end_date": "2025-01-31 23:59:59"
}
}
relatedMessageId, callbackUrl, callbackStatus, and callbackResponse only appear in the response if they have values.
<?php
// Query incoming messages
$ch = curl_init('https://api.websms.co.nz/api/connexus/mo/query');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'userId' => 'user@domain.co.nz',
'password' => 'yourpassword',
'from' => '6421234567',
'start_date' => '2025-01-01',
'end_date' => '2025-01-31',
'limit' => 100,
'page' => 1
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$data = json_decode($response, true);
echo "Total messages: " . $data['pagination']['totalRecords'] . "\n";
foreach ($data['messages'] as $msg) {
echo "From: {$msg['from']}, Message: {$msg['body']}\n";
}
// Fetch next page if available
if ($data['pagination']['hasNextPage']) {
// Query with page=2
}
} else {
echo "Error: HTTP $httpCode";
}
?>
Three ways to query. Looking up by message ID works for messages of any age; browsing with filters is limited to the current calendar month.
curl -X POST https://api.websms.co.nz/api/connexus/sms/status/query \
-H "Authorization: Bearer wst_your_token..." \
-d "message_id=1188236,1188240"
curl -X POST https://api.websms.co.nz/api/connexus/sms/status/query \
-H "Authorization: Bearer wst_your_token..." \
-d "messageId=PRAC-042-7731"
curl -X POST https://api.websms.co.nz/api/connexus/sms/status/query \
-H "Authorization: Bearer wst_your_token..." \
-d "status=UNDELIV" \
-d "start_date=2026-08-01 09:00:00" \
-d "end_date=2026-08-30" \
-d "limit=50" \
-d "page=1"
Optional filters: to (recipient number), status, start_date/end_date (date or date-time, current calendar month only). Max 100 results per page.
{
"status": "success",
"messages": [
{
"message_id": "1188236",
"to": "6421234567",
"from": "2190",
"status": "DELIVRD",
"statusCode": 1,
"sentTime": "2026-08-30 10:15:22",
"statusTime": "2026-08-30 10:15:31",
"customerMessageId": "PRAC-042-7731"
}
],
"notFound": []
}
Note: status uses the same values as the delivery report webhook (see the status codes table above). PENDING with statusCode: null means no delivery report has been received yet. customerMessageId, rateCode, statusTime and blockedReason only appear when they have values. Username/password authentication (userId/password) is also supported.
If you send on behalf of your own customers, tag each send with a rateCode (e.g. your customer number) and use this endpoint at the end of the month to see exactly what each rateCode was billed. Reports actual billed transactions, so multi-part messages are counted in segments and the amounts always reconcile with your WebSMS account balance.
curl -X POST https://api.websms.co.nz/api/connexus/billing/query \
-H "Authorization: Bearer wst_your_token..." \
-d "month=2026-08"
Optional parameters: month (Y-m, defaults to the current month) or start_date/end_date (max 366 days); rateCode to report a single code — a base code also matches its -OTP/-APPT variants, so filtering on your customer number returns that customer's full spend (use untagged for messages sent without one); detail=1 for individual transactions with limit/page pagination (max 500 per page).
{
"status": "success",
"rateCodes": [
{
"rateCode": "CUST-0042",
"sendType": "sms",
"messages": 1250,
"segments": 1311,
"amount": 91.77,
"gst": 13.7655,
"totalInclGst": 105.5355
},
{
"rateCode": "CUST-0042-OTP",
"sendType": "otp",
"baseRateCode": "CUST-0042",
"messages": 340,
"segments": 340,
"amount": 27.20,
"gst": 4.08,
"totalInclGst": 31.28
},
{
"rateCode": "CUST-0042-APPT",
"sendType": "appointment",
"baseRateCode": "CUST-0042",
"messages": 95,
"segments": 102,
"amount": 8.16,
"gst": 1.224,
"totalInclGst": 9.384
},
{
"rateCode": "untagged",
"sendType": "sms",
"messages": 18,
"segments": 18,
"amount": 1.26,
"gst": 0.189,
"totalInclGst": 1.449
}
],
"totals": {
"messages": 1703,
"segments": 1771,
"amount": 128.39,
"gst": 19.2585,
"totalInclGst": 147.6485
},
"filters": {
"rateCode": null,
"start_date": "2026-08-01 00:00:00",
"end_date": "2026-08-31 23:59:59"
}
}
Note: amounts are in NZD exclusive of GST, with GST reported separately (zero for non-NZ accounts). OTP and appointment sends are billed at their own rates, so they always report separately: with a rateCode supplied they appear as {rateCode}-OTP / {rateCode}-APPT (giving you up to three lines per customer), otherwise under the fixed codes OTP and APPT. Messages blocked before carrier submission are billed and appear under their rateCode. Billing detail is recorded from August 2026 onwards. Username/password authentication (userId/password) is also supported.
The same list as Members Area » Blocked Numbers. Use since to poll for changes only — a repeat STOP refreshes blockedAt, and with include=all resubscribed numbers come back with active: false so you can unblock them on your side too.
curl -X POST https://api.websms.co.nz/api/connexus/unsubscribes/query \
-H "Authorization: Bearer wst_your_token..." \
-d "since=2026-08-30 00:00:00" \
-d "include=all"
Omit since for the full current list. Optional: limit (default 100, max 200), page. Username/password auth also supported.
{
"status": "success",
"numbers": [
{
"number": "6421234567",
"keyword": "STOP",
"blockedAt": "2026-08-30 14:22:10",
"active": true,
"shortcode": "2190"
},
{
"number": "6427654321",
"keyword": "STOP",
"blockedAt": "2026-08-27 09:15:02",
"active": false,
"resubscribedAt": "2026-08-30 09:01:44"
}
],
"pagination": { "page": 1, "limit": 100, "totalRecords": 2, "totalPages": 1 }
}
curl -X POST https://api.websms.co.nz/api/connexus/sms/balance \
-H "Authorization: Bearer wst_your_token..."
curl -X POST https://api.websms.co.nz/api/connexus/sms/balance \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword"
{
"balance": "125.50",
"currency": "NZD"
}
<?php
// Check account balance
$ch = curl_init('https://api.websms.co.nz/api/connexus/sms/balance');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'userId' => 'user@domain.co.nz',
'password' => 'yourpassword'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$data = json_decode($response, true);
echo "Current balance: $" . $data['balance'] . " " . $data['currency'];
} else {
echo "Error: HTTP $httpCode";
}
?>
curl -X POST https://api.websms.co.nz/api/connexus/sms/otp \
-H "Authorization: Bearer wst_your_token..." \
-d "to=6421234567" \
-d "msgCompany=MyApp"
curl -X POST https://api.websms.co.nz/api/connexus/sms/otp \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword" \
-d "to=6421234567" \
-d "msgCompany=MyApp"
| Parameter | Required | Description | Example |
|---|---|---|---|
userId |
Required | Your WebSMS account email | user@domain.co.nz |
password |
Required | Your WebSMS account password | yourpassword |
to |
Required | Recipient phone number | 6421234567 |
msgCompany |
Required | Your company/app name (shown in message) | MyApp |
msgCode |
Optional | Custom 4-8 digit code (auto-generated if not provided) | 426817 |
msgComment |
Optional | Additional text appended to message (truncated to fit 160 chars) | Valid for 5 minutes. |
from |
Optional | Sender ID (default: 552) | 552 |
webOtpDomain |
Optional | Bare hostname (example.com or app.example.com) to append as a WebOTP API autofill marker on the SMS's last line (format: @host #code). Lets Chrome/Safari offer one-tap autofill into a matching <input autocomplete="one-time-code"> on your origin. Schemes/paths/spaces are rejected - pass the host only. |
app.example.com |
rateCode |
Optional | Billing tag (e.g. your own customer number, max 95 chars). Reported in the billing report as {rateCode}-OTP; sends without one report under the fixed code OTP. |
CUST-0042 |
sandbox |
Optional | If true, returns a demo response without sending or billing. Useful for integration testing. |
true |
{msgCode} is your {msgCompany} verification code{msgComment}
[\n@{webOtpDomain} #{msgCode}] ← only if webOtpDomain is supplied
Examples:
- "426817 is your MyApp verification code"
- "426817 is your MyApp verification code. Valid for 5 minutes."
- "426817 is your MyApp verification code. Valid for 5 minutes.
@example.com #426817"
{
"status": "success",
"message_id": "abc123def456",
"to": "6421234567",
"from": "552",
"code": "426817", // The OTP code that was sent (store this to verify later!)
"parts": 1
}
code field is always returned in the response. You must store this code on your server to verify the user's input later. The code is not stored by WebSMS.
<?php
// Send OTP code
function sendOTP($phone, $company) {
$ch = curl_init('https://api.websms.co.nz/api/connexus/sms/otp');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'userId' => 'user@domain.co.nz',
'password' => 'yourpassword',
'to' => $phone,
'msgCompany' => $company,
'msgComment' => 'Valid for 5 minutes.'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
// Store the code for verification
$_SESSION['otp_code'] = $data['code'];
$_SESSION['otp_expires'] = time() + 300; // 5 minutes
return $data;
}
// Verify OTP code
function verifyOTP($userInput) {
if (time() > $_SESSION['otp_expires']) {
return false; // Expired
}
return $userInput === $_SESSION['otp_code'];
}
// Usage
$result = sendOTP('6421234567', 'MyApp');
echo "OTP sent! Code: " . $result['code'];
?>
const axios = require('axios');
// In-memory store (use Redis/DB in production)
const otpStore = new Map();
async function sendOTP(phone, company) {
const response = await axios.post('https://api.websms.co.nz/api/connexus/sms/otp', {
userId: 'user@domain.co.nz',
password: 'yourpassword',
to: phone,
msgCompany: company,
msgComment: 'Valid for 5 minutes.'
});
// Store code for verification
otpStore.set(phone, {
code: response.data.code,
expires: Date.now() + 300000 // 5 minutes
});
return response.data;
}
function verifyOTP(phone, userInput) {
const stored = otpStore.get(phone);
if (!stored || Date.now() > stored.expires) return false;
if (userInput === stored.code) {
otpStore.delete(phone);
return true;
}
return false;
}
// Usage
const result = await sendOTP('6421234567', 'MyApp');
console.log('OTP sent! Code:', result.code);
import requests
import time
# In-memory store (use Redis/DB in production)
otp_store = {}
def send_otp(phone, company):
response = requests.post('https://api.websms.co.nz/api/connexus/sms/otp', data={
'userId': 'user@domain.co.nz',
'password': 'yourpassword',
'to': phone,
'msgCompany': company,
'msgComment': 'Valid for 5 minutes.'
})
data = response.json()
# Store code for verification
otp_store[phone] = {
'code': data['code'],
'expires': time.time() + 300 # 5 minutes
}
return data
def verify_otp(phone, user_input):
stored = otp_store.get(phone)
if not stored or time.time() > stored['expires']:
return False
if user_input == stored['code']:
del otp_store[phone]
return True
return False
# Usage
result = send_otp('6421234567', 'MyApp')
print(f"OTP sent! Code: {result['code']}")
using System.Net.Http;
using System.Text.Json;
public class OtpService
{
private static Dictionary<string, (string code, DateTime expires)> _store = new();
private readonly HttpClient _http = new();
public async Task<string> SendOtpAsync(string phone, string company)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("userId", "user@domain.co.nz"),
new KeyValuePair<string, string>("password", "yourpassword"),
new KeyValuePair<string, string>("to", phone),
new KeyValuePair<string, string>("msgCompany", company),
new KeyValuePair<string, string>("msgComment", "Valid for 5 minutes.")
});
var response = await _http.PostAsync(
"https://api.websms.co.nz/api/connexus/sms/otp", content);
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(json);
var code = data.GetProperty("code").GetString();
// Store code for verification
_store[phone] = (code, DateTime.UtcNow.AddMinutes(5));
return code;
}
public bool VerifyOtp(string phone, string userInput)
{
if (!_store.TryGetValue(phone, out var stored)) return false;
if (DateTime.UtcNow > stored.expires) return false;
if (userInput == stored.code)
{
_store.Remove(phone);
return true;
}
return false;
}
}
// Usage
var otp = new OtpService();
var code = await otp.SendOtpAsync("6421234567", "MyApp");
Console.WriteLine($"OTP sent! Code: {code}");
curl -X POST https://api.websms.co.nz/api/connexus/sms/appointment \
-H "Authorization: Bearer wst_your_token..." \
-d "to=6421234567" \
-d "msgCompany=Sunrise Wellness Clinic" \
-d "name=Barry" \
-d "date=28/10/25" \
-d "time=11:15am" \
-d "replyY=true" \
-d "callUs=09-555-1234" \
-d "address=42 Main Street, Auckland"
curl -X POST https://api.websms.co.nz/api/connexus/sms/appointment \
-H "Authorization: Bearer wst_your_token..." \
-d "to=6421234567" \
-d "msgCompany=Sunrise Wellness Clinic"
| Parameter | Required | Description | Example |
|---|---|---|---|
to |
Required | Recipient phone number | 6421234567 |
msgCompany |
Required | Your business/company name | Sunrise Wellness Clinic |
name |
Optional | Customer's first name (adds "Hi {name}, " prefix) | Barry |
date |
Optional | Appointment date | 28/10/25 |
time |
Optional | Appointment time | 11:15am |
replyY |
Optional | If true, adds "Reply Y to confirm" |
true |
callUs |
Optional | Phone number to call for rebooking | 09-555-1234 |
address |
Optional | Business address | 42 Main Street, Auckland |
from |
Optional | Sender ID (default: your account default) | 552 |
rateCode |
Optional | Billing tag (e.g. your own customer number, max 95 chars). Reported in the billing report as {rateCode}-APPT; sends without one report under the fixed code APPT. |
CUST-0042 |
sandbox |
Optional | If true, returns a demo response without sending or billing. Useful for integration testing. |
true |
// All fields provided:
Hi Barry, your next appointment with Sunrise Wellness Clinic is on 28/10/25 at 11:15am. Reply Y to confirm or Pls call us on 09-555-1234 to re-book. Our Address: 42 Main Street, Auckland. Std SMS charges apply.
// Without name:
Your next appointment with Sunrise Wellness Clinic is on 28/10/25 at 11:15am. Reply Y to confirm or Pls call us on 09-555-1234 to re-book. Our Address: 42 Main Street, Auckland. Std SMS charges apply.
// Without date/time:
Hi Barry, your next appointment with Sunrise Wellness Clinic is coming up. Reply Y to confirm or Pls call us on 09-555-1234 to re-book. Std SMS charges apply.
// Minimal (company only):
Your next appointment with Sunrise Wellness Clinic is coming up. Std SMS charges apply.
// Without replyY:
Hi Barry, your next appointment with Sunrise Wellness Clinic is on 28/10/25 at 11:15am. Pls call us on 09-555-1234 to re-book. Our Address: 42 Main Street, Auckland. Std SMS charges apply.
{
"status": "success",
"message_id": "abc123def456",
"to": "6421234567",
"from": "552",
"message": "Hi Barry, your next appointment with Sunrise Wellness Clinic is on 28/10/25 at 11:15am. Reply Y to confirm or Pls call us on 09-555-1234 to re-book. Our Address: 42 Main Street, Auckland. Std SMS charges apply.",
"parts": 2
}
<?php
function sendAppointmentReminder($phone, $company, $options = []) {
$params = [
'to' => $phone,
'msgCompany' => $company
];
// Add optional fields if provided
if (!empty($options['name'])) $params['name'] = $options['name'];
if (!empty($options['date'])) $params['date'] = $options['date'];
if (!empty($options['time'])) $params['time'] = $options['time'];
if (!empty($options['replyY'])) $params['replyY'] = 'true';
if (!empty($options['callUs'])) $params['callUs'] = $options['callUs'];
if (!empty($options['address'])) $params['address'] = $options['address'];
$ch = curl_init('https://api.websms.co.nz/api/connexus/sms/appointment');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer wst_your_token...']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return json_decode($response, true);
}
// Usage examples:
// Full appointment reminder
$result = sendAppointmentReminder('6421234567', 'Sunrise Wellness Clinic', [
'name' => 'Barry',
'date' => '28/10/25',
'time' => '11:15am',
'replyY' => true,
'callUs' => '09-555-1234',
'address' => '42 Main Street, Auckland'
]);
// Minimal reminder
$result = sendAppointmentReminder('6421234567', 'Sunrise Wellness Clinic');
// With name and date only
$result = sendAppointmentReminder('6421234567', 'Sunrise Wellness Clinic', [
'name' => 'Barry',
'date' => '28/10/25',
'time' => '11:15am'
]);
?>
curl -X POST https://api.websms.co.nz/api/connexus/number/lookup \
-H "Authorization: Bearer wst_your_token..." \
-d "number=0211234567"
curl -X POST https://api.websms.co.nz/api/connexus/number/lookup \
-d "userId=user@domain.co.nz" \
-d "password=yourpassword" \
-d "number=0211234567"
| Parameter | Required | Description | Example |
|---|---|---|---|
userId |
Required | Your WebSMS account email | user@domain.co.nz |
password |
Required | Your WebSMS account password | yourpassword |
number |
Required | NZ mobile number (local 02x or international 642x format) | 0211234567 or 6421234567 |
{
"success": true,
"number": "6421234567",
"carrier": "Spark",
"ported": false,
"original_network": "Spark",
"current_network": "Spark",
"network_code": "TCNZ"
}
// Number too short
{
"success": false,
"error": "Invalid number",
"message": "Number too short. NZ mobile numbers must be 9-11 digits (e.g., 021234567)."
}
// Number too long
{
"success": false,
"error": "Invalid number",
"message": "Number too long. NZ mobile numbers must be 9-11 digits (e.g., 021234567)."
}
// Invalid prefix
{
"success": false,
"error": "Invalid number",
"message": "Invalid prefix. NZ mobile numbers must start with 02X (e.g., 021, 022, 027)."
}
// Invalid format
{
"success": false,
"error": "Invalid number",
"message": "Invalid format. NZ mobile numbers must start with 02X (local) or 642X (international)."
}
| Field | Description |
|---|---|
success | Whether the lookup was successful |
number | Normalized phone number (international format) |
carrier | Current carrier name (Spark, One NZ, 2degrees, Skinny, etc.) |
ported | Whether the number has been ported from its original network |
original_network | Original network based on number prefix |
current_network | Current network servicing the number |
network_code | Network operator code (TCNZ, VNZL, TNZD, etc.) |
<?php
// Lookup a phone number
$ch = curl_init('https://api.websms.co.nz/api/connexus/number/lookup');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'userId' => 'user@domain.co.nz',
'password' => 'yourpassword',
'number' => '0211234567'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$data = json_decode($response, true);
if ($data['success']) {
echo "Number: " . $data['number'] . "\n";
echo "Carrier: " . $data['carrier'] . "\n";
echo "Ported: " . ($data['ported'] ? 'Yes' : 'No') . "\n";
}
} else {
echo "Error: HTTP $httpCode";
}
?>
# Get current configuration
curl -X GET https://api.websms.co.nz/api/connexus/configure \
-H "Authorization: Bearer wst_your_token..."
# Set webhook URL (receives both incoming SMS and delivery reports)
curl -X POST https://api.websms.co.nz/api/connexus/configure \
-H "Authorization: Bearer wst_your_token..." \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://yoursite.com/webhook"
}'
# Get current configuration
curl -X GET https://api.websms.co.nz/api/connexus/configure \
-u "user@domain.co.nz:password"
# Set webhook URL (receives both incoming SMS and delivery reports)
curl -X POST https://api.websms.co.nz/api/connexus/configure \
-u "user@domain.co.nz:password" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://yoursite.com/webhook"
}'
type field: "SMS" for incoming SMS, "dlr" for delivery reports.
See the Webhooks section for full payload details and examples.
<?php
// Step 1: Get access token
$ch = curl_init('https://api.websms.co.nz/api/connexus/auth/token');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'client_id' => 'cid_your_client_id',
'client_secret' => 'csk_your_client_secret'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$tokenResponse = json_decode(curl_exec($ch), true);
curl_close($ch);
$accessToken = $tokenResponse['access_token'];
// Step 2: Send SMS with token
$ch = curl_init('https://api.websms.co.nz/api/connexus/sms/out');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'to' => '6421234567',
'body' => 'Hello from WebSMS!'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $httpCode == 200 ? "Sent!" : "Error: $httpCode";
?>
<?php
$url = 'https://api.websms.co.nz/api/connexus/sms/out';
$data = [
'userId' => 'user@domain.co.nz',
'password' => 'yourpassword',
'to' => '6421234567',
'body' => 'Hello from WebSMS!'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 204) {
echo "Message sent successfully!";
} else {
echo "Error: HTTP $httpCode";
}
?>
import requests
# Step 1: Get access token
token_response = requests.post(
'https://api.websms.co.nz/api/connexus/auth/token',
data={
'client_id': 'cid_your_client_id',
'client_secret': 'csk_your_client_secret'
}
)
access_token = token_response.json()['access_token']
# Step 2: Send SMS with token
response = requests.post(
'https://api.websms.co.nz/api/connexus/sms/out',
headers={'Authorization': f'Bearer {access_token}'},
data={'to': '6421234567', 'body': 'Hello from WebSMS!'}
)
print("Sent!" if response.status_code == 200 else f"Error: {response.status_code}")
import requests
url = 'https://api.websms.co.nz/api/connexus/sms/out'
data = {
'userId': 'user@domain.co.nz',
'password': 'yourpassword',
'to': '6421234567',
'body': 'Hello from WebSMS!'
}
response = requests.post(url, data=data)
if response.status_code == 204:
print("Message sent successfully!")
else:
print(f"Error: HTTP {response.status_code}")
const axios = require('axios');
async function sendSMS() {
// Step 1: Get access token
const tokenRes = await axios.post(
'https://api.websms.co.nz/api/connexus/auth/token',
new URLSearchParams({
client_id: 'cid_your_client_id',
client_secret: 'csk_your_client_secret'
})
);
// Step 2: Send SMS with token
const smsRes = await axios.post(
'https://api.websms.co.nz/api/connexus/sms/out',
new URLSearchParams({ to: '6421234567', body: 'Hello!' }),
{ headers: { Authorization: `Bearer ${tokenRes.data.access_token}` }}
);
console.log('Sent!');
}
sendSMS().catch(err => console.error('Error:', err.response?.status));
const axios = require('axios');
const data = new URLSearchParams({
userId: 'user@domain.co.nz',
password: 'yourpassword',
to: '6421234567',
body: 'Hello from WebSMS!'
});
axios.post('https://api.websms.co.nz/api/connexus/sms/out', data)
.then(response => {
if (response.status === 204) {
console.log('Message sent successfully!');
}
})
.catch(error => {
console.error('Error:', error.response?.status);
});
using System.Net.Http;
using System.Text.Json;
var client = new HttpClient();
// Step 1: Get access token
var tokenContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", "cid_your_client_id" },
{ "client_secret", "csk_your_client_secret" }
});
var tokenRes = await client.PostAsync(
"https://api.websms.co.nz/api/connexus/auth/token", tokenContent);
var tokenJson = JsonDocument.Parse(await tokenRes.Content.ReadAsStringAsync());
var accessToken = tokenJson.RootElement.GetProperty("access_token").GetString();
// Step 2: Send SMS with token
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
var smsContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "to", "6421234567" },
{ "body", "Hello from WebSMS!" }
});
var smsRes = await client.PostAsync(
"https://api.websms.co.nz/api/connexus/sms/out", smsContent);
Console.WriteLine(smsRes.IsSuccessStatusCode ? "Sent!" : $"Error: {smsRes.StatusCode}");
using System;
using System.Net.Http;
using System.Collections.Generic;
var client = new HttpClient();
var values = new Dictionary<string, string>
{
{ "userId", "user@domain.co.nz" },
{ "password", "yourpassword" },
{ "to", "6421234567" },
{ "body", "Hello from WebSMS!" }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync(
"https://api.websms.co.nz/api/connexus/sms/out",
content
);
if ((int)response.StatusCode == 204)
{
Console.WriteLine("Message sent successfully!");
}
else
{
Console.WriteLine($"Error: {response.StatusCode}");
}
Our team is here to help with your integration:
Email: support@websms.co.nz
Phone: +64 27 4909-712
Hours: Monday-Friday, 9am-5pm NZST
If you're moving from Bulletin Connect, switching to WebSMS is straightforward. Our Connexus API is fully compatible - just update three things:
service.bulletinconnect.net
websms.co.nz
| Function | Bulletin Connect | WebSMS |
|---|---|---|
| Send SMS | http://service.bulletinconnect.net/api/1/sms/out |
https://api.websms.co.nz/api/connexus/sms/out |
| Receive SMS | http://service.bulletinconnect.net/api/1/sms/in |
https://api.websms.co.nz/api/connexus/sms/in |
| Status Updates | http://service.bulletinconnect.net/api/1/sms/status |
https://api.websms.co.nz/api/connexus/sms/status |
Need help with migration? Contact us at support@websms.co.nz - we offer free migration assistance.