Bandwidth
Send SMS through the Bandwidth Messaging API v2 with the SMS SDK bandwidth() adapter, and receive delivery and inbound callbacks behind Basic authentication.
The bandwidth() adapter sends SMS through the Bandwidth Messaging API v2 and parses Bandwidth’s delivery and inbound callbacks. To compare it with other providers, see Choose a provider.
Support status
Partial. The adapter passes the shared contract tests, with these limits:
- SMS only.
mediaUrls,sendAt,validityPeriodSec, and a per-messagewebhookUrlthrowUnsupportedFieldErrorbefore any request. - Bandwidth does not sign callbacks. The webhook parser requires the Basic credentials you set on your Bandwidth application instead.
- Rejections are classified by HTTP status, and a 429 counts as a rate limit only when Bandwidth wrote the body; any other 429 is an unknown outcome. Bandwidth’s full error code list is not mapped, so most rejections are
requestand do not fall back. See Error codes.
The same notes are in adapter.support.notes at runtime.
Setup
npm install @opencoredev/sms-sdkpnpm add @opencoredev/sms-sdkyarn add @opencoredev/sms-sdkbun add @opencoredev/sms-sdknub add @opencoredev/sms-sdkaube add @opencoredev/sms-sdk| Variable | Required | Used for |
|---|---|---|
BANDWIDTH_USER_ID |
Yes | Your Bandwidth account ID, used in the API path. |
BANDWIDTH_API_TOKEN, BANDWIDTH_API_SECRET |
Yes | API credentials, sent as HTTP Basic auth. |
BANDWIDTH_APPLICATION_ID |
Yes | The messaging application linked to your sending number. |
BANDWIDTH_FROM |
No | Default sender. |
The adapter reads no environment variables itself. The examples and the doctor CLI use these names.
import { createSmsClient } from "@opencoredev/sms-sdk";
import { bandwidth } from "@opencoredev/sms-sdk/bandwidth";
const sms = createSmsClient({
adapters: [
bandwidth({
userId: process.env.BANDWIDTH_USER_ID ?? "",
apiToken: process.env.BANDWIDTH_API_TOKEN ?? "",
apiSecret: process.env.BANDWIDTH_API_SECRET ?? "",
applicationId: process.env.BANDWIDTH_APPLICATION_ID ?? "",
from: "+15550100001",
}),
],
});
const result = await sms.send({ to: "+14155550123", body: "Your order has shipped." });
console.log(result.providerId); // the Bandwidth message ID
bandwidth() throws ConfigurationError when any of the four credentials is empty. baseUrl (default https://messaging.bandwidth.com) and fetch are optional.
Senders
Bandwidth sends from a long code, a toll-free number, or a short code. Alphanumeric sender IDs and { messagingService } fail validation before any request. US long codes, toll-free numbers, and short codes need registration with carriers; see Registration and consent.
Features
| Feature | Support |
|---|---|
MMS (mediaUrls) |
No |
Scheduling (sendAt) |
No |
Validity period (validityPeriodSec) |
No |
Per-message webhookUrl |
No. Callbacks go to the URL on your Bandwidth application. |
| Text length | Up to 2,048 characters, checked before sending |
| Native idempotency | No. Use an idempotency store. |
Provider options
Pass Bandwidth-only fields under providerOptions.bandwidth:
// Loading the adapter module adds the typed providerOptions.bandwidth entry.
import "@opencoredev/sms-sdk/bandwidth";
import { sms } from "./sms";
await sms.send({
to: "+14155550123",
body: "Your code is 123456",
providerOptions: { bandwidth: { tag: "otp", priority: "high" } },
});
| Option | Wire field | Type |
|---|---|---|
tag |
tag |
string, up to 1,024 characters, returned in callbacks |
priority |
priority |
"default" | "high" |
Webhooks
Bandwidth posts delivery and inbound events to the callback URL on your messaging application. Set callback credentials on that application, then pass the same values to the parser. The credentials are required.
import { parseSmsWebhook, WebhookSignatureError } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";
export async function POST(request: Request): Promise<Response> {
try {
const event = await parseSmsWebhook({
provider: "bandwidth",
request,
credentials: {
username: process.env.BANDWIDTH_CALLBACK_USERNAME ?? "",
password: process.env.BANDWIDTH_CALLBACK_PASSWORD ?? "",
},
});
if (await db.webhookEvents.insertIfNew(event.dedupeKey)) {
console.log(event.type);
}
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof WebhookSignatureError) return new Response(null, { status: 401 });
throw error;
}
}
A request without a matching Authorization header throws WebhookSignatureError. The comparison runs in constant time. Bandwidth sends no signature and no timestamp, so there is no replay window: store event.dedupeKey and skip repeats.
| Bandwidth event | SMS SDK event |
|---|---|
message-received |
message.received, or recipient.opted_out / recipient.help for STOP and HELP keywords |
message-sending |
message.queued |
message-delivered |
message.delivered |
message-failed |
message.undelivered |
| anything else | unrecognized |
dedupeKey is bandwidth:{messageId}:{type}.
Limitations
- No MMS. Use Twilio, Telnyx, or Plivo.
- No scheduling or validity period. Use Telnyx or Sinch for scheduling, and Twilio, Plivo, Vonage, or Sinch for a validity period.
- No alphanumeric sender IDs.
- Callbacks carry no signature, so the Basic credentials and HTTPS are the only proof a request came from Bandwidth.
Error codes
| Bandwidth response | Category | Falls back |
|---|---|---|
| 401 or 403 | auth |
Yes |
| 404 | account |
Yes |
429 with a Bandwidth error body (JSON with type or description) |
rate_limited |
Yes, after retries |
| Any other 4xx | request |
No |
| 429 without a Bandwidth error body, such as a proxy page | unknown | Never |
| 5xx, network error, timeout, or 2xx without a message ID | unknown | Never |
error.provider.requestId holds the X-Request-Id or X-Correlation-Id header. See Fallback and retries for what each category does.