Sinch
Send and schedule SMS through the Sinch SMS API with the SMS SDK sinch() adapter, and receive delivery reports and inbound messages with Basic or HMAC callback authentication.
The sinch() adapter sends and schedules SMS through the Sinch SMS REST API and parses Sinch delivery reports and inbound messages. To compare it with other providers, see Choose a provider.
Support status
Partial. The adapter passes the shared contract tests, with these limits:
- No MMS.
mediaUrlsthrowsUnsupportedFieldErrorbefore any request. - Only a few rejections are classified (see Error codes). Everything else is
request, which never falls back. - Sinch’s SMS docs do not publish the HMAC callback format. Basic callback credentials are the documented option. See Webhooks.
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 |
|---|---|---|
SINCH_SERVICE_PLAN_ID |
Yes | Your service plan ID, used in the API path. |
SINCH_API_TOKEN |
Yes | API token, sent as a Bearer token. |
SINCH_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 { sinch } from "@opencoredev/sms-sdk/sinch";
const sms = createSmsClient({
adapters: [
sinch({
servicePlanId: process.env.SINCH_SERVICE_PLAN_ID ?? "",
apiToken: process.env.SINCH_API_TOKEN ?? "",
from: "+15550100001",
}),
],
});
const result = await sms.send({ to: "+14155550123", body: "Your order has shipped." });
console.log(result.providerId); // the Sinch batch ID
The adapter sends POST /xms/v1/{servicePlanId}/batches. providerId is the batch ID, and delivery reports refer to the same ID. sinch() throws ConfigurationError when servicePlanId or apiToken is empty. baseUrl (default https://sms.api.sinch.com) and fetch are optional.
Senders
Sinch sends from a long code, a toll-free number, a short code, or an alphanumeric sender ID ({ senderId: "Acme" }). { messagingService } is not supported. Which senders your account may use depends on the destination country; see Registration and consent.
Features
| Feature | Support |
|---|---|
MMS (mediaUrls) |
No |
Scheduling (sendAt) |
Yes, sent as send_at |
Validity period (validityPeriodSec) |
1 to 2,592,000 seconds, sent as expire_at counted from the moment of the request |
Per-message webhookUrl |
Yes, sent as callback_url |
| Empty body | Not allowed |
| Native idempotency | No. Use an idempotency store. |
Provider options
Pass Sinch-only fields under providerOptions.sinch:
// Loading the adapter module adds the typed providerOptions.sinch entry.
import "@opencoredev/sms-sdk/sinch";
import { sms } from "./sms";
await sms.send({
to: "+14155550123",
body: "Your order has shipped.",
providerOptions: { sinch: { deliveryReport: "per_recipient", clientReference: "order-123" } },
});
| Option | Wire field | Type |
|---|---|---|
deliveryReport |
delivery_report |
"none" | "per_recipient" | "per_recipient_final" | "summary" | "full" |
clientReference |
client_reference |
string, up to 128 characters |
maxNumberOfMessageParts |
max_number_of_message_parts |
integer, 1 to 255 |
Set deliveryReport: "per_recipient" to receive a status webhook for each message.
Webhooks
Sinch authenticates callbacks in one of two ways, configured on your callback URL or service plan. Point Sinch at your handler with a per-message webhookUrl or the callback URL on the service plan, then pass the matching credentials:
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: "sinch",
request,
credentials: {
kind: "basic",
username: process.env.SINCH_CALLBACK_USERNAME ?? "",
password: process.env.SINCH_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;
}
}
- Basic (
{ kind: "basic", username, password }): Sinch documents this for SMS callbacks. Use it unless you have a reason not to. Basic requests carry no timestamp, so there is no replay window. - HMAC (
{ kind: "hmac", webhookSecret }): Sinch sets this up through your account manager and does not publish the SMS format. The parser uses the scheme Sinch documents for its Conversation API: a base64 HMAC-SHA256 ofbody.nonce.timestampfrom thex-sinch-webhook-signature,-nonce, and-timestampheaders, with a 300-second timestamp window (toleranceSec). Confirm the format with Sinch before relying on it.
OAuth 2.0 callbacks are not supported. Sinch can send a report more than once and out of order, so store event.dedupeKey and skip repeats. The key is sinch:{batchId}:{recipient}:{status} for delivery reports and sinch:{messageId}:received for inbound messages.
| Sinch status | SMS SDK event |
|---|---|
inbound mo_text, mo_binary, inbound_sms |
message.received, or recipient.opted_out / recipient.help for STOP and HELP keywords |
Queued, Pending |
message.queued |
Sent, Accepted |
message.sent |
Delivered |
message.delivered |
Failed, Undelivered, Expired |
message.undelivered |
Rejected or Filtered with code 40003 |
message.filtered |
Rejected or Filtered with any other code |
message.undelivered |
| anything else | unrecognized |
Limitations
- No MMS. Use Twilio, Telnyx, or Plivo.
- OAuth 2.0 callback authentication is not supported.
- The HMAC callback format is unconfirmed for SMS. Prefer Basic credentials.
Error codes
| Sinch response | Category | Falls back |
|---|---|---|
| 401 or 403 | auth |
Yes |
429 with code 429 or TOO_MANY_REQUESTS |
rate_limited |
Yes, after retries |
| Code 40004 or 40005 | sender |
Yes |
| Code 40301 or 40302 | account |
Yes |
| Code 40002 or 40003 | recipient |
No |
| Any other 4xx | request |
No |
| 429 without the rate-limit code | unknown | Never |
| 5xx, network error, timeout, or 2xx without a batch ID | unknown | Never |
error.provider.requestId holds the X-Request-Id header. See Fallback and retries for what each category does.