diff --git a/fern/apis/signalwire-rest/openapi.yaml b/fern/apis/signalwire-rest/openapi.yaml
index 1ca62a818..2a8a898f6 100644
--- a/fern/apis/signalwire-rest/openapi.yaml
+++ b/fern/apis/signalwire-rest/openapi.yaml
@@ -291,6 +291,11 @@ tags:
externalDocs:
url: https://signalwire.com/docs/apis
description: Developer documentation on Message API endpoints
+ - name: Messages
+ description: Endpoints for sending and redacting messages
+ externalDocs:
+ url: https://signalwire.com/docs/apis
+ description: Developer documentation on Message API endpoints
- name: Voice Logs
description: Endpoints related to accessing voice logs
externalDocs:
@@ -6053,6 +6058,121 @@ paths:
$ref: '#/components/schemas/Types.StatusCodes.StatusCode500'
tags:
- Message Logs
+ /api/messaging/messages:
+ post:
+ operationId: create_message
+ summary: Send a message
+ description: |-
+ Create and queue an outbound SMS or MMS message for delivery. The system determines whether the message is SMS or MMS based on the presence of `media` or the `send_as_mms` flag. The `from` number must be a purchased SignalWire phone number on the authenticated project.
+
+ #### Permissions
+
+ The API token used to authenticate must have the following scope(s) enabled to make a successful request: _Messaging_.
+
+ [Learn more about API scopes](/docs/platform/your-signalwire-api-space).
+ parameters: []
+ responses:
+ '201':
+ description: Response returned when a message is successfully created and queued for delivery.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message.Message'
+ '400':
+ description: The request is invalid.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode400'
+ '401':
+ description: Access is unauthorized.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode401'
+ '422':
+ description: The request contains invalid parameters. See errors for details.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message.MessagesCreateStatusCode422'
+ '500':
+ description: An internal server error occurred.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode500'
+ tags:
+ - Messages
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message.CreateMessageRequest'
+ /api/messaging/messages/{message_id}:
+ patch:
+ operationId: update_message
+ summary: Redact a message
+ description: |-
+ Redact the body of a previously sent message. This endpoint clears the message body for compliance, privacy, or moderation purposes — it does not support arbitrary updates to message attributes. The only accepted value for `body` is an empty string (`""`); any other value is rejected.
+
+ Messages that are still in progress (`queued` or `initiated`) cannot be redacted. Messages in terminal states such as `delivered`, `undelivered`, or `failed` are eligible. Once redacted, the original body is overwritten and cannot be recovered.
+
+ The `:message_id` path parameter is the message segment ID — the same ID returned by the create endpoint and shown in `/api/messaging/logs`.
+
+ #### Permissions
+
+ The API token used to authenticate must have the following scope(s) enabled to make a successful request: _Messaging_.
+
+ [Learn more about API scopes](/docs/platform/your-signalwire-api-space).
+ parameters:
+ - $ref: '#/components/parameters/Message.MessagePathID'
+ responses:
+ '200':
+ description: Response returned when a message has been successfully redacted.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message.Message'
+ '400':
+ description: The request is invalid.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode400'
+ '401':
+ description: Access is unauthorized.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode401'
+ '404':
+ description: The server cannot find the requested resource.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode404'
+ '422':
+ description: The request contains invalid parameters. See errors for details.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message.MessagesUpdateStatusCode422'
+ '500':
+ description: An internal server error occurred.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Types.StatusCodes.StatusCode500'
+ tags:
+ - Messages
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message.UpdateMessageRequest'
/api/project/tokens:
post:
operationId: create_token
@@ -12185,6 +12305,14 @@ components:
schema:
$ref: '#/components/schemas/uuid'
format: uuid
+ Message.MessagePathID:
+ name: message_id
+ in: path
+ required: true
+ description: The message segment ID — the same ID returned by the create endpoint and shown in `/api/messaging/logs`.
+ schema:
+ type: string
+ format: uuid
MfaRequestIdPath:
name: mfa_request_id
in: path
@@ -28884,6 +29012,48 @@ components:
examples:
- 0.00415
description: Details on charges associated with this log.
+ Message.CreateMessageRequest:
+ type: object
+ required:
+ - to
+ - from
+ properties:
+ to:
+ type: string
+ description: Destination phone number in E.164 format (`+` followed by 5-17 digits). Also accepts passthrough numbers like `988`/`+988`.
+ examples:
+ - '+15551234567'
+ from:
+ type: string
+ description: Source phone number. Must be a purchased SignalWire phone number on the project in E.164 format, or a shortcode (5-6 digits). Verified caller IDs are not permitted.
+ examples:
+ - '+15559876543'
+ body:
+ type: string
+ description: Message body text. Required if `media` is not provided. Subject to provider-specific character limits.
+ examples:
+ - 'Your order #12345 has shipped!'
+ media:
+ type: array
+ items:
+ type: string
+ format: uri
+ description: Array of HTTP or HTTPS URLs for media attachments. Presence of media makes the message MMS. Maximum 8 items.
+ examples:
+ - - https://example.com/tracking.png
+ send_as_mms:
+ type: boolean
+ description: Force the message to be sent as MMS even when no media attachments are provided.
+ examples:
+ - false
+ default: false
+ status_callback:
+ type: string
+ format: uri
+ description: A valid URL to receive message status callback events at each state change. See the [Message status callback](/docs/apis/rest/messages/webhooks/message-status-callback) webhook for the payload your URL will receive.
+ examples:
+ - https://example.com/webhooks/message-status
+ description: Request body for sending a new SMS or MMS message.
Message.LogListResponse:
type: object
required:
@@ -29037,6 +29207,132 @@ components:
examples:
- '2024-05-06T12:20:00Z'
description: Response model for message log retrieve endpoint
+ Message.Message:
+ type: object
+ required:
+ - id
+ - from
+ - to
+ - body
+ - status
+ - direction
+ - kind
+ - media
+ - number_of_segments
+ - error_code
+ - error_message
+ - created_at
+ - project_id
+ - status_callback_url
+ - message_uri
+ properties:
+ id:
+ allOf:
+ - $ref: '#/components/schemas/uuid'
+ format: uuid
+ description: The unique ID of the message. This is the `MessageSegment` ID, consistent with the dashboard and the `/api/messaging/logs` endpoint.
+ examples:
+ - c2d3e4f5-a6b7-8901-cdef-234567890abc
+ from:
+ type: string
+ description: The source phone number.
+ examples:
+ - '+15559876543'
+ to:
+ type: string
+ description: The destination phone number.
+ examples:
+ - '+15551234567'
+ body:
+ type: string
+ description: The message body text. Returns an empty string when the message has been redacted.
+ examples:
+ - 'Your order #12345 has shipped!'
+ status:
+ allOf:
+ - $ref: '#/components/schemas/Message.MessageStatus'
+ description: Delivery state of the message.
+ examples:
+ - queued
+ direction:
+ allOf:
+ - $ref: '#/components/schemas/Message.MessageDirection'
+ description: The direction of the message.
+ examples:
+ - outbound
+ kind:
+ allOf:
+ - $ref: '#/components/schemas/Message.MessageKind'
+ description: The kind of message.
+ examples:
+ - sms
+ media:
+ type: array
+ items:
+ type: string
+ format: uri
+ description: Array of URLs for any media attachments on the message. Empty for SMS.
+ examples:
+ - []
+ number_of_segments:
+ type: integer
+ format: int32
+ description: Number of segments the message body was split into for delivery.
+ examples:
+ - 1
+ error_code:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Provider-specific error code if delivery failed. Null when no error occurred.
+ examples:
+ - null
+ error_message:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Human-readable error message if delivery failed. Null when no error occurred.
+ examples:
+ - null
+ created_at:
+ type: string
+ format: date-time
+ description: Date and time when the message was created.
+ examples:
+ - '2024-05-06T12:20:00Z'
+ project_id:
+ allOf:
+ - $ref: '#/components/schemas/uuid'
+ format: uuid
+ description: The ID of the project the message belongs to.
+ examples:
+ - a1b2c3d4-e5f6-7890-abcd-ef1234567890
+ status_callback_url:
+ anyOf:
+ - type: string
+ format: uri
+ - type: 'null'
+ description: Callback URL configured to receive message status events. Null if no callback was configured.
+ examples:
+ - null
+ message_uri:
+ type: string
+ description: Relative URL for retrieving the message via the `/api/messaging/logs` endpoint.
+ examples:
+ - /api/messaging/logs/c2d3e4f5-a6b7-8901-cdef-234567890abc
+ description: A message record. Returned by the create and update endpoints.
+ Message.MessageDirection:
+ type: string
+ enum:
+ - inbound
+ - outbound
+ description: The direction of a message.
+ Message.MessageKind:
+ type: string
+ enum:
+ - sms
+ - mms
+ description: The kind of message.
Message.MessageLog:
type: object
required:
@@ -29187,6 +29483,151 @@ components:
message: This value must be a DateTime
attribute: created_before
url: https://signalwire.com/docs/apis/error-codes
+ Message.MessageStatus:
+ type: string
+ enum:
+ - queued
+ - initiated
+ - sent
+ - delivered
+ - undelivered
+ - failed
+ - read
+ description: Delivery state of a message.
+ Message.MessageStatusCallbackPayload:
+ type: object
+ required:
+ - id
+ - project_id
+ - status
+ - to
+ - from
+ - body
+ - number_of_segments
+ - timestamp
+ - error_code
+ - error_message
+ properties:
+ id:
+ allOf:
+ - $ref: '#/components/schemas/uuid'
+ format: uuid
+ description: The unique ID of the message segment.
+ examples:
+ - a1b2c3d4-e5f6-7890-abcd-ef1234567890
+ project_id:
+ allOf:
+ - $ref: '#/components/schemas/uuid'
+ format: uuid
+ description: The ID of the project the message belongs to.
+ examples:
+ - b2c3d4e5-f6a7-8901-bcde-f12345678901
+ status:
+ allOf:
+ - $ref: '#/components/schemas/Message.MessageStatus'
+ description: The current delivery state of the message.
+ examples:
+ - delivered
+ to:
+ type: string
+ description: The destination phone number.
+ examples:
+ - '+15551234567'
+ from:
+ type: string
+ description: The source phone number.
+ examples:
+ - '+15559876543'
+ body:
+ type: string
+ description: The message body text.
+ examples:
+ - Hello World!
+ number_of_segments:
+ type: integer
+ format: int32
+ description: Number of segments the message body was split into for delivery.
+ examples:
+ - 1
+ timestamp:
+ type: string
+ format: date-time
+ description: Timestamp of the status transition.
+ examples:
+ - '2026-03-17T22:26:57Z'
+ error_code:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Provider-specific error code if delivery failed. Null when no error occurred.
+ examples:
+ - null
+ error_message:
+ anyOf:
+ - type: string
+ - type: 'null'
+ description: Human-readable error message if delivery failed. Null when no error occurred.
+ examples:
+ - null
+ description: |-
+ Payload sent by SignalWire to the `status_callback` URL each time a message transitions to a new state. The same payload shape is used for RELAY SDK message callbacks and SWML `send_sms` status callbacks.
+
+ Configure `status_callback` when [sending a message](/docs/apis/rest/messages/create-message).
+ title: Message status callback
+ Message.MessagesCreateStatusCode422:
+ type: object
+ required:
+ - errors
+ properties:
+ errors:
+ type: array
+ items:
+ $ref: '#/components/schemas/Types.StatusCodes.RestApiErrorItem'
+ description: List of validation errors.
+ description: The request contains invalid parameters. See errors for details.
+ examples:
+ - statusCode: 422
+ errors:
+ - type: validation_error
+ code: invalid_from_number
+ message: From must be a valid purchased phone number or WhatsApp business number from your SignalWire project.
+ attribute: from
+ url: https://developer.signalwire.com/rest/overview/error-codes/#invalid_from_number
+ Message.MessagesUpdateStatusCode422:
+ type: object
+ required:
+ - errors
+ properties:
+ errors:
+ type: array
+ items:
+ $ref: '#/components/schemas/Types.StatusCodes.RestApiErrorItem'
+ description: List of validation errors.
+ description: The request contains invalid parameters. See errors for details.
+ examples:
+ - statusCode: 422
+ errors:
+ - type: validation_error
+ code: body_must_be_empty
+ message: must be an empty string to redact the message
+ attribute: body
+ url: https://developer.signalwire.com/rest/overview/error-codes/#body_must_be_empty
+ - type: validation_error
+ code: cannot_redact_in_progress_message
+ message: Cannot redact a message that is in progress.
+ attribute: base
+ url: https://developer.signalwire.com/rest/overview/error-codes/#cannot_redact_in_progress_message
+ Message.UpdateMessageRequest:
+ type: object
+ required:
+ - body
+ properties:
+ body:
+ type: string
+ description: Must be an empty string (`""`) to redact the message. Any non-empty value is rejected with `body_must_be_empty`. This is the only field that can be updated.
+ examples:
+ - ''
+ description: Request body for redacting the body of a previously sent message. Only `body` may be updated, and it must be an empty string.
MessagingChannel:
type: object
required:
@@ -42278,6 +42719,92 @@ servers:
default: '{Your_Space_Name}'
description: Your SignalWire Space name
webhooks:
+ messageStatusCallback:
+ post:
+ operationId: message_status_callback
+ summary: Message status callback
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: The unique ID of the message segment.
+ example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
+ project_id:
+ type: string
+ description: The ID of the project the message belongs to.
+ example: b2c3d4e5-f6a7-8901-bcde-f12345678901
+ status:
+ type: string
+ enum:
+ - queued
+ - initiated
+ - sent
+ - delivered
+ - undelivered
+ - failed
+ - read
+ description: The current delivery state of the message.
+ example: delivered
+ to:
+ type: string
+ description: The destination phone number.
+ example: '+15551234567'
+ from:
+ type: string
+ description: The source phone number.
+ example: '+15559876543'
+ body:
+ type: string
+ description: The message body text.
+ example: Hello World!
+ number_of_segments:
+ type: integer
+ description: Number of segments the message body was split into for delivery.
+ example: 1
+ timestamp:
+ type: string
+ format: date-time
+ description: Timestamp of the status transition.
+ example: '2026-03-17T22:26:57Z'
+ error_code:
+ type: string
+ nullable: true
+ description: Provider-specific error code if delivery failed. Null when no error occurred.
+ example: null
+ error_message:
+ type: string
+ nullable: true
+ description: Human-readable error message if delivery failed. Null when no error occurred.
+ example: null
+ required:
+ - id
+ - project_id
+ - status
+ - to
+ - from
+ - body
+ - number_of_segments
+ - timestamp
+ - error_code
+ - error_message
+ description: |-
+ Payload sent by SignalWire to the `status_callback` URL each time a message transitions to a new state. The same payload shape is used for RELAY SDK message callbacks and SWML `send_sms` status callbacks.
+
+ Configure `status_callback` when [sending a message](/docs/apis/rest/messages/create-message).
+ responses:
+ '200':
+ description: Webhook received
+ description: |-
+ Payload sent by SignalWire to the `status_callback` URL each time a message transitions to a new state. The same payload shape is used for RELAY SDK message callbacks and SWML `send_sms` status callbacks.
+
+ Configure `status_callback` when [sending a message](/docs/apis/rest/messages/create-message).
+ tags:
+ - Messages
tenDlcStatusCallback:
post:
operationId: ten_dlc_status_callback
diff --git a/fern/products/apis.yml b/fern/products/apis.yml
index a8d067acc..8f31b10da 100644
--- a/fern/products/apis.yml
+++ b/fern/products/apis.yml
@@ -101,6 +101,11 @@ navigation:
- section: Messaging
skip-slug: true
contents:
+ - messages:
+ - section: Webhooks
+ slug: webhooks
+ contents:
+ - subpackage_messages.message_status_callback
- section: Campaign Registry
contents:
- section: Brands
diff --git a/fern/products/apis/pages/core/error-codes.mdx b/fern/products/apis/pages/core/error-codes.mdx
index 9b612dbe5..45e6cc7d0 100644
--- a/fern/products/apis/pages/core/error-codes.mdx
+++ b/fern/products/apis/pages/core/error-codes.mdx
@@ -52,6 +52,16 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- Unable to locate a billing route.
+
+
+- The `body` value must be an empty string to redact the message.
+
+
+
+
+- A `body` must be included if there is no `media`.
+
+
- The CSP Brand Reference must be unique within the Space
@@ -97,6 +107,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- The request cannot be processed right now.
+
+
+- Cannot redact a message that is in progress. Messages in `queued` or `initiated` state cannot be redacted; only messages in terminal states (`delivered`, `undelivered`, `failed`) are eligible.
+
+
- Call is not in-progress. Cannot redirect.
@@ -132,6 +147,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- The number of channels exceeds the allowed limit.
+
+
+- The message `body` exceeds the provider-specific character limit.
+
+
- The provided token is invalid for the provided room session.
@@ -222,6 +242,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- The provided from_fabric_address_id does not match an address for the provided subscriber.
+
+
+- The `from` number must belong to an active campaign.
+
+
- The parameters that were input cannot be specified together.
@@ -252,6 +277,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- This account has an insufficient balance.
+
+
+- The `to` number must be the verified caller ID for the trial campaign.
+
+
- Addresses must contain non-empty values for name and destination
@@ -332,6 +362,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- Provided chunking parameters are not valid for sliding chunking. Allowed parameters are chunk_size and overlap_size.
+
+
+- The `from` value must be a valid purchased phone number on your SignalWire project. Verified caller IDs cannot be used for outbound messaging.
+
+
- Fabric addresses must form a valid conversation group (a single room or two non-room subscribers) and group ID must start with 'sw_'.
@@ -472,6 +507,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- has exceeded the maximum number of queued messages/calls for this number
+
+
+- Exceeded the maximum number of media items allowed per message (currently 8).
+
+
- The media size exceeds the allowed limit.
@@ -482,6 +522,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- A media URL is required.
+
+
+- The project's messaging backlog limit has been exceeded. Wait before sending more messages.
+
+
- The message body is required.
@@ -497,6 +542,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- Messaging provider is invalid.
+
+
+- The `from` number is not capable of sending the determined message kind (SMS or MMS).
+
+
- The passed value is blank or empty, but it is required.
@@ -652,6 +702,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- Unable to locate a route to the destination number. Please check geographic permissions and the destination number, and contact Support if issues persist.
+
+
+- The `to` number is not routeable.
+
+
- The value is not valid (must be an E.164 number, caller ID string or SIP URI)
@@ -717,6 +772,11 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- The page token is required when the page number is passed and greater than 0.
+
+
+- The Platform Free Trial campaign daily message cap of 200 messages has been reached.
+
+
- The provided ID is unrecognized. Verify that the ID points to a valid resource belonging to the current project.
@@ -787,11 +847,21 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- The provided send_as number is missing or invalid.
+
+
+- Sending is restricted to verified numbers for this account.
+
+
- Only external SIP entities are supported for the SIP Gateway. Do not provide a SignalWire-managed SIP entity.
+
+
+- The sole proprietor campaign daily message cap of 1,000 messages has been reached.
+
+
- Sort order cannot be specified without sort by.
@@ -925,4 +995,9 @@ When using SignalWire REST APIs, some errors will include error codes. Below, yo
- This number has received a verification call too recently. Please wait a minute before requesting another call.
+
+
+
+
+- Verified caller IDs are not permitted for outbound messaging.
\ No newline at end of file
diff --git a/fern/products/platform/pages/messaging/sms/overview.mdx b/fern/products/platform/pages/messaging/sms/overview.mdx
index 5883953c2..d9b161ce4 100644
--- a/fern/products/platform/pages/messaging/sms/overview.mdx
+++ b/fern/products/platform/pages/messaging/sms/overview.mdx
@@ -268,6 +268,12 @@ You can see this list in full detail [here](/docs/platform/messaging/sms-best-pr
+
+
+For compliance, privacy, or moderation use cases, you can redact the body of a previously sent message by sending a `PATCH` request to `/api/messaging/messages/:message_id` with a `body` of `""`. Only messages in terminal states (`delivered`, `undelivered`, `failed`) are eligible; messages still in progress (`queued` or `initiated`) cannot be redacted. Once redacted, the original body is overwritten and cannot be recovered. See the [Redact a message](/docs/apis/rest/signalwire-rest/messages/update-message) API reference for full details.
+
+
+
Carrier Passthrough Fees, also known as Network Access Fees (NAFs),
diff --git a/specs/_shared/webhook/decorator.js b/specs/_shared/webhook/decorator.js
index 276831436..3b56c1e41 100644
--- a/specs/_shared/webhook/decorator.js
+++ b/specs/_shared/webhook/decorator.js
@@ -1,4 +1,5 @@
import { setExtension, getExtensions, getTagsMetadata } from "@typespec/openapi";
+import { serializeValueAsJson } from "@typespec/compiler";
// ── String helpers ───────────────────────────────────────────────────
@@ -24,9 +25,22 @@ function getDecoratorArg(type, decoratorName) {
return undefined;
}
+// Returns the TypeSpec Value (not jsValue) for a decorator argument. Use for
+// values that need to be JSON-serialized via serializeValueAsJson — jsValue
+// for complex args (enum members, scalar constructors like utcDateTime.fromISO)
+// contains cyclic TypeSpec objects that crash the YAML emitter.
+function getDecoratorArgValue(type, decoratorName) {
+ for (const dec of type.decorators || []) {
+ if (dec.definition?.name === decoratorName) {
+ return dec.args?.[0]?.value;
+ }
+ }
+ return undefined;
+}
+
// ── Schema serialization ─────────────────────────────────────────────
-function modelToJsonSchema(model, seen) {
+function modelToJsonSchema(model, seen, program) {
if (!seen) seen = new Set();
if (seen.has(model)) return { type: "object" };
seen.add(model);
@@ -35,11 +49,13 @@ function modelToJsonSchema(model, seen) {
const required = [];
for (const [name, prop] of model.properties) {
- const schema = typeToSchema(prop.type, seen);
+ const schema = typeToSchema(prop.type, seen, program);
const doc = getDecoratorArg(prop, "@doc");
if (doc) schema.description = doc;
- const example = getDecoratorArg(prop, "@example");
- if (example !== undefined) schema.example = example;
+ const exampleValue = getDecoratorArgValue(prop, "@example");
+ if (exampleValue !== undefined && program) {
+ schema.example = serializeValueAsJson(program, exampleValue, prop.type);
+ }
properties[name] = schema;
if (!prop.optional) required.push(name);
}
@@ -53,7 +69,7 @@ function modelToJsonSchema(model, seen) {
return result;
}
-function typeToSchema(type, seen) {
+function typeToSchema(type, seen, program) {
if (!seen) seen = new Set();
switch (type.kind) {
@@ -64,7 +80,7 @@ function typeToSchema(type, seen) {
if (["int32", "int64", "integer", "uint32", "uint64"].includes(n)) return { type: "integer" };
if (["float", "float32", "float64", "numeric"].includes(n)) return { type: "number" };
if (n === "utcDateTime") return { type: "string", format: "date-time" };
- if (type.baseScalar) return typeToSchema(type.baseScalar, seen);
+ if (type.baseScalar) return typeToSchema(type.baseScalar, seen, program);
return { type: "string" };
}
case "Intrinsic":
@@ -79,7 +95,7 @@ function typeToSchema(type, seen) {
return nullVariant ? { oneOf: [schema, { type: "null" }] } : schema;
}
- const schemas = variants.map((v) => typeToSchema(v.type, seen));
+ const schemas = variants.map((v) => typeToSchema(v.type, seen, program));
if (schemas.length === 2 && nullVariant) {
const s = schemas.find((s) => s.type !== "null");
if (s) return { ...s, nullable: true };
@@ -94,9 +110,9 @@ function typeToSchema(type, seen) {
return { type: "boolean", enum: [type.value] };
case "Model": {
if (type.indexer?.key?.name === "integer") {
- return { type: "array", items: type.indexer.value ? typeToSchema(type.indexer.value, seen) : {} };
+ return { type: "array", items: type.indexer.value ? typeToSchema(type.indexer.value, seen, program) : {} };
}
- return modelToJsonSchema(type, seen);
+ return modelToJsonSchema(type, seen, program);
}
case "Enum":
return { type: "string", enum: [...type.members.values()].map((m) => m.value ?? m.name) };
@@ -197,7 +213,7 @@ export function $webhook(context, target, name, payload, tag, operationId) {
summary: getDecoratorArg(payload, "@summary") ?? toHumanReadable(name),
requestBody: {
required: true,
- content: { "application/json": { schema: modelToJsonSchema(payload) } },
+ content: { "application/json": { schema: modelToJsonSchema(payload, undefined, context.program) } },
},
responses: { 200: { description: "Webhook received" } },
};
diff --git a/specs/signalwire-rest/main.tsp b/specs/signalwire-rest/main.tsp
index 6b8abecdc..15158e785 100644
--- a/specs/signalwire-rest/main.tsp
+++ b/specs/signalwire-rest/main.tsp
@@ -42,6 +42,7 @@ using TypeSpec.OpenAPI;
// Voice API
@tagMetadata(VOICE_LOGS_TAG, VOICE_LOGS_TAG_METADATA)
// Message API
+@tagMetadata(MESSAGES_TAG, MESSAGES_TAG_METADATA)
@tagMetadata(MESSAGE_LOGS_TAG, MESSAGE_LOGS_TAG_METADATA)
// Fax API
@tagMetadata(FAX_LOGS_TAG, FAX_LOGS_TAG_METADATA)
diff --git a/specs/signalwire-rest/message-api/main.tsp b/specs/signalwire-rest/message-api/main.tsp
index 976b7a90d..c7db10bc5 100644
--- a/specs/signalwire-rest/message-api/main.tsp
+++ b/specs/signalwire-rest/message-api/main.tsp
@@ -2,6 +2,7 @@ import "@typespec/http";
import "@typespec/openapi";
import "../types";
import "./logs";
+import "./messages";
import "../_globally_shared/const.tsp";
import "../../_shared/auth.tsp";
import "./tags.tsp";
diff --git a/specs/signalwire-rest/message-api/messages/main.tsp b/specs/signalwire-rest/message-api/messages/main.tsp
new file mode 100644
index 000000000..239e0e621
--- /dev/null
+++ b/specs/signalwire-rest/message-api/messages/main.tsp
@@ -0,0 +1,58 @@
+import "@typespec/http";
+import "@typespec/openapi";
+import "../../types/status-codes/main.tsp";
+import "../tags.tsp";
+import "./models/core.tsp";
+import "./models/requests.tsp";
+import "./models/responses.tsp";
+import "./models/errors.tsp";
+import "./models/webhooks.tsp";
+import "../../../_shared/alias/token-permissions.tsp";
+import "../../../_shared/webhook/decorator.tsp";
+
+using TypeSpec.Http;
+using TypeSpec.OpenAPI;
+using Types.StatusCodes;
+
+@webhook("messageStatusCallback", MessageStatusCallbackPayload, MESSAGES_TAG)
+@route("/messages")
+namespace SignalWireAPI.Message.Messages {
+ @tag(MESSAGES_TAG)
+ @friendlyName("Messages")
+ interface Messages {
+ @operationId("create_message")
+ @summary("Send a message")
+ @doc("""
+ Create and queue an outbound SMS or MMS message for delivery. The system determines whether the message is SMS or MMS based on the presence of `media` or the `send_as_mms` flag. The `from` number must be a purchased SignalWire phone number on the authenticated project.
+
+ ${tokenPermissions<"_Messaging_">}
+ """)
+ @post
+ create(...CreateMessageRequest):
+ | CreateMessageResponse
+ | StatusCode400
+ | StatusCode401
+ | MessagesCreateStatusCode422
+ | StatusCode500;
+
+ @operationId("update_message")
+ @summary("Redact a message")
+ @doc("""
+ Redact the body of a previously sent message. This endpoint clears the message body for compliance, privacy, or moderation purposes — it does not support arbitrary updates to message attributes. The only accepted value for `body` is an empty string (`""`); any other value is rejected.
+
+ Messages that are still in progress (`queued` or `initiated`) cannot be redacted. Messages in terminal states such as `delivered`, `undelivered`, or `failed` are eligible. Once redacted, the original body is overwritten and cannot be recovered.
+
+ The `:message_id` path parameter is the message segment ID — the same ID returned by the create endpoint and shown in `/api/messaging/logs`.
+
+ ${tokenPermissions<"_Messaging_">}
+ """)
+ @patch(#{ implicitOptionality: false })
+ update(...MessagePathID, ...UpdateMessageRequest):
+ | UpdateMessageResponse
+ | StatusCode400
+ | StatusCode401
+ | StatusCode404
+ | MessagesUpdateStatusCode422
+ | StatusCode500;
+ }
+}
diff --git a/specs/signalwire-rest/message-api/messages/models/core.tsp b/specs/signalwire-rest/message-api/messages/models/core.tsp
new file mode 100644
index 000000000..98b6b8072
--- /dev/null
+++ b/specs/signalwire-rest/message-api/messages/models/core.tsp
@@ -0,0 +1,123 @@
+import "@typespec/http";
+import "../../../types";
+import "../../../_globally_shared/const.tsp";
+
+using TypeSpec.Http;
+
+namespace SignalWireAPI.Message;
+
+@doc("Path parameter identifying the target message by its segment ID.")
+model MessagePathID {
+ @path
+ @format("uuid")
+ @doc("The message segment ID — the same ID returned by the create endpoint and shown in `/api/messaging/logs`.")
+ @example("c2d3e4f5-a6b7-8901-cdef-234567890abc")
+ message_id: string;
+}
+
+@doc("Delivery state of a message.")
+enum MessageStatus {
+ @doc("Message has been created and queued for delivery.")
+ queued,
+
+ @doc("Message delivery has been initiated.")
+ initiated,
+
+ @doc("Message has been sent to the carrier network.")
+ sent,
+
+ @doc("Message was successfully delivered to the recipient.")
+ delivered,
+
+ @doc("Message could not be delivered to the recipient.")
+ undelivered,
+
+ @doc("Message delivery failed. Check `error_code` and `error_message` for details.")
+ failed,
+
+ @doc("Message has been read by the recipient (when supported by the channel).")
+ read,
+}
+
+@doc("The direction of a message.")
+enum MessageDirection {
+ @doc("A message received by the project.")
+ inbound,
+
+ @doc("A message sent from the project.")
+ outbound,
+}
+
+@doc("The kind of message.")
+enum MessageKind {
+ @doc("SMS text message.")
+ sms,
+
+ @doc("MMS message containing media.")
+ mms,
+}
+
+@doc("A message record. Returned by the create and update endpoints.")
+model Message {
+ @format("uuid")
+ @doc("The unique ID of the message. This is the `MessageSegment` ID, consistent with the dashboard and the `/api/messaging/logs` endpoint.")
+ @example("c2d3e4f5-a6b7-8901-cdef-234567890abc")
+ id: uuid;
+
+ @doc("The source phone number.")
+ @example("+15559876543")
+ from: string;
+
+ @doc("The destination phone number.")
+ @example("+15551234567")
+ to: string;
+
+ @doc("The message body text. Returns an empty string when the message has been redacted.")
+ @example("Your order #12345 has shipped!")
+ body: string;
+
+ @doc("Delivery state of the message.")
+ @example(MessageStatus.queued)
+ status: MessageStatus;
+
+ @doc("The direction of the message.")
+ @example(MessageDirection.outbound)
+ direction: MessageDirection;
+
+ @doc("The kind of message.")
+ @example(MessageKind.sms)
+ kind: MessageKind;
+
+ @doc("Array of URLs for any media attachments on the message. Empty for SMS.")
+ @example(#[])
+ media: url[];
+
+ @doc("Number of segments the message body was split into for delivery.")
+ @example(1)
+ number_of_segments: int32;
+
+ @doc("Provider-specific error code if delivery failed. Null when no error occurred.")
+ @example(null)
+ error_code: string | null;
+
+ @doc("Human-readable error message if delivery failed. Null when no error occurred.")
+ @example(null)
+ error_message: string | null;
+
+ @doc("Date and time when the message was created.")
+ @example(UTC_TIME_EXAMPLE)
+ created_at: utcDateTime;
+
+ @format("uuid")
+ @doc("The ID of the project the message belongs to.")
+ @example("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
+ project_id: uuid;
+
+ @doc("Callback URL configured to receive message status events. Null if no callback was configured.")
+ @example(null)
+ status_callback_url: url | null;
+
+ @doc("Relative URL for retrieving the message via the `/api/messaging/logs` endpoint.")
+ @example("/api/messaging/logs/c2d3e4f5-a6b7-8901-cdef-234567890abc")
+ message_uri: string;
+}
diff --git a/specs/signalwire-rest/message-api/messages/models/errors.tsp b/specs/signalwire-rest/message-api/messages/models/errors.tsp
new file mode 100644
index 000000000..080e4d539
--- /dev/null
+++ b/specs/signalwire-rest/message-api/messages/models/errors.tsp
@@ -0,0 +1,40 @@
+import "../../../types/status-codes";
+
+using Types.StatusCodes;
+
+namespace SignalWireAPI.Message;
+
+@example(#{
+ statusCode: 422,
+ errors: #[
+ #{
+ type: "validation_error",
+ code: "invalid_from_number",
+ message: "From must be a valid purchased phone number or WhatsApp business number from your SignalWire project.",
+ attribute: "from",
+ url: "https://developer.signalwire.com/rest/overview/error-codes/#invalid_from_number",
+ }
+ ],
+})
+model MessagesCreateStatusCode422 is StatusCode422;
+
+@example(#{
+ statusCode: 422,
+ errors: #[
+ #{
+ type: "validation_error",
+ code: "body_must_be_empty",
+ message: "must be an empty string to redact the message",
+ attribute: "body",
+ url: "https://developer.signalwire.com/rest/overview/error-codes/#body_must_be_empty",
+ },
+ #{
+ type: "validation_error",
+ code: "cannot_redact_in_progress_message",
+ message: "Cannot redact a message that is in progress.",
+ attribute: "base",
+ url: "https://developer.signalwire.com/rest/overview/error-codes/#cannot_redact_in_progress_message",
+ }
+ ],
+})
+model MessagesUpdateStatusCode422 is StatusCode422;
diff --git a/specs/signalwire-rest/message-api/messages/models/requests.tsp b/specs/signalwire-rest/message-api/messages/models/requests.tsp
new file mode 100644
index 000000000..1916acb21
--- /dev/null
+++ b/specs/signalwire-rest/message-api/messages/models/requests.tsp
@@ -0,0 +1,40 @@
+import "@typespec/http";
+import "./core.tsp";
+
+using TypeSpec.Http;
+
+namespace SignalWireAPI.Message;
+
+@doc("Request body for sending a new SMS or MMS message.")
+model CreateMessageRequest {
+ @doc("Destination phone number in E.164 format (`+` followed by 5-17 digits). Also accepts passthrough numbers like `988`/`+988`.")
+ @example("+15551234567")
+ to: string;
+
+ @doc("Source phone number. Must be a purchased SignalWire phone number on the project in E.164 format, or a shortcode (5-6 digits). Verified caller IDs are not permitted.")
+ @example("+15559876543")
+ from: string;
+
+ @doc("Message body text. Required if `media` is not provided. Subject to provider-specific character limits.")
+ @example("Your order #12345 has shipped!")
+ body?: string;
+
+ @doc("Array of HTTP or HTTPS URLs for media attachments. Presence of media makes the message MMS. Maximum 8 items.")
+ @example(#["https://example.com/tracking.png"])
+ media?: url[];
+
+ @doc("Force the message to be sent as MMS even when no media attachments are provided.")
+ @example(false)
+ send_as_mms?: boolean = false;
+
+ @doc("A valid URL to receive message status callback events at each state change. See the [Message status callback](/docs/apis/rest/messages/webhooks/message-status-callback) webhook for the payload your URL will receive.")
+ @example("https://example.com/webhooks/message-status")
+ status_callback?: url;
+}
+
+@doc("Request body for redacting the body of a previously sent message. Only `body` may be updated, and it must be an empty string.")
+model UpdateMessageRequest {
+ @doc("Must be an empty string (`\"\"`) to redact the message. Any non-empty value is rejected with `body_must_be_empty`. This is the only field that can be updated.")
+ @example("")
+ body: string;
+}
diff --git a/specs/signalwire-rest/message-api/messages/models/responses.tsp b/specs/signalwire-rest/message-api/messages/models/responses.tsp
new file mode 100644
index 000000000..baab89d79
--- /dev/null
+++ b/specs/signalwire-rest/message-api/messages/models/responses.tsp
@@ -0,0 +1,24 @@
+import "@typespec/http";
+import "./core.tsp";
+
+using TypeSpec.Http;
+
+namespace SignalWireAPI.Message;
+
+@doc("Response returned when a message is successfully created and queued for delivery.")
+model CreateMessageResponse {
+ @statusCode
+ statusCode: 201;
+
+ @body
+ body: Message;
+}
+
+@doc("Response returned when a message has been successfully redacted.")
+model UpdateMessageResponse {
+ @statusCode
+ statusCode: 200;
+
+ @body
+ body: Message;
+}
diff --git a/specs/signalwire-rest/message-api/messages/models/webhooks.tsp b/specs/signalwire-rest/message-api/messages/models/webhooks.tsp
new file mode 100644
index 000000000..e6a12a092
--- /dev/null
+++ b/specs/signalwire-rest/message-api/messages/models/webhooks.tsp
@@ -0,0 +1,55 @@
+import "../../../../_shared/webhook/decorator.tsp";
+import "../../../types";
+import "./core.tsp";
+
+namespace SignalWireAPI.Message;
+
+@summary("Message status callback")
+@doc("""
+ Payload sent by SignalWire to the `status_callback` URL each time a message transitions to a new state. The same payload shape is used for RELAY SDK message callbacks and SWML `send_sms` status callbacks.
+
+ Configure `status_callback` when [sending a message](/docs/apis/rest/messages/create-message).
+ """)
+model MessageStatusCallbackPayload {
+ @format("uuid")
+ @doc("The unique ID of the message segment.")
+ @example("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
+ id: uuid;
+
+ @format("uuid")
+ @doc("The ID of the project the message belongs to.")
+ @example("b2c3d4e5-f6a7-8901-bcde-f12345678901")
+ project_id: uuid;
+
+ @doc("The current delivery state of the message.")
+ @example(MessageStatus.delivered)
+ status: MessageStatus;
+
+ @doc("The destination phone number.")
+ @example("+15551234567")
+ to: string;
+
+ @doc("The source phone number.")
+ @example("+15559876543")
+ from: string;
+
+ @doc("The message body text.")
+ @example("Hello World!")
+ body: string;
+
+ @doc("Number of segments the message body was split into for delivery.")
+ @example(1)
+ number_of_segments: int32;
+
+ @doc("Timestamp of the status transition.")
+ @example(utcDateTime.fromISO("2026-03-17T22:26:57Z"))
+ timestamp: utcDateTime;
+
+ @doc("Provider-specific error code if delivery failed. Null when no error occurred.")
+ @example(null)
+ error_code: string | null;
+
+ @doc("Human-readable error message if delivery failed. Null when no error occurred.")
+ @example(null)
+ error_message: string | null;
+}
diff --git a/specs/signalwire-rest/message-api/tags.tsp b/specs/signalwire-rest/message-api/tags.tsp
index 064fc2547..5e5cba5e6 100644
--- a/specs/signalwire-rest/message-api/tags.tsp
+++ b/specs/signalwire-rest/message-api/tags.tsp
@@ -9,3 +9,13 @@ const MESSAGE_LOGS_TAG_METADATA = #{
description: "Developer documentation on Message API endpoints",
},
};
+
+const MESSAGES_TAG = "Messages";
+
+const MESSAGES_TAG_METADATA = #{
+ description: "Endpoints for sending and redacting messages",
+ externalDocs: #{
+ url: "https://signalwire.com/docs/apis",
+ description: "Developer documentation on Message API endpoints",
+ },
+};