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", + }, +};