Start a free trial of 10DLC SMS
Bandwidth Build lets you send and receive SMS between your Build phone number and your verified mobile number during your free trial. Test your full messaging flow, including outbound sends, inbound replies, and delivery callbacks, before you commit to anything.
This guide walks you through sending a message, handling replies, and moving to a full Bandwidth account when you're ready to text your customers.
What you can do on Build today
Messaging on Build is free trial only. Here's what that means:
- You can text yourself. Send messages from your Build number to the mobile number you verified at sign-up, and reply from that phone back to your Build number.
- You can't text anyone else yet. Messages to any number other than your verified mobile number will be rejected.
- You're already on 10DLC. Your Build number is hosted on Bandwidth's Platform Free Trial 10DLC campaign, so you can start sending without registering a brand or campaign yourself.
- Your trial lasts 30 days. Messaging runs on the same free trial credits as voice, SIP Trunking, and everything else in Build.
- SMS only, US and Canada. You can send to US and Canadian (
+1) numbers. MMS (picture and media messages) isn't available on Build yet.
Ready to text real customers? Upgrade to the full Bandwidth App.
Coming soon: registering your own 10DLC brand and campaign on Build.
Before you start
You'll need:
- A Bandwidth Build account (and your account ID)
- A Build phone number with messaging enabled
- A messaging application ID, which tells Bandwidth where to send your callbacks
- Your API credentials for bearer auth
- A public HTTPS endpoint to receive callbacks (a tunneling tool like ngrok will work fine for this)
Send a message
Send a POST request to the Build messaging endpoint:
POST https://api.bandwidth.com/v1/build/accounts/{accountId}/messages
Request body
| Field | Type | Required | Description |
|---|---|---|---|
applicationId | string | Yes | The messaging application ID linked to your Build number |
to | array of strings | Yes | The recipient in E.164 format. On Build, this must be your verified mobile number |
from | string | Yes | Your Build phone number in E.164 format |
text | string | Yes | The message content |
tag | string | No | Any custom string, up to 1,024 characters. Bandwidth returns it in every callback for this message |
priority | string | No | default or high |
Free trial message rules
Trial accounts have a few extra constraints the full Messaging API doesn't apply:
- Every message gets a prefix. Bandwidth prepends
Bandwidth Free Trial Message -(31 characters) to your text. - One segment only. Your text plus the prefix has to fit a single 160-character SMS segment, so keep your own text to 129 characters or fewer.
- Plain ASCII only. Curly quotes, en dashes, and emoji are rejected. Use
-instead of–. - One recipient.
toaccepts an array, but on a trial it has to hold exactly your verified mobile number. - No media. Including a
mediafield rejects the request. - Send rate. Up to 6 messages per minute and 200 per day.
If your credits run out, the API returns 402 Payment Required.
Sample request
- cURL
- Python
curl -X POST "https://api.bandwidth.com/v1/build/accounts/9900123/messages" \
-H "Authorization: Bearer $BW_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"applicationId": "93de2206-9669-4e07-948d-329f4b722ee2",
"to": ["+19195559876"],
"from": "+19195551234",
"text": "Hi from Bandwidth Build! Your appointment is confirmed for Tuesday at 2 p.m. Reply YES to confirm or NO to reschedule.",
"tag": "appointment-reminder-test"
}'
import os
import requests
ACCOUNT_ID = "9900123"
URL = f"https://api.bandwidth.com/v1/build/accounts/{ACCOUNT_ID}/messages"
payload = {
"applicationId": "93de2206-9669-4e07-948d-329f4b722ee2",
"to": ["+19195559876"],
"from": "+19195551234",
"text": "Your verification code is 482913. It expires in 10 minutes.",
"tag": "otp-test",
}
response = requests.post(
URL,
json=payload,
headers={"Authorization": f"Bearer {os.environ['BW_ACCESS_TOKEN']}"},
)
print(response.status_code)
print(response.json())
Sample response
A successful request returns 202 Accepted. That means Bandwidth accepted the message for delivery. Watch your callbacks to confirm it reached the handset.
Note that text comes back with the free trial prefix already applied.
{
"id": "1760281200000xk4m2je7j7jklbn2",
"owner": "+19195551234",
"applicationId": "93de2206-9669-4e07-948d-329f4b722ee2",
"time": "2026-10-12T15:00:00.000Z",
"segmentCount": 1,
"direction": "out",
"to": ["+19195559876"],
"from": "+19195551234",
"text": "Bandwidth Free Trial Message - Hi from Bandwidth Build! Your appointment is confirmed for Tuesday at 2 p.m. Reply YES to confirm or NO to reschedule.",
"tag": "appointment-reminder-test",
"priority": "default"
}
Save the id. Every callback for this message references it.
Handle callbacks
Bandwidth sends callbacks to the URL configured on your messaging application. Each callback is a JSON array, so loop through it rather than reading a single object. Respond with a 2xx status to acknowledge receipt.
Bandwidth doesn't store message content, so text is empty on outbound delivery and failure callbacks. Log your own copy if you need it. See Webhooks for the full field reference.
Message delivered
The handset received your message.
[
{
"type": "message-delivered",
"time": "2026-10-12T15:00:02.418Z",
"description": "ok",
"to": "+19195559876",
"message": {
"id": "1760281200000xk4m2je7j7jklbn2",
"owner": "+19195551234",
"applicationId": "93de2206-9669-4e07-948d-329f4b722ee2",
"time": "2026-10-12T15:00:00.000Z",
"segmentCount": 1,
"direction": "out",
"to": ["+19195559876"],
"from": "+19195551234",
"text": "",
"tag": "appointment-reminder-test"
}
}
]
Message failed
The message couldn't be delivered. Check errorCode and description for the reason, and look them up in the messaging error codes.
[
{
"type": "message-failed",
"time": "2026-10-12T15:00:05.102Z",
"description": "Delivery receipt expired",
"to": "+19195559876",
"errorCode": 9902,
"message": {
"id": "1760281200000xk4m2je7j7jklbn2",
"owner": "+19195551234",
"applicationId": "93de2206-9669-4e07-948d-329f4b722ee2",
"time": "2026-10-12T15:00:00.000Z",
"segmentCount": 1,
"direction": "out",
"to": ["+19195559876"],
"from": "+19195551234",
"text": "",
"tag": "appointment-reminder-test"
}
}
]
Message received
You replied from your phone to your Build number. This is how your app reads inbound messages.
[
{
"type": "message-received",
"time": "2026-10-12T15:01:47.330Z",
"description": "Incoming message received",
"to": "+19195551234",
"message": {
"id": "1760281307330ab7c1de4f5g6hijk",
"owner": "+19195551234",
"applicationId": "93de2206-9669-4e07-948d-329f4b722ee2",
"time": "2026-10-12T15:01:47.330Z",
"segmentCount": 1,
"direction": "in",
"to": ["+19195551234"],
"from": "+19195559876",
"text": "YES"
}
}
]
Sample callback handler: Python (Flask)
from flask import Flask, request
app = Flask(__name__)
@app.post("/callbacks/messaging")
def messaging_callback():
for event in request.get_json():
msg = event["message"]
if event["type"] == "message-received":
print(f"Reply from {msg['from']}: {msg['text']}")
elif event["type"] == "message-delivered":
print(f"Delivered: {msg['id']}")
elif event["type"] == "message-failed":
print(f"Failed: {msg['id']} ({event['errorCode']}: {event['description']})")
return "", 204
Try a two-way conversation
Put it together and you've got a working back-and-forth in a few minutes:
- Send "Your appointment is Tuesday at 2 p.m. Reply YES to confirm or NO to reschedule." to your verified number.
- Watch for the
message-deliveredcallback. - Reply YES from your phone.
- Your handler receives a
message-receivedcallback with"text": "YES". - Send a follow-up: "Thanks, you're all set. See you Tuesday!"
A few more test messages worth trying:
| Use case | Sample text |
|---|---|
| One-time passcode | Your Acme verification code is 482913. It expires in 10 minutes. |
| Order update | Your order #10482 has shipped. Track it here: https://example.com/track/10482 |
| Voice AI follow-up | Thanks for calling Acme. Here's the summary of today's call: your refund of $42.18 will post in 3-5 business days. |
| Opt-out handling | Reply STOP from your phone and confirm your app handles it |
Save the STOP test for last. Once your verified number opts out, Bandwidth rejects further sends to it, which ends your testing. Reply START to opt back in.
Free trial limits
| Build free trial | Full Bandwidth account | |
|---|---|---|
| Who you can text | Your verified mobile number only | Anyone, once your 10DLC campaign is approved |
| Duration | 30 days | No limit |
| Message types | SMS | SMS and MMS |
| Coverage | US and Canada (+1) | US and international |
| Message text | Plain ASCII, prefixed, one 160-character segment | Up to 2,048 characters, no prefix, multi-segment |
| Send rate | 6 per minute, 200 per day | See rate limits |
| 10DLC registration | Bandwidth's shared trial campaign | Register your own brand and campaign |
If you send to a number other than your verified mobile number, the request is rejected.
Move to a full Bandwidth account
When you're ready to text customers, kick off an upgrade to the full Bandwidth App with one of our messaging experts.
Once you have a new account, updating your API integration is simple. Your request and response bodies stay exactly the same, and so does your callback handling. You'll just need to change your URL.
| URL | |
|---|---|
| Build | https://api.bandwidth.com/v1/build/accounts/{accountId}/messages |
| Full Bandwidth account | https://messaging.bandwidth.com/api/v2/users/{accountId}/messages |
Store the base URL in an environment variable from day one, and the switch becomes a config change instead of a code change.
import os
MESSAGING_URL = os.environ.get(
"BW_MESSAGING_URL",
"https://api.bandwidth.com/v1/build/accounts/{accountId}/messages",
)
After you upgrade to the full Bandwidth App:
- Register for 10DLC. Register your brand and campaign in the Bandwidth App. Carriers require this before you can send application-to-person (A2P) traffic on a local number.
- Assign your phone number to your campaign. Once your campaign is approved, link your number to it.
- Update your messaging URL. Point your app at the full Messaging API endpoint above.
- Send to anyone. The verified-number limit and the free trial message prefix no longer apply.
You also get access to MMS, toll-free messaging, and the rest of the Bandwidth Universal Platform.
Next steps
- Read the Send a Message guide for the full Messaging API request reference
- Explore the Bandwidth CLI to manage your account setup and try out Voice
- Add voice to your app with calling, IVR, and SIP Trunking on Build
- Not on Build yet? Get started at bandwidth.com/build-sign-up