Send SMS messages programmatically for just 3¢ each. Simple REST API, no SDK required. Get started in minutes.
The Texto SMS API is a REST API for sending SMS to Australian and New Zealand numbers. Authenticate with a bearer API key and POST to /send. HMAC-SHA256 signed webhooks are available for delivery receipts and inbound messages, with account management endpoints for SaaS platforms, agencies and multi-account hierarchies.
No SDK to install — most customers are integrating and sending within 5 minutes.
Sign up free and get 5 SMS credits to test with.
Go to the Developer page in-app and create a key.
Make a POST request to /send with your key.
curl -X POST https://api.texto.com.au/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+61412345678",
"message": "Hello from Texto!",
"sender": "MyBrand"
}'Built for Australian businesses and developers who need reliable, affordable SMS delivery.
No monthly fees, no setup charges, no minimum spend. Pay only for the messages you send — every SMS costs exactly 3 cents.
Messages are routed through direct Australian carrier connections for the fastest possible delivery. Most SMS arrive within one second.
All traffic stays on Australian carrier networks. No grey routes, no international re-routing — just reliable local delivery to every Australian mobile.
Texto is a clean REST API. Any language that can make an HTTP request works — Python, JavaScript, PHP, Go, cURL, even VBA in Excel macros.
Base URL: https://api.texto.com.au
Authentication
All requests require an Authorization: Bearer txt_... header with your API key.
Credit Calculation
1 credit = 1 message part. Messages ≤160 characters = 1 part. Longer messages split into 154-character parts.
Request Parameters
| Parameter | Required | Description |
|---|---|---|
| to | Yes | Recipient Australian mobile number (e.g. +61412345678) |
| message | Yes | SMS body text |
| sender | No | Specify a registered Sender ID (e.g. "MyBrand") or dedicated number (e.g. "+61400000000") to send from. If omitted, your account's default sending number will be used. |
| campaign | No | Optional campaign name (max 200 chars) that groups sends under a named campaign for reporting. Reuse the same name to roll up stats (recipient count + credits) across many API calls. Useful when sending on behalf of different clients, departments, or workflows. |
A multi-account SMS API built for SaaS platforms, agencies and resellers.
If you sell software or services to other businesses, SMS shouldn't mean onboarding forms, shared logins and manual top-ups. The Texto account management endpoints let you create a sub-account for every customer, issue them their own API key, fund them with credits, assign a dedicated Australian number, invite their users and pull usage reporting, all via the API. See account hierarchy for account structure details and see our pricing for volume rates as you scale.
Account hierarchy needs to be switched on for your parent account before these endpoints return data. and we'll enable it.
Accounts
inherit_parent_senders to true on a sub-account and it can send using your registered Sender IDs and dedicated numbers in the from field, on top of anything it holds itself. It is off by default, only the parent account can change it, and a sub-account can't switch it on for itself. An ACMA-registered Sender ID may only be used by the business (ABN) it was registered to — sharing it across locations, brands or departments of that same business is fine, but a sub-account that is a separate legal entity with its own ABN must register its own Sender ID.Credits
API keys
Users & access
Numbers
Webhook configuration
Reporting
| Status | Meaning |
|---|---|
| 400 | Bad request — invalid parameters or phone number |
| 401 | Unauthorized — invalid or revoked API key |
| 402 | Payment required — insufficient credits |
| 404 | Not found — resource doesn't exist |
| 500 | Internal server error |
/send-batch call.Download a full specification of the Texto SMS API in your preferred format. Import directly into Postman, Insomnia, Stoplight, Swagger UI, or generate a client SDK in minutes.
Industry standard. Works with Postman, Swagger UI, Stoplight, openapi-generator.
DownloadImport straight into Postman — all endpoints, auth, and sample bodies pre-configured.
DownloadMarkdown-based. Works with Apiary, Aglio, Dredd.
DownloadYAML-based design spec. Works with MuleSoft Anypoint, API Workbench.
DownloadConfigure an endpoint URL in your Texto dashboard and we'll POST a JSON event every time a delivery receipt is recorded for one of your messages. Signed with HMAC-SHA256, retried on failure, and idempotent by design.
Fired once per delivery receipt for any message on your account, including team-member sends.
<your endpoint URL>| Header | Value | Description |
|---|---|---|
| Content-Type | application/json | Body is always UTF-8 JSON. |
| X-Texto-Event | message.dlr | Event type. Currently always message.dlr. |
| X-Texto-Delivery | UUID | Unique per delivery. Same UUID is sent on retries — dedupe on this. |
| X-Texto-Signature | sha256=<hex> | HMAC-SHA256 of the raw body using your signing secret. Only sent when signing is enabled. |
Byte-identical to GET /message/:id on the REST API — same fields, same names, same types.
{
"message": {
"id": "8c1f9b2e-1a4c-4f87-9bd2-2d2f6f6f6f6f",
"recipient": "+61412345678",
"body": "Hi Sam, your appointment is confirmed for Tue 9am.",
"status": "delivered",
"sent_at": "2026-05-06T03:14:22.000Z"
},
"delivery_receipt": {
"status": "delivered",
"received_at": "2026-05-06T03:14:25.421Z"
}
}Always verify the signature before trusting the payload. Use the raw request bytes (not a re-serialised JSON string) and a constant-time comparison.
import crypto from "node:crypto";
app.post("/webhooks/texto", express.raw({ type: "application/json" }), (req, res) => {
const signatureHeader = req.header("x-texto-signature") || "";
const expected = crypto
.createHmac("sha256", process.env.TEXTO_WEBHOOK_SECRET)
.update(req.body) // raw bytes — not JSON.stringify(parsed)
.digest("hex");
const provided = signatureHeader.replace(/^sha256=/, "");
const ok =
provided.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(provided, "hex"), Buffer.from(expected, "hex"));
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// event.message, event.delivery_receipt …
res.sendStatus(200);
});Up to 3 attempts in total. After the third failure the delivery is dropped and shown as failed in your "Recent deliveries" log. Reply with any 2xx within 15 seconds to acknowledge.
| Attempt | When |
|---|---|
| 1 | Immediately when the delivery receipt is recorded |
| 2 | ~1 minute after attempt 1 fails |
| 3 | ~5 minutes after attempt 2 fails |
| — | After a further ~30 minutes the delivery is given up on |
Want every reply pushed to your server in real time? Configure an inbound webhook in your Texto dashboard and we'll POST a JSON event every time someone texts back to one of your numbers — an MO (Mobile Originated) message. STOP replies are included, with an is_optout flag so you can handle them separately. Signed with HMAC-SHA256, retried on failure, idempotent by message_id.
Fired once for every inbound message received on any of your numbers, including replies to team-member sends. Opt-out replies (STOP, UNSUBSCRIBE, etc.) still fire the webhook — is_optout tells you which they were, and the opt-out has already been recorded on your account.
<your endpoint URL>| Header | Value | Description |
|---|---|---|
| Content-Type | application/json | Body is always UTF-8 JSON. |
| X-Texto-Event | message.inbound | Event type. Currently always message.inbound. |
| X-Texto-Delivery | UUID | Unique per delivery attempt. Same UUID on retries — dedupe on this or on message_id. |
| X-Texto-Signature | sha256=<hex> | HMAC-SHA256 of the raw body using your signing secret. Only sent when signing is enabled. |
message_id is the stable identifier for the inbound message — use it as your idempotency key so duplicate deliveries (network hiccups, retries) don't double-process.
{
"event": "message.inbound",
"message_id": "8c1f9b2e-1a4c-4f87-9bd2-2d2f6f6f6f6f",
"from": "+61412345678",
"to": "+61480123456",
"body": "STOP",
"received_at": "2026-05-06T03:14:25.421Z",
"in_reply_to": "1d4e9b2e-1a4c-4f87-9bd2-2d2f6f6f6f6f",
"is_optout": true
}| Field | Type | Description |
|---|---|---|
| message_id | uuid | Stable ID of the inbound message. Use as your idempotency key. |
| from | string | The customer's phone number in E.164 format. |
| to | string | The Texto number that received the reply. |
| body | string | The message text as received. |
| received_at | ISO 8601 | When we received the message. |
| in_reply_to | uuid | null | If this looks like a reply to an outbound message, the ID of that message. |
| is_optout | boolean | True if we detected an opt-out keyword (STOP, UNSUBSCRIBE, etc.). We've already recorded the opt-out. |
Always verify the signature before trusting the payload. Use the raw request bytes (not a re-serialised JSON string) and a constant-time comparison.
import crypto from "node:crypto";
app.post("/webhooks/texto-inbound", express.raw({ type: "application/json" }), (req, res) => {
const signatureHeader = req.header("x-texto-signature") || "";
const expected = crypto
.createHmac("sha256", process.env.TEXTO_INBOUND_WEBHOOK_SECRET)
.update(req.body) // raw bytes — not JSON.stringify(parsed)
.digest("hex");
const provided = signatureHeader.replace(/^sha256=/, "");
const ok =
provided.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(provided, "hex"), Buffer.from(expected, "hex"));
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// Dedupe on event.message_id (stable across retries)
res.sendStatus(200);
});Up to 3 attempts in total — same schedule as delivery receipts. Reply with any 2xx within 15 seconds to acknowledge.
| Attempt | When |
|---|---|
| 1 | Immediately when the message is received |
| 2 | ~1 minute after attempt 1 fails |
| 3 | ~5 minutes after attempt 2 fails |
| — | After a further ~30 minutes the delivery is given up on |
Send your first SMS in any language.
curl -X POST https://api.texto.com.au/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+61412345678",
"message": "Hello from Texto!"
}'SMS is still the highest-engagement channel businesses have. Open rates sit around 98% — usually within three minutes of delivery — compared with about 20% for email. If you actually need someone to see a message, SMS is hard to beat.
An SMS API takes that channel and makes it programmable. Instead of logging into a dashboard, you fire a single HTTPS request from your app, CRM, automation tool, or AI agent, and the message goes out through a direct carrier route. The whole exchange usually completes in under a second.
That unlocks the use cases below — anything where timing, reliability and "did they actually see it" matter more than fancy formatting. 2FA codes, payment receipts, appointment reminders, delivery updates, outage alerts, marketing broadcasts, AI-driven workflows.
The job of a good SMS API is to get out of the way: plain REST, no SDK, predictable pricing, real delivery receipts, and opt-out handling that keeps you compliant without extra code. That's what Texto is built for — Australian carriers, Australian data, 3¢ a message, 24/7 chat if you ever need a human.
Deliver one-time passwords in under a second for sign-ins and verification.
Order confirmations, payment receipts, fraud alerts, status changes.
Cut no-shows in half — clinics, salons, trades, professional services.
Dispatch, on-the-way, and arrival pings with shortened tracking links.
Personalised broadcasts with merge fields, opt-out handling baked in.
Let AI agents send SMS via the API or our MCP server at mcp.texto.com.au.
A quick map of every capability and how to reach it. Everything is plain REST — no SDKs, no proprietary wire formats.
| Capability | Endpoint / mechanism | Notes |
|---|---|---|
| Send single SMS | POST /send | One-off transactional and conversational sends. |
| Batch send | POST /send-batch | Up to 1,000 recipients per call, merge fields supported. |
| Personalisation / merge fields | {{merge}} placeholders | Per-recipient values in /send-batch. |
| Delivery receipts (poll) | GET /message/:id | Status + timestamp for any message. |
| Delivery receipts (push) | Webhook (HMAC-SHA256 signed) | Real-time, retried, idempotent. |
| Inbound messages (poll) | GET /inbox | Two-way SMS with pagination and filters. |
| Inbound replies (push) | Webhook (HMAC-SHA256 signed) | Real-time MOs including opt-outs, retried, idempotent. |
| Opt-out management | GET /optouts | Auto STOP handling, queryable list. |
| One-tap opt-out link | {{OptOutLink}} merge field | Short texto.au link per recipient. Confirms opt-out on click. Ideal for Sender ID sends without inbound reply path. |
| Campaign tracking | GET /campaign/:id | Per-campaign rollup and message detail. |
| Balance check | GET /balance | Programmatic credit monitoring. |
| Sender ID / dedicated number | "sender" parameter | ACMA-registered alpha sender or your AU number. |
| Scheduled sending | In-app scheduler | Schedule from the dashboard or via the app. |
| AI agent integration | MCP server | https://mcp.texto.com.au — Claude, OpenAI tools, etc. |
| Language support | Plain REST | Any language with HTTP — no SDK required. |
| Sub-account provisioning | POST /accounts | Create an account per customer, with its own balance, opt-out list and sender ID. |
| Credit allocation | POST /account/:id/credits/allocate | Fund or recall customer credits from your parent balance. |
| API key provisioning | POST /account/:id/key | Issue and revoke keys on behalf of your customers. |
| Number assignment | POST /account/:id/numbers/assign | Give a customer a dedicated AU or NZ number, recall it later. |
| Group reporting | GET /report/group | Usage, credits and delivery rate for every account in one call. |
The boring stuff that actually matters when you put a phone number in front of your customers.
Your data stays onshore. No offshore failover, no surprise data transfers.
TLS 1.2+ for every request; AES-256 for stored data.
Message content is automatically deleted after 90 days. Spreadsheet uploads are discarded as soon as the campaign is queued.
Every delivery receipt is signed with your secret, retried on failure, and idempotent by design.
Bearer token auth. Rotate or revoke keys instantly from the Developer page — no downtime.
Telecommunications Industry Ombudsman member and ACMA-approved for SMS Sender ID registration.
Real humans on chat around the clock. No tier-1 ticket queue, no overseas overnight handoff.
Sending commercial SMS in Australia is governed by the Spam Act 2003 and the ACMA. The rules boil down to three things: consent, identification, and unsubscribe.
STOP.Texto handles the unsubscribe side automatically. STOP replies are processed in real time, the number is added to your account-wide opt-out list, and any future API send to that number is blocked before it hits the carrier — so you can't accidentally re-message someone who has opted out. You can query the full list at any time via GET /optouts.
For branded alphanumeric senders (e.g. "MyBrand"), Australia requires registration with the ACMA SMS Sender ID Register. Texto handles that registration for you for free as part of the service.
Easy to use, intuitive product and great value. So good to see a new business getting it right. Product, performance and price.
Switched from Sinch MessageMedia
Mark O.
Director NBS · Banking
Simple and Responsive. Their pricing is competitive and a great alternative to other established options.
Josh S.
Principal Consultant · Management Consulting
Sign up free, get 5 credits, and send your first message in under a minute.
Get Your Free API KeyHave questions? .