Commit 0bdcd212 authored by Prasad Vittaldev's avatar Prasad Vittaldev

Initial commit

parents
Pipeline #10177 failed with stages
.venv/
__pycache__/
*.py[cod]
.env
*.log
.DS_Store
FROM python:3.10-slim
WORKDIR /app
# Install system dependencies if needed
# RUN apt-get update && apt-get install -y ...
# Copy project configuration and source first so editable install sees the package
COPY pyproject.toml .
COPY README.md .
COPY src/ src/
# Install dependencies in editable mode for dev convenience
RUN pip install --no-cache-dir -e .
# Set python path to include src
ENV PYTHONPATH=/app/src
# Default command
CMD ["python", "-m", "src.server"]
# Communication MCP Middleware Server
This is an MCP server that acts as a middleware for the Ivrnet Message API.
## Features
- Voice, Text, and Email job management.
- Voice file management.
- List management (bounce list, opt-in list).
- Error reporting.
- Support for multiple environments (Production CA, Production US, Test).
## Usage
### Docker
```bash
docker-compose up --build
```
Once running, the MCP server is available at `http://localhost:8000/sse` (see `langflow_config.json`).
### Local Development
```bash
pip install -e .
python src/server.py
```
### Connect from Langflow
Import `langflow_config.json` into Langflow (or copy its values) so Langflow knows to talk to `http://localhost:8000/sse`.
### Testing
Install dev tooling and run pytest:
```bash
pip install -e .[dev]
pytest
```
version: '3.8'
services:
app:
build: .
volumes:
- .:/app
environment:
- PYTHONPATH=/app/src
# Keep the container running for development if needed, or run the server
# For development, you might want to run tests or a shell
ports:
- "8000:8000"
# command: tail -f /dev/null
command: uvicorn src.server:app --host 0.0.0.0 --port 8000
stdin_open: true # docker run -i
tty: true # docker run -t
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("test")
print("Attributes:", dir(mcp))
try:
app = mcp.sse_app()
print("sse_app() Result Type:", type(app))
except Exception as e:
print("Error calling sse_app():", e)
{
"name": "communication-mcp",
"url": "http://localhost:8000/sse"
}
# Message API Specifications
The Message REST API provides applications a simple way to send SMS, Voice and Email messages.
## Accessing the API
### Request Format
The API is served over HTTPS to ensure data privacy. Unencrypted HTTP is not supported. All requests require authentication implemented using either basic authentication or apikey (whichever is preferred by the end client).
ONLY one form of authorization must be present in the header.
* The username:password tuple is provided to you by Ivrnet and must be Base64 encoded and added to the `Authorization` header of your request.
* The apikey is provided to you by Ivrnet and must be placed in the `X-API-KEY` header of your request.
Endpoints described in this document assume that each request is prefixed with the one of the base URL's below.
| Environment | Endpoint |
| ----------- | -------- |
| Production Canada | https://message.ivrnet.com/api/v1/ |
| Production USA | https://message.us.ivrnet.com/api/v1/ |
| Test | https://message-staging.ivrnet.com/api/v1/ |
* NOTE: The test environment is not configured to handle a very high load. If you require load testing, please contact Ivrnet so we can provision a production-like environment for you.
* NOTE: IETF language tags are used throughout the API and map common messages to recipients. See below for specialization and restrictions:
* The /voice/ endpoint uses language tags matched to transcribe Text-To-Speech (TTS) to the desired female or male voice where applicable.
* The /text/ and /email/ endpoints do not transcribe text from one language to another by default, this must be delivered to the API in advance. However, an (optional) `body.options.languagetranslate` feature is available to translate from one language to the desired language when set to true (default is false).
* <u><b>IMPORTANT NOTE</b></u>: Any Voice/Email/Text job using TTS or LanguageTranslation services MAY require special characters to prevent automated translation of certain original text where not desired
* For example: if the message delivered to the endpoint is `"Welcome to Sangre de Christo, we hope you enjoy your stay."` and the IETF languagetag is set to `en-us` and translation services are enabled (Voice TTS has translation enabled by default), the translator may convert `Sangre de Christo` from the interpreted language of `Spanish` and replace the content with the English equivalent `Blood of Christ` (which is not always desirable).
* In order to maintain `Sangre de Christo` as-is and without being translated to English (from the interpreted Spanish), you would need to enclose the words with `{{` and `}}`, such as `"Welcome to {{Sangre de Christo}}, we hope you enjoy your stay."`
* <u><b>IMPORTANT NOTE</b></u>: Keeping in mind that if you also use Variables within any message content payload, you will need to ensure that the variable tags do not conflict with the special characters. For example, if you have a variable for `name`, such as `<<name>>`, then the message would be `"Welcome to {{Sangre de Christo}}, <<name>>, we hope you enjoy your stay."` and confirm that `{{` and `}}` are not used as variables within the job processing.
### Endpoint Status Codes
| Status Code | Description |
| ------------------------- | ---------------------------------------- |
| 200 OK | The request was successful and the response body contains the representation requested. |
| 400 BAD REQUEST | The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications. |
| 401 UNAUTHORIZED | The supplied credentials, if any, are not sufficient to access the resource. |
| 403 FORBIDDEN | The requested resource is not available for your level of access. |
| 404 NOT FOUND | The requested URL or resource was not found. |
| 422 UNPROCESSABLE ENTRY | Invalid data was present in your request. The error message will have specific details on the nature of the problem. |
| 500 INTERNAL SERVER ERROR | The server encountered an unexpected condition that prevented it from fulfilling the request. The error message will have specific details on the nature of the problem. |
### Endpoint Response Format
Job Creation `[POST]` endpoints respond with a JSON `payloadResponse` structure and may contain the jobid (if known). Error message detail will be present in the `errormessage` key of the response payload. The HTTP status code is set appropriately.
Success Example:
```json
{
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"payloadResponse": {
"valid": true,
"message": ":Job successfully added to database:",
"errormessage": null
}
}
```
Error Example:
```json
{
"jobid": null,
"payloadResponse": {
"valid": false,
"message": "Bad Payload",
"errormessage": ":Error converting TTS for body.messagecontent:"
}
}
```
Job Reporting/Change `[GET] [PUT] [DELETE]` endpoints respond with a JSON `payloadValidation` structure and response specific payload. The HTTP status code is set appropriately.
Success Example:
```json
{
"payloadValidation": {
"valid": true,
"message": "Success"
}
}
```
Error Example:
```json
{
"payloadValidation": {
"valid": false,
"message": "Issue Getting Job",
"errormessage": "Missing Required JobID"
}
}
```
## CURL examples to Message API implementing Basic Auth or APIKEY
### APIKEY Authentication Example
```bash
curl -X GET \
https://message.ivrnet.com/api/v1/text/0a39d4f6-e341-4e72-a2b5-892d8d47fb59 \
-H "X-API-KEY: fmq4hkWZvvA88HXowoBK7UQTnGQ0w1WeCXxQUddSoME="
```
### BASIC AUTH Authentication Example
```bash
curl -X GET \
https://message.ivrnet.com/api/v1/text/0a39d4f6-e341-4e72-a2b5-892d8d47fb59 \
-H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
```
## Jobs
The message object has the following properties, shared across all voice/text/email jobs.
* All properties are `STRING` data type, unless otherwise noted.
* Any Property with * is required.
| Primary Property | Description |
| ---------------- | ----------- |
| job | The (optional) job setup |
| to * | The intended recipient(s) of the message |
| from * | The sender of the message (short code/long code/voice/email sender) |
| body * | The content of the message |
| schedule * | The schedule to follow |
### Job Object
| Primary Property | Secondary Property | Description |
| ---------------- | ------------------ | ----------- |
| name | | The (optional) name of the job. Default will be the job UUID once created |
| metadata | | The (optional) key:value pairs of any data that needs to be returned in reporting for the job. This is useful for correlating job data to your internal systems |
| | `"key1":"value1","key2":"value2"` | Comma separated key:value pairs, dynamic key naming convention |
| options | | The (optional) job options structure |
| | webhook | see `Setting Webhooks` below for details |
### From Object
| Primary Property | Description |
| ---------------- | ----------- |
| resource * | The resource the message is sent from (must be registered with Ivrnet). For example: sending email address, sending 10DLC, sending Voice number |
| resourcename | The (optional) name to assign to the resource |
### To Object
| Primary Property | Secondary Property | Description |
| -- | -- | -- |
| resourceid | | The (optional) identifier from the calling system associated to this resource |
| resource * | | The required resource to send the message to (recipient). For example: email address, mobile number, phone number |
| resourcename | | The (optional) name to assign to the resource |
| languagename * | | The required IETF language tag for this recipient and must match any `body.message` language code |
| variables | | This is required only if `body.message` contains the same variable tags for substitution |
| | `"<<var1>>":"value1","<<var2>>":"value2"` | Comma separated variable:value pairs, where the variable is the key for substitution with it's value across any messages that use the same named variable |
| metadata | | The (optional) key:value pairs of any data that needs to be return in reporting for the recipient |
| | `"key1":"value1","key2":"value2"` | Comma separated key:value pairs, dynamic key naming convention. This is useful for correlating recipients to your internal systems |
### Body Object
| Primary Property | Secondary Property | Tertiary Property | Description |
| -- | -- | -- | --|
| message * | | | The primary content of the message. May contain (optional) variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags. All variables within the value MUST have corresponding tags in the list of TO recipients for the same IETF language code. |
| options | | | The (optional) body options structure. See `options` from each respective POST endpoint for details on usage |
| | messagevariabletags | | Used to denote any variables within the message for substitution |
| | | opentag | defines the opentag for variables, ie: `<<` |
| | | closetag | defines the closetag for variables, ie: `>>` |
### Schedule Object
| Primary Property | Description |
| ----------------- | ---------------------------------------- |
| priority | The (optional) priority of the message to be sent (`emergency`, `urgent`, `normal`, `low`, `test`). Default priority is `normal`, and `test` will NOT send the job's messages |
| start * | ISO8601 formatted string for datetime using UTC, the start time to begin the job from UTC. Format `yyyy-mm-ddThh:mmZ` or `yyyy-mm-ddThh:mm:ssZ` |
| end * | ISO8601 formatted string for datetime using UTC, the end time to stop the job from UTC. Format `yyyy-mm-ddThh:mmZ` or `yyyy-mm-ddThh:mm:ssZ` |
| retryattempts | `INTEGER` the number of times to retry sending the message to the recipient on Failed attempts (default is 3) |
| retryinterval | `INTEGER` the number of minutes in between each retryattempt (default is 15) |
### Setting Webhooks in `job.options`
Webhooks are an (optional) setup in the job.options structure as follows
| Primary Property | Secondary Property | Tertiary Property | Description |
| ----------------- | ------------------ | ----------------- | ----------- |
| webhook * | | | The webhook construct |
| | url * | | The webhook URL to send data TO |
| | auth | | The (optional) authorization for the url (if applicable), empty or basic auth or apikey |
| | | `"apikey": "<apikey>"`| if apikey, the Header `X-API-KEY` is set with the value you provide |
| | | `"username": "<username>", "password": "<password>"`| if basic auth, send the username and password as key:value pairs, and the api will base64 encode username:password and set the `Authorization` Header |
| | | `"basicauth": "<AuthorizationHeaderValue>"`| if basic auth, you may provide the `Authorization` Header value directly |
| | | | if empty, the payload is sent without auth |
| | method | | The method used to send the payload to your api. See `Receiving Webhooks` for information on parameters. Default: POST, options: POST/GET/PUT |
| | retry | | The (optional) persistence model for best effort to guarantee delivery, using attempts (Default 3) and interval in seconds (Default 15) combination |
| | | attempts | `INTEGER` value (default 0, cannot be larger than 100) |
| | | interval | `INTEGER` value in seconds (default 0) |
| | | interval_list | (optional) list of `INTEGER` values in seconds (specialized for staggered retry intervals). This list will override `interval` but you must not include `interval` in the payload |
| | | statuscode_list | (optional) list of http status codes in `INTEGER` values where a retry will occur, default values [500, 502, 503, 504] |
| types * | | | Types of webhooks, options: |
| | jobstatus | | When a job status changes. Keys are `scheduled`, `running`, `completed`, `expired` and must contain a `BOOLEAN` value to determine applicability |
| | responsevariables | | When a user provides input as a response to receiving a message. (Not Applicable to Email at the time of this writing) |
| | | | - each responsevariable key defined here must have a setup in the `body.options.responsevariables` and must contain a `BOOLEAN` value to determine applicability |
| | | | - <u><b>IMPORTANT for TEXT</b></u>: if you wish to receive the entire message from the recipient (other than the closed ended variables), you must include a key:value pair of `"MESSAGE": true`. FULL TEXT MESSAGES will be sent to your webhook endpoint until the to.resource receives a new message associated to another job using the same from.resource |
##### Example of Webhook Request Retry Object using static values
This example will retry a webhook up to 3 times and 15 seconds apart
```json
"retry": {
"attempts": 3,
"interval": 15
}
```
##### Example of Webhook Request Retry Object using custom defined staggered values
This example will retry a webhook up to 3 times, the first retry at 15 seconds, the second retry at 45 seconds after the first, and the third retry at 90 seconds after the second
```json
"retry": {
"attempts": 3,
"interval_list": [15, 45, 90]
}
```
##### Example of Webhook Request Retry Object using specialized status code values
This example will retry only on HTTP Status Code 500
```json
"retry": {
"attempts": 3,
"interval_list": [15, 45, 90],
"statuscode_list": [500]
}
```
##### Example Webhook Request Body using Basic Auth (Authorization header)
```json
{
"job": {
"options": {
"webhook": {
"url": "https://your_webhook.com/path/to/resource/",
"auth": {
"basicauth": "eW91cl9hcHBsaWNhdGlvbl91c2VybmFtZTp5b3VyX2FwcGxpY2F0aW9uX3Bhc3N3b3JkCg=="
},
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": true,
"running": true,
"completed": true,
"expired": true
},
"responsevariables": {
"1": true,
"2": true,
"MESSAGE": true
}
}
}
}
}
}
```
- Ivrnet webhooks will execute the following (see `Receiving Webhooks` below for -d body content)
```bash
curl -X POST \
https://your_webhook.com/path/to/resource/ \
-H "Authorization: eW91cl9hcHBsaWNhdGlvbl91c2VybmFtZTp5b3VyX2FwcGxpY2F0aW9uX3Bhc3N3b3JkCg==" \
-H "Content-Type: application/json" \
-d '{"body content"}'
```
##### Example Webhook Request Body using Basic Auth (username and password)
```json
{
"job": {
"options": {
"webhook": {
"url": "https://your_webhook.com/path/to/resource/",
"auth": {
"username": "your_application_username",
"password": "your_application_password"
},
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": true,
"running": true,
"completed": true,
"expired": true
},
"responsevariables": {
"1": true,
"2": true,
"MESSAGE": true
}
}
}
}
}
}
```
- Ivrnet webhooks will execute the following (see `Receiving Webhooks` below for -d body content)
```bash
curl -X POST \
https://your_webhook.com/path/to/resource/ \
-H "Authorization: eW91cl9hcHBsaWNhdGlvbl91c2VybmFtZTp5b3VyX2FwcGxpY2F0aW9uX3Bhc3N3b3JkCg==" \
-H "Content-Type: application/json" \
-d '{"body content"}'
```
##### Example Webhook Request Body using APIKEY Auth
```json
{
"job": {
"options": {
"webhook": {
"url": "https://your_webhook.com/path/to/resource/",
"auth": {
"apikey": "your_application_apikey"
},
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": true,
"running": true,
"completed": true,
"expired": true
},
"responsevariables": {
"1": true,
"2": true,
"MESSAGE": true
}
}
}
}
}
}
```
- Ivrnet webhooks will execute the following (see `Receiving Webhooks` below for -d body content)
```bash
curl -X POST \
https://your_webhook.com/path/to/resource/ \
-H "X-API-KEY: your_application_apikey" \
-H "Content-Type: application/json" \
-d '{"body content"}'
```
##### Example Webhook Request Body with NO Auth
```json
{
"job": {
"options": {
"webhook": {
"url": "https://your_webhook.com/path/to/resource/",
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": true,
"running": true,
"completed": true,
"expired": true
},
"responsevariables": {
"1": true,
"2": true,
"MESSAGE": true
}
}
}
}
}
}
```
- Ivrnet webhooks will execute the following (see `Receiving Webhooks` below for -d body content)
```bash
curl -X POST \
https://your_webhook.com/path/to/resource/ \
-H "Content-Type: application/json" \
-d '{"body content"}'
```
### Receiving Webhooks
#### Job Status Changes
- Reference: `job.options.webhook.types.jobstatus`
Webhook Payload will be delivered to the URL endpoint in the following JSON format
##### Example Job Webhook Body Delivered to your endpoint
This JSON payload is an example of a Job Complete webhook
```json
{
"eventtype": "Completed",
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"jobtype": "text",
"jobmetadata": {
"key1": "value1",
"key2": "value2"
}
}
```
This is the Query Params example of a Job Complete webhook.
* NOTE: metadata key:value pairs are NOT delivered on GET, as the metadata may be too numerous and it is best practice to keep URL length to less than 2,000 characters. When you receive this webhook, you may proceed to call the [GET] endpoint for the same communication type and the corresponding jobid to retrieve the full report (including metadata).
```
https://your_webhook.com/path/to/resource?jobid=0a39d4f6-e341-4e72-a2b5-892d8d47fb59&jobtype=text&eventtype=Completed
```
#### Recipient Input
- Reference: `job.options.webhook.types.responsevariables`
Webhook Payload will be delivered to the URL endpoint in the following JSON format
##### Example Job/Recipient Webhook Body Delivered to your endpoint
This JSON payload is an example of a recipient in the `to` list responding with a message back to the platform, and the message itself is set up as valid input from the `job.options.webhook.responsevariables` list that holds a `true` value for the response variable `key`. In this case, the response input from the recipient was `1` and the eventtype is set to `confirm` (which is the value of the key listed in `body.options.responsevariables` from below text example payload)
```json
{
"eventtype": "confirm",
"eventvalue": "1",
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"jobtype": "text",
"jobmetadata": {
"key1": "value1",
"key2": "value2"
},
"resource": "4035551234",
"resourceid": "eb91a628-2e01-4f04-a96e-57b83fb9bf43",
"resourcemetadata": {
"key1": "value1",
"key2": "value2"
}
}
```
This is the Query Params example of a Job Recipient webhook
* NOTE: metadata key:value pairs are NOT delivered on GET, as the metadata may be too numerous and it is best practice to keep URL length to less than 2,000 characters. When you receive this webhook, you may proceed to call the [GET] endpoint for the same communication type and the corresponding jobid to retrieve the full report (including metadata).
```
https://your_webhook.com/path/to/resource?jobid=0a39d4f6-e341-4e72-a2b5-892d8d47fb59&jobtype=text&eventtype=confirm&eventvalue=1&resource=4035551234&resourceid=eb91a628-2e01-4f04-a96e-57b83fb9bf43
```
This JSON payload is an example of a recipient in the `to` list responding with a message back to the platform, and the message itself is set up as valid input from the `job.options.webhook.responsevariables` list that holds a `true` value for the response variable `key`. In this case, the response input from the recipient was `This is the full message that a user may reply with` and the eventtype is set to `MESSAGE` (which is the value of the key listed in `body.options.responsevariables` from below text example payload).
```json
{
"eventtype": "MESSAGE",
"eventvalue": "This is the full message that a user may reply with",
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"jobtype": "text",
"jobmetadata": {
"key1": "value1",
"key2": "value2"
},
"resource": "4035551234",
"resourceid": "eb91a628-2e01-4f04-a96e-57b83fb9bf43",
"resourcemetadata": {
"key1": "value1",
"key2": "value2"
}
}
```
This is the Query Params example of a Job Recipient webhook
* NOTE: metadata key:value pairs are NOT delivered on GET, as the metadata may be too numerous and it is best practice to keep URL length to less than 2,000 characters. When you receive this webhook, you may proceed to call the [GET] endpoint for the same communication type and the corresponding jobid to retrieve the full report (including metadata).
```
https://your_webhook.com/path/to/resource?jobid=0a39d4f6-e341-4e72-a2b5-892d8d47fb59&jobtype=text&eventtype=MESSAGE&eventvalue=This%20is%20the%20full%20message%20that%20a%20user%20may%20reply%20with&resource=4035551234&resourceid=eb91a628-2e01-4f04-a96e-57b83fb9bf43
```
## Job Creation API Endpoints
Properties marked with * are required.
### [POST] /voice/
Create a Job to send a voice message or messages.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| job.name | (optional) name of the job. Default will be the job UUID once created |
| job.metadata | (optional) key:value pairs of any data that needs to be returned in reporting |
| job.options.webhook | (optional) |
| from * | Registered Number with Ivrnet that the message should be sent from |
| from.resource * | Registered Number with Ivrnet that the message should be sent from |
| from.resourcename | (optional) caller name display to use when dialing |
| to [] * | The array of recipient(s) for the message to be sent to |
| to.resourceid | (optional) identifier from the calling system associated to this resource |
| to.resource * | The required phone number of the recipient to dial |
| to.resourcename | (optional) name to assign to the resource |
| to.languagename * | The IETF language tag for this recipient and must match the `body.message` language code |
| to.variables | The variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags that will be used to replace the `body.message` tags. This is required only if `body.message` contains the same tags intended for variable substitution. |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| to.metadata | (optional) key:value pairs of any data that needs to be returned in reporting |
| body * | The content of the message |
| body.message * | used for Live Answer and Voice Mail messages, using the list of IETF language tags and their respective messages. May contain (optional) variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags. All variable tags in this value MUST have corresponding tags in the list of TO recipients for the same IETF language code. |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| | if the result of Answering Machine Detection (AMD) results as being determined to be voicemail, and if `body.voicemailmessage` is not used (below), then this message is played once and then hangs up. No other options are available |
| body.voicemailmessage | (optional) Will override the `body.message` when AMD results as being determined to be voicemail, using the list of IETF language tags and their respective messages. May contain (optional) variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags. All variable tags in this value MUST have corresponding tags in the list of TO recipients for the same IETF language code. |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| | if the result of Answering Machine Detection (AMD) results as being determined to be voicemail, then this message is played once and then hangs up. No other options are available |
| body.options.messagetype | (optional) hybrid/tts/voice (default is "tts") |
| | TTS is used exclusively for converting Text to Speech, based on the message and the associated IETF language tag |
| | Voice is used exclusively when referencing voice files from your library using the {fileid:<<fileid>>} construct inline |
| | Hybrid is used when there may be a mix of TTS and Voice. It may also be used for TTS only or Voice only |
| body.options.messagevariabletags | `opentag` and `closetag` are the set of keys you must provide, with the associated values in string format. Required if there are variables within the body.message or body.voicemailmessage payload that must match all variables |
| body.options.ttsmessagevoice | (optional) `female` or `male` voice (where available), default is `male` |
| body.options.responsemessageprompt | (optional) Based on IETF language tag, this is the prompt to the recipient, asking for response input |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| body.options.responsevariables | (optional) the list of accepted responses, the call will hang up unless there are actions are defined below. This should be in place if a `body.options.responsemessageprompt` is used |
| body.options.responsesuccessmessage | ((optional), VOICE only) can also use responsevariablesmessages for more customization, otherwise this is the success message sent to the recipient using the IETF language tag if the recipient inputs one of the responsevariables |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| body.options.responseerrormessage | ((optional), VOICE only) the error message sent to the recipient using the IETF language tag if the recipient did not correctly input one of the responsevariables |
| body.options.responsevariablesactions | ((optional), VOICE only) the list of actions to perform based on the response. Accepted values are: `HangUp`, `Transfer`, `Jump` |
| | Hangup: Hangs up the call and ((optional)ly) play a message based on `body.options.hangupmessage` generic message, or from `body.options.responsevariablesmessages` for the same key |
| | Transfer: Retrieves the `body.options.responsevariables` value for the same key as the Number to Transfer to as a SIP Refer, and ((optional)ly) play a message based on `body.options.transfermessage` generic message, or from `body.options.responsevariablesmessages` for the same key |
| | Jump: Jumps the call to the start of the `body.options.responsemessageprompt` and ((optional)ly) play a message based on `body.options.jumpmessage` generic message, or from `body.options.responsevariablesmessages` for the same key |
| body.options.responsevariablesmessages | (optional) the list of custom messages to playback to the user based on the response and IETF Language tag |
| | must contain languagetag, followed by a struct containing the k:v pairs of DTMF responses:Custom message
| body.options.hangupmessage | (optional) generic message for any hangup action |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| body.options.transfermessage | (optional) generic message for any transfer action |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| body.options.jumpmessage | (optional) generic message for any jump action |
| | see `Voice Files Management` reference for creating voice files to be used within a message (in this case, as the value of a variable to be substituted), and subsequently referenced by using the following construct inline `"{fileid:<fileid>}"` to insert a voice file from your library. If a voice file is used, set the `body.options.messagetype` to `hybrid` |
| body.schedule * | the scheduling and priority |
| body.schedule.priority | Used to assign priority of the message to be sent (`emergency`, `urgent`, `normal`, `low`, `test`). Default priority is `normal`. |
| body.schedule.start * | ISO8601 formatted string for datetime using UTC, the start time to begin the job from UTC. Format `yyyy-mm-ddThh:mmZ` |
| body.schedule.end * | ISO8601 formatted string for datetime using UTC, the end time to stop the job from UTC. Format `yyyy-mm-ddThh:mmZ` |
| body.schedule.retryattempts | `INTEGER` the number of times to retry dialing the recipient on No Answer/Busy (default is 1) |
| body.schedule.retryinterval | `INTEGER` the number of minutes in between each retryattempt (default is 15) |
##### Example Request Body
```json
{
"job": {
"name": "Test Job",
"metadata": { "key1": "value1", "key2": "value2" },
"options": {
"webhook": {
"url": "https://your_webhook.com/",
"auth": {
"username": "user",
"password": "pass"
},
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": false,
"running": false,
"completed": true,
"expired": false
},
"responsevariables": {
"1": true,
"2": true
}
}
}
}
},
"from": {
"resource": "4035551234",
"resourcename": "Ivrnet"
},
"to": [
{
"resourceid": "eb91a628-2e01-4f04-a96e-57b83fb9bf43",
"resource": "4035555335",
"resourcename": "Person 1",
"languagename": "en-us",
"variables": {
"<<name>>": "Test User",
"<<date>>": "April 23, 2024"
},
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
],
"body": {
"message": {
"en-us": "{file:d462c033-4b00-45dd-a989-66f05e304525} <<name>> you have an appointment on <<date>>"
},
"voicemailmessage": {
"en-us": "{file:d462c033-4b00-45dd-a989-66f05e304525} <<name>> you have an appointment on <<date>>"
},
"options": {
"messagetype": "tts",
"messagevariabletags":
{
"opentag": "<<",
"closetag": ">>"
},
"ttsmessagevoice": "female",
"responsemessageprompt": {
"en-us": "Press 1 to confirm, 2 to cancel or 9 to transfer your call to the contact centre"
},
"responsesuccessmessage": {
"en-us": "Ok, now"
},
"responseerrormessage": {
"en-us": "Sorry, this is an incorrect option."
},
"responsevariables": {
"1": "confirm",
"2": "cancel",
"9": "4035551234"
},
"responsevariablesactions": {
"1": "HangUp",
"2": "HangUp",
"9": "Transfer"
},
"responsevariablesmessages": {
"en-us": {
"1": "Thankyou for confirming",
"2": "Your appointment will be cancelled",
"9": "Please wait while we transfer your call"
}
},
"hangupmessage":
"en-us": "Good bye"
},
"transfermessage": {
"en-us": "Transferring"
},
"jumpmessage": {
"en-us": "Ok, let's try that again"
}
}
},
"schedule": {
"start": "2024-05-02T16:00Z",
"end": "2024-05-03T16:00Z",
"retryattempts": 1,
"retryinterval": 30,
"priority": "normal"
}
}
```
### [POST] /email/
Create a Job to send an email message or messages.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| job.name | The (optional) name of the job. Default will be the job UUID once created |
| job.metadata | (optional) key:value pairs of any data that needs to be returned in reporting |
| job.options.webhook | (optional) |
| from * | Registered email with Ivrnet that the message should be sent from |
| from.resource * | Registered email with Ivrnet that the message should be sent from |
| from.resourcename | (optional) email sender name |
| to [] * | The array of recipient(s) for the message to be sent to |
| to.resourceid | The (optional) identifier from the calling system associated to this resource |
| to.resource * | The required email address of the person to send the message to |
| to.resourcename | The (optional) name to assign to the resource |
| to.languagename * | The IETF language tag for this recipient and must match the `body.message` language code |
| to.variables | The variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags that will be used to replace the `body.message` tags. This is required only if `body.message` contains the same tags. |
| to.metadata | (optional) key:value pairs of any data that needs to be return in reporting |
| body * | The content of the message |
| body.message * | The list of IETF language tags and their respective messages. May contain (optional) variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags. All variable tags within the value MUST have corresponding tags in the list of TO recipients for the same IETF language code. |
| body.subject * | The list of IETF language tags and their respective email subject (max 256 characters). May contain (optional) variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag tags`. All variable tags within the value MUST have corresponding tags in the list of TO recipients for the same IETF language code.|
| body.options.languagetranslate | `BOOLEAN` (optional) `true` or `false` (default false), used to convert the messages in the payload to associated IETF language tag of each message |
| body.options.messagetype | `plaintext` or `html` (default is plaintext)|
| body.options.messagevariabletags | `opentag` and `closetag` are the set of keys you must provide, with the associated values in string format. Required if there are variables within the payload that must match all variables |
| body.options.emailattachments | List of email attachments `"filename":"<filename.extension>", "filebase64data": "<base64 encoded file>"` }` |
| | * filetypes permitted: pdf, doc(x), xls(x), ppt(x), csv, png, jpeg, gif, txt, rtf |
| body.schedule * | the scheduling and priority |
| body.schedule.priority | Used to assign priority of the message to be sent (`emergency`, `urgent`, `normal`, `low`, `test`). Default priority is `normal`. |
| body.schedule.start * | ISO8601 formatted string for datetime using UTC, the start time to begin the job from UTC. Format `yyyy-mm-ddThh:mmZ` |
| body.schedule.end * | ISO8601 formatted string for datetime using UTC, the end time to stop the job from UTC. Format `yyyy-mm-ddThh:mmZ` |
| body.schedule.retryattempts | `INTEGER` the number of times to retry dialing the recipient on No Answer/Busy (default is 1) |
| body.schedule.retryinterval | `INTEGER` the number of minutes in between each retryattempt (default is 15)
##### Example Request Body
```json
{
"job": {
"name": "Test Job",
"options": {
"webhook": {
"url": "https://your_webhook.com/",
"auth": {
"username": "user",
"password": "pass"
},
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": true,
"running": true,
"completed": true,
"expired": true
}
}
}
}
},
"from": {
"resource": "noreply@ivrnet.com",
"resourcename": "Ivrnet"
},
"to": [
{
"resourceid": "eb91a628-2e01-4f04-a96e-57b83fb9bf43",
"resource": "test@email.com",
"resourcename": "Person 1",
"languagename": "en-us",
"variables": {
"<<name>>": "Test User",
"<<date>>": "April 23, 2024"
},
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
],
"body": {
"message": {
"en-us": "Hello <<name>> you have an appointment on <<date>>"
},
"subject": {
"en-us": "Appointment confirmation for <<name>>"
},
"options": {
"messagetype": "html",
"languagetranslate": false,
"emailattachments": [
{
"filename": "attachment.pdf",
"filebase64data": "<replace with base64 encoded file>"
}
],
"messagevariabletags":
{
"opentag": "<<",
"closetag": ">>"
}
}
},
"schedule": {
"start": "2024-05-02T16:00Z",
"end": "2024-05-03T16:00Z",
"retryattempts": 1,
"retryinterval": 30,
"priority": "normal"
}
}
```
### [POST] /text/
Create a Job to send a text message or messages.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| job.name | The (optional) name of the job. Default will be the job UUID once created |
| job.metadata | (optional) key:value pairs of any data that needs to be returned in reporting |
| job.options.webhook | (optional) |
| from * | Registered 10DLC or ShortCode with Ivrnet that the message should be sent from |
| from.resource * | Registered 10DLC or ShortCode with Ivrnet that the message should be sent from |
| to [] * | The array of recipient(s) for the message to be sent to |
| to.resourceid | The (optional) identifier from the calling system associated to this resource |
| to.resource * | The required Mobile number of the recipient to send the message to |
| to.resourcename | The (optional) name to assign to the resource |
| to.languagename * | The IETF language tag for this recipient and must match the `body.message` language code |
| to.variables | The variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags that will be used to replace the `body.message` tags. This is required only if `body.message` contains the same tags. |
| to.metadata | (optional) key:value pairs of any data that needs to be return in reporting |
| body * | The content of the message |
| body.message * | The list of IETF language tags and their respective messages. May contain (optional) variables denoted with `body.options.messagevariabletags.opentag` and `body.options.messagevariabletags.closetag` tags. All variable tags within the value MUST have corresponding tags in the list of TO recipients for the same IETF language code. |
| body.options.languagetranslate | `BOOLEAN` (optional) true/false (default false), used to convert the messages in the payload to associated IETF language tag of each message |
| body.options.messagevariabletags | `opentag` and `closetag` are the set of keys you must provide, with the associated values in string format. Required if there are variables within the payload that must match all variables |
| body.options.responsevariables | (optional) the list of accepted responses. This should be in place if a `body.options.responsemessageprompt` is used |
| body.options.responsevariablesmessages | (optional) the list of custom messages to playback to the user based on the response |
| body.schedule * | the scheduling and priority |
| body.schedule.priority | Used to assign priority of the message to be sent (`emergency`, `urgent`, `normal`, `low`, `test`). Default priority is `normal`. |
| body.schedule.start * | ISO8601 formatted string for datetime using UTC, the start time to begin the job from UTC. Format `yyyy-mm-ddThh:mmZ` |
| body.schedule.end * | ISO8601 formatted string for datetime using UTC, the end time to stop the job from UTC. Format `yyyy-mm-ddThh:mmZ` |
| body.schedule.retryattempts | `INTEGER` the number of times to retry sending a message the recipient on Failed attempts (default is 0) |
| body.schedule.retryinterval | `INTEGER` the number of minutes in between each retryattempt (default is 15)
##### Example Request Body
```json
{
"job": {
"name": "Test Job",
"options": {
"webhook": {
"url": "https://your_webhook.com/",
"auth": {
"username": "user",
"password": "pass"
},
"method": "POST",
"retry": {
"attempts": 3,
"interval": 15
},
"types": {
"jobstatus": {
"scheduled": false,
"running": false,
"completed": true,
"expired": false
},
"responsevariables": {
"1": true,
"2": true,
"MESSAGE": true,
"STOP": true
}
}
}
}
},
"from": {
"resource": "4035556789"
},
"to": [
{
"resourceid": "eb91a628-2e01-4f04-a96e-57b83fb9bf43",
"resource": "4035551234",
"resourcename": "Person 1",
"languagename": "en-us",
"variables": {
"<<name>>": "Test User",
"<<date>>": "April 23, 2024"
},
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
],
"body": {
"message": {
"en-us": "Hello <<name>> you have an appointment on <<date>>"
},
"options": {
"languagetranslate": false,
"messagevariabletags":
{
"opentag": "<<",
"closetag": ">>"
},
"responsemessageprompt": {
"en-us": "Please confirm with 1 or 2 to cancel, or STOP to unsubscribe"
},
"responsesuccessmessage": {
"en-us": "Thank you"
},
"responseerrormessage": {
"en-us": "Sorry, this is an incorrect option"
},
"responsevariables": {
"1": "confirm",
"2": "cancel",
"MESSAGE": "full message",
"STOP" : "unsubscribe"
},
"responsevariablesmessages": {
"en-us": {
"1": "Thank you for confirming your appointment",
"2": "OK, please call us back to rebook",
"MESSAGE": "Thank you for your message, we will respond shortly",
"STOP": "You have been unsubscribed. Reply START to resubscribe"
}
}
}
},
"schedule": {
"start": "2024-05-02T16:00Z",
"end": "2024-05-03T16:00Z",
"retryattempts": 1,
"retryinterval": 30,
"priority": "normal"
}
}
```
##### Response
```json
{
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"payloadResponse": {
"valid": true,
"message": "Job successfully added to database."
}
}
```
### [POST] /text/msg/recipient/
Using an existing JobID, send a follow up message to a recipient from the job. Any existing webhooks that were previously setup for the JobID will remain.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| jobid | The required jobid |
| resource | The required mobile phone number from the previous job (to.resource) |
| message | The required message to send |
##### Example Request Body
```json
{
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"resource": "4035551234",
"message": "This is a follow up message"
}
```
##### Response
```json
{
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"payloadResponse": {
"valid": true,
"message": "Message Sent"
}
}
## Voice TTS supported
### [GET] /voice/ttslist/
### Response Format
JSON Array Listing of language names, indicating male/female voice availability
```json
[
{
"language": "en-US",
"male": true,
"female": true
}
]
```
## Voice Files Management
### [POST] /voice/audio/accesscode/
Create a file placeholder for a future voice message with temporary accesscode.
After creation, dial the tollfree number and enter the accesscode to validate, and then follow the prompts to complete the process.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| filename | The Required file name |
##### Example Request Body
```json
{
"filename": "Test File for a Job"
}
```
##### Response
```json
{
"tollfree": "8001234567",
"accesscode": "1234"
}
```
### [GET] /voice/audio/list/accesscode/
Get a list of incomplete audio files with access codes that have not expired
##### Response
```json
[
{
"tollfree": "8001234567",
"accesscode": "1234",
"filename": "Test File for Voice Job"
}
]
```
### [GET] /voice/audio/list/
Get a list of all completed audio files associated to your account
##### Response
```json
[
{
"fileid": "d462c033-4b00-45dd-a989-66f05e304525",
"filename": "Test File for a Job"
}
]
```
### [GET] /voice/audio/file/`fileid`
Get the file as application/octet-stream
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| fileid | The Required fileid |
| | fileid can be used for substitution on any voice job messaging, and would be referenced inline as `{file:'fileid'}`|
##### Example Request Parameters
```CURL
https://message.ivrnet.com/api/v1/voice/audio/file/d462c033-4b00-45dd-a989-66f05e304525
```
### [DELETE] /voice/audio/file/`fileid`
Remove a voice message file from your account
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| fileid | The Required fileid |
##### Example Request Parameters
```
https://message.ivrnet.com/api/v1/voice/audio/file/d462c033-4b00-45dd-a989-66f05e304525
```
## Job Actions
### [PUT] /voice/
### [PUT] /text/
### [PUT] /email/
Pause or Resume the Job.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| jobid | The Required Jobid |
| action | Either `pause` or `resume` |
##### Example Request Body
```json
{
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"action": "resume"
}
```
##### Example Response Body
```json
{
"payloadValidation": {
"valid": true,
"message": "Job Resumed Success",
"errormessage": null
}
}
```
### [DELETE] /voice/`jobid`
### [DELETE] /text/`jobid`
### [DELETE] /email/`jobid`
Cancel the Job.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| jobid | The Required Jobid |
##### Example Request Parameters
```
https://message.ivrnet.com/api/v1/email/0a39d4f6-e341-4e72-a2b5-892d8d47fb59
```
##### Example Response Body
```json
{
"payloadValidation": {
"valid": true,
"message": "Job Deleted Success",
"errormessage": null
}
}
```
## Email Bounce List Management
### [DELETE] /email/bouncelist/cleardomain/ by domain
Clear the entire bouncelist by domain that is registered to your account
### [DELETE] /email/bouncelist/clearlist/ by domain and list of email addresses
Clear the bouncelist by domain and list of email addresses
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| to | list|
| resource | the email address |
##### Example Request Body
```json
{
"to":
[
{
"resource": "email@domain.com",
}
]
}
```
## Text Opt-In List Management
### [POST] /text/optin/
Upload a list of mobile numbers to the opt-in list for your account and remove them from the Deactivation list (where applicable) provided by the Mobile Network Operators
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| to | list of "resource":"value" pairs|
| resource | the mobile number |
##### Example Request Body
```json
{
"to":
[
{
"resource": "4035551212",
}
]
}
```
## Job Listing
### [GET] /voice/list/?page=`page`&pagesize=`pagesize`&status=`status`
### [GET] /text/list/?page=`page`&pagesize=`pagesize`&status=`status`
### [GET] /email/list/?page=`page`&pagesize=`pagesize`&status=`status`
Query the Job List.
##### Parameters
| Property | Description |
| --------- | ---------------------------------------- |
| page | (optional) Numeric page number (default 1) |
| pagesize | (optional) Numeric page size (default 10) |
| status | (optional) One of `scheduled`,`running`,`paused`,`cancelled`,`complete`,`expired`,`all` (default all) |
##### Example Request Parameters
```
https://message.ivrnet.com/api/v1/email/list/?page=1&pagesize=10&status=all
```
##### Response
```json
[
{
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"name": "Test",
"status": "Complete",
"createdat": "2024-06-07T09:36:10Z",
"deletedat": null
}
]
```
## Job Reporting
### [GET] /voice/`jobid`
### [GET] /text/`jobid`
### [GET] /email/`jobid`
Query the Job. The `to` list of recipients has an extended `disposition` structure added to reflect the state of each `to` recipient within the job. Additionally, a `messaging` structure includes a list of messages to and from the recipient is included to demonstrate any back and forth communication (text only).
### [GET] /voice/`jobid`/`to_id`
### [GET] /text/`jobid`/`to_id`
### [GET] /email/`jobid`/`to_id`
Query the Job and Recipient.
##### Parameters
| Property | Description |
| ----- | ----- |
| jobid | The Required jobid |
| to_id | Required when specific to a recipient report (reference to.id from primary [GET] /{jobid} report) |
##### Response
The "disposition" structure details the initial state of the communication to the recipient
The "messaging" structure details a list of the messages and the direction (text)
The "disposition" and "messaging" structures have the following format:
| Property | Description |
| --------- | ---------------------------------------- |
| disposition | The most recent outbound communication to the recipient |
| disposition.start | Start time of the communication in UTC. Format `yyyy-mm-ddThh:mm:ssZ` |
| disposition.duration | `INTEGER` the duration of the total communication in seconds (voice) |
| disposition.status | Delivery status of the communication |
| disposition.responseinput | * DEPRECATED, all recipient responses are included in the `messaging` list of communications * |
| disposition.errormessage | Error message of the communication (if applicable) with details |
| messaging | List of communications |
| messaging.direction | `in` means the recipient provided a response to the message, `out` means the system sent a message to the recipient |
| messaging.message | the message |
| messaing.processed | the timestamp the message was processed in UTC. Format `yyyy-mm-ddThh:mm:ssZ` |
| messaging.status | the status of the message |
```json
"disposition": {
"start": "2024-05-02T16:00:00Z",
"status": "delivered",
"duration": 0,
"responseinput": null,
"errormessage": null
},
"messaging": [
{
"direction": "out", "message": "Respond with 1 to confirm", "processed": "2024-05-02T16:01:15Z", "status": "delivered"
},
{
"direction": "in", "message": "1", "processed": "2024-05-02T16:04:11Z", "status" : "received"
}
]
```
#### Disposition status types
| Job Type | Working State | Final State |
| ----- | ----- | ----- |
| Voice | calling | No Answer, No Connect, Voice Mail, Connected |
| Email | pending, running | completed, failed |
| Text | installing, processing, queued | delivered, failed, sent, rejected |
##### Example Request Parameters
```
https://message.ivrnet.com/api/v1/text/0a39d4f6-e341-4e72-a2b5-892d8d47fb59
```
##### Response
```json
{
"payloadValidation": {
"valid": true,
"message": "Success"
},
"job": {
"jobid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"name": "Text Job Name",
"status": "Complete",
"createdat": "2024-05-01T15:48:10Z",
"deletedat": null,
"metadata": {
"key1": "value1",
"key2": "value2"
},
"errors": {}
},
"schedule": {
"start": "2024-05-02T16:00Z",
"end": "2024-05-03T21:00Z",
"retryattempts": 1,
"retryinterval": 30,
"priority": "normal"
},
"from": {
"resource": "4035551234",
"resourcename": null
},
"to": [
{
"id": "EB91A628-2E01-4F04-A96E-57B83FB9BF43",
"resourceid": "eb91a628-2e01-4f04-a96e-57b83fb9bf43",
"resource": "4035551234",
"active": false,
"metadata": { },
"disposition": {
"start": "2024-05-02T16:00:59Z",
"status": "delivered",
"duration": 0,
"responseinput": null,
"errormessage": null
},
"messaging": [
{
"direction": "out", "message": "Respond with '1' to confirm", "processed": "2024-05-02T16:01:15Z", "status": "delivered"
},
{
"direction": "in", "message": "1", "processed": "2024-05-02T16:04:11Z", "status" : "received"
}
]
}
]
}
```
## Job Error Reporting
### [GET] /voice/errors/?days=`numdays`
### [GET] /text/errors/?days=`numdays`
### [GET] /email/errors/?days=`numdays`
Query the Errors endpoint for any resources with errors in the last `numdays` days.
##### Parameters
| Property | Description |
| ----- | ----- |
| days | (optional) Number of days to look back (default 30) |
##### Example Request Parameters
```
https://message.ivrnet.com/api/v1/email/errors/
```
##### Response
```json
{
"payloadValidation": {
"valid": true,
"message": "Success"
},
"errors": [
{
"resource": "test@test.com",
"errormessage": "CLIENT: User's mailbox does not exist",
"networkmessage": null
}
]
}
```
## Webhook Error Reporting
### [GET] /voice/webhook/errors/?days=`numdays`
### [GET] /text/webhook/errors/?days=`numdays`
### [GET] /email/webhook/errors/?days=`numdays`
Query the Errors endpoint for any Webhooks with errors in the last `numdays` days.
##### Parameters
| Property | Description |
| ----- | ----- |
| days | (optional) Number of days to look back (default 30) |
##### Example Request Parameters
```
https://message.ivrnet.com/api/v1/email/webhook/errors/
```
##### Response
```json
{
"payloadValidation": {
"valid": true,
"message": "Success"
},
"errors": [
{
"jobuuid": "0a39d4f6-e341-4e72-a2b5-892d8d47fb59",
"statuscode": "500",
"jsoncontent": "{ \"eventtype\": \"Completed\", \"jobid\": \"0a39d4f6-e341-4e72-a2b5-892d8d47fb59\", \"jobmetadata\": { \"key1\": \"value1\", \"key2\": \"value2\" }}",
"createdat": "2024-06-07T09:36:12Z"
}
]
}
```
\ No newline at end of file
[project]
name = "communication-mcp"
version = "0.1.0"
description = "MCP middleware for Ivrnet Message API"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"mcp>=0.1.0",
"httpx>=0.27.0",
"pydantic>=2.0.0",
"python-dotenv>=1.0.0",
"uvicorn>=0.20.0",
]
[project.optional-dependencies]
dev = ["pytest"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src"]
import httpx
from typing import Optional, Dict, Any
from .models import Environment
ENV_URLS = {
Environment.PRODUCTION_CA: "https://message.ivrnet.com/api/v1",
Environment.PRODUCTION_US: "https://message.us.ivrnet.com/api/v1",
Environment.TEST: "https://message-staging.ivrnet.com/api/v1",
}
class IvrnetClient:
def __init__(
self,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
):
self.base_url = ENV_URLS[environment]
self.headers = {"Content-Type": "application/json"}
if api_key:
self.headers["X-API-KEY"] = api_key
elif basic_auth:
self.headers["Authorization"] = basic_auth
elif username and password:
import base64
credentials = f"{username}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
self.headers["Authorization"] = f"Basic {encoded}"
else:
raise ValueError("Authentication is required: provide api_key or username/password or basic_auth")
async def request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = f"{self.base_url}{endpoint}"
async with httpx.AsyncClient() as client:
response = await client.request(method, url, headers=self.headers, json=data, params=params)
try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
# Try to parse error message from body
try:
error_body = response.json()
raise Exception(f"API Error: {e.response.status_code} - {error_body}")
except:
raise e
if response.status_code == 204:
return {}
return response.json()
async def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
return await self.request("POST", endpoint, data=data)
async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return await self.request("GET", endpoint, params=params)
async def put(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
return await self.request("PUT", endpoint, data=data)
async def delete(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
return await self.request("DELETE", endpoint, data=data)
from enum import Enum
from typing import List, Optional, Dict, Any, Union
from pydantic import BaseModel, Field
class Environment(str, Enum):
PRODUCTION_CA = "production-ca"
PRODUCTION_US = "production-us"
TEST = "test"
class JobStatus(str, Enum):
SCHEDULED = "scheduled"
RUNNING = "running"
COMPLETED = "completed"
EXPIRED = "expired"
PAUSED = "paused"
CANCELLED = "cancelled"
ALL = "all"
class Priority(str, Enum):
EMERGENCY = "emergency"
URGENT = "urgent"
NORMAL = "normal"
LOW = "low"
TEST = "test"
class MessageType(str, Enum):
TTS = "tts"
VOICE = "voice"
HYBRID = "hybrid"
PLAINTEXT = "plaintext"
HTML = "html"
class WebhookType(BaseModel):
scheduled: Optional[bool] = None
running: Optional[bool] = None
completed: Optional[bool] = None
expired: Optional[bool] = None
class ResponseVariables(BaseModel):
# Dynamic keys, but we can define common ones or use Dict
pass
class WebhookTypes(BaseModel):
jobstatus: Optional[WebhookType] = None
responsevariables: Optional[Dict[str, bool]] = None
class Retry(BaseModel):
attempts: Optional[int] = 3
interval: Optional[int] = 15
interval_list: Optional[List[int]] = None
statuscode_list: Optional[List[int]] = None
class WebhookAuth(BaseModel):
apikey: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
basicauth: Optional[str] = None
class Webhook(BaseModel):
url: str
auth: Optional[WebhookAuth] = None
method: Optional[str] = "POST"
retry: Optional[Retry] = None
types: Optional[WebhookTypes] = None
class JobOptions(BaseModel):
webhook: Optional[Webhook] = None
class FromResource(BaseModel):
resource: str
resourcename: Optional[str] = None
class ToResource(BaseModel):
resourceid: Optional[str] = None
resource: str
resourcename: Optional[str] = None
languagename: str
variables: Optional[Dict[str, str]] = None
metadata: Optional[Dict[str, str]] = None
class MessageVariableTags(BaseModel):
opentag: str
closetag: str
class EmailAttachment(BaseModel):
filename: str
filebase64data: str
class BodyOptions(BaseModel):
messagetype: Optional[MessageType] = None
messagevariabletags: Optional[MessageVariableTags] = None
ttsmessagevoice: Optional[str] = None # female or male
responsemessageprompt: Optional[Dict[str, str]] = None
responsesuccessmessage: Optional[Dict[str, str]] = None
responseerrormessage: Optional[Dict[str, str]] = None
responsevariables: Optional[Dict[str, str]] = None
responsevariablesactions: Optional[Dict[str, str]] = None
responsevariablesmessages: Optional[Dict[str, Dict[str, str]]] = None
hangupmessage: Optional[Dict[str, str]] = None
transfermessage: Optional[Dict[str, str]] = None
jumpmessage: Optional[Dict[str, str]] = None
languagetranslate: Optional[bool] = False
emailattachments: Optional[List[EmailAttachment]] = None
class Body(BaseModel):
message: Dict[str, str]
voicemailmessage: Optional[Dict[str, str]] = None
subject: Optional[Dict[str, str]] = None # For email
options: Optional[BodyOptions] = None
class Schedule(BaseModel):
priority: Optional[Priority] = Priority.NORMAL
start: str # ISO8601
end: str # ISO8601
retryattempts: Optional[int] = None
retryinterval: Optional[int] = None
class CreateJobRequest(BaseModel):
job: Optional[Dict[str, Any]] = None # name, metadata, options
from_: FromResource = Field(..., alias="from")
to: List[ToResource]
body: Body
schedule: Schedule
class JobResponse(BaseModel):
jobid: Optional[str] = None
payloadResponse: Optional[Dict[str, Any]] = None
payloadValidation: Optional[Dict[str, Any]] = None
class VoiceFileResponse(BaseModel):
tollfree: Optional[str] = None
accesscode: Optional[str] = None
fileid: Optional[str] = None
filename: Optional[str] = None
from mcp.server.fastmcp import FastMCP
from .tools.jobs import (
create_voice_job,
create_text_job,
create_email_job,
get_job,
list_jobs,
cancel_job,
pause_resume_job,
send_followup_message,
)
from .tools.voice_files import (
create_voice_file_placeholder,
list_voice_files,
delete_voice_file,
)
from .tools.lists import (
clear_email_bounce_list,
upload_text_optin_list,
)
from .tools.errors import (
get_job_errors,
get_webhook_errors,
)
mcp = FastMCP("communication-mcp")
# Register tools
mcp.tool()(create_voice_job)
mcp.tool()(create_text_job)
mcp.tool()(create_email_job)
mcp.tool()(get_job)
mcp.tool()(list_jobs)
mcp.tool()(cancel_job)
mcp.tool()(pause_resume_job)
mcp.tool()(send_followup_message)
mcp.tool()(create_voice_file_placeholder)
mcp.tool()(list_voice_files)
mcp.tool()(delete_voice_file)
mcp.tool()(clear_email_bounce_list)
mcp.tool()(upload_text_optin_list)
mcp.tool()(get_job_errors)
mcp.tool()(get_webhook_errors)
# Expose the ASGI app for uvicorn
app = mcp.sse_app()
def main():
mcp.run()
if __name__ == "__main__":
main()
from typing import Optional, List, Dict, Any
from ..api import IvrnetClient
from ..models import Environment
def get_client(
environment: Environment,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> IvrnetClient:
return IvrnetClient(environment, api_key, username, password, basic_auth)
async def get_job_errors(
job_type: str, # voice, text, email
days: int = 30,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Get job errors."""
client = get_client(environment, api_key, username, password, basic_auth)
params = {"days": days}
return await client.get(f"/{job_type}/errors/", params=params)
async def get_webhook_errors(
job_type: str, # voice, text, email
days: int = 30,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Get webhook errors."""
client = get_client(environment, api_key, username, password, basic_auth)
params = {"days": days}
return await client.get(f"/{job_type}/webhook/errors/", params=params)
from typing import Optional, List, Dict, Any
from ..api import IvrnetClient
from ..models import Environment, CreateJobRequest, JobStatus
def get_client(
environment: Environment,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> IvrnetClient:
return IvrnetClient(environment, api_key, username, password, basic_auth)
async def create_voice_job(
from_resource: str,
to_resource: str,
languagename: str,
message: Dict[str, str],
start_time: str,
end_time: str,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
job_name: Optional[str] = None,
job_metadata: Optional[Dict[str, str]] = None,
webhook_url: Optional[str] = None,
from_resourcename: Optional[str] = None,
to_resourceid: Optional[str] = None,
to_resourcename: Optional[str] = None,
to_variables: Optional[Dict[str, str]] = None,
to_metadata: Optional[Dict[str, str]] = None,
voicemail_message: Optional[Dict[str, str]] = None,
priority: Optional[str] = "normal",
retry_attempts: Optional[int] = 1,
retry_interval: Optional[int] = 15,
) -> Dict[str, Any]:
"""Create a voice job."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {
"job": {
"name": job_name,
"metadata": job_metadata,
},
"from": {
"resource": from_resource,
"resourcename": from_resourcename,
},
"to": [
{
"resourceid": to_resourceid,
"resource": to_resource,
"resourcename": to_resourcename,
"languagename": languagename,
"variables": to_variables,
"metadata": to_metadata,
}
],
"body": {
"message": message,
"voicemailmessage": voicemail_message,
},
"schedule": {
"start": start_time,
"end": end_time,
"priority": priority,
"retryattempts": retry_attempts,
"retryinterval": retry_interval,
}
}
if webhook_url:
payload["job"]["options"] = {"webhook": {"url": webhook_url}}
return await client.post("/voice/", payload)
async def create_text_job(
from_resource: str,
to_resource: str,
languagename: str,
message: Dict[str, str],
start_time: str,
end_time: str,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
job_name: Optional[str] = None,
job_metadata: Optional[Dict[str, str]] = None,
webhook_url: Optional[str] = None,
to_resourceid: Optional[str] = None,
to_resourcename: Optional[str] = None,
to_variables: Optional[Dict[str, str]] = None,
to_metadata: Optional[Dict[str, str]] = None,
priority: Optional[str] = "normal",
retry_attempts: Optional[int] = 1,
retry_interval: Optional[int] = 15,
) -> Dict[str, Any]:
"""Create a text job."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {
"job": {
"name": job_name,
"metadata": job_metadata,
},
"from": {
"resource": from_resource,
},
"to": [
{
"resourceid": to_resourceid,
"resource": to_resource,
"resourcename": to_resourcename,
"languagename": languagename,
"variables": to_variables,
"metadata": to_metadata,
}
],
"body": {
"message": message,
},
"schedule": {
"start": start_time,
"end": end_time,
"priority": priority,
"retryattempts": retry_attempts,
"retryinterval": retry_interval,
}
}
if webhook_url:
payload["job"]["options"] = {"webhook": {"url": webhook_url}}
return await client.post("/text/", payload)
async def create_email_job(
from_resource: str,
to_resource: str,
languagename: str,
message: Dict[str, str],
subject: Dict[str, str],
start_time: str,
end_time: str,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
job_name: Optional[str] = None,
job_metadata: Optional[Dict[str, str]] = None,
webhook_url: Optional[str] = None,
from_resourcename: Optional[str] = None,
to_resourceid: Optional[str] = None,
to_resourcename: Optional[str] = None,
to_variables: Optional[Dict[str, str]] = None,
to_metadata: Optional[Dict[str, str]] = None,
messagetype: Optional[str] = "plaintext",
priority: Optional[str] = "normal",
retry_attempts: Optional[int] = 1,
retry_interval: Optional[int] = 15,
) -> Dict[str, Any]:
"""Create an email job."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {
"job": {
"name": job_name,
"metadata": job_metadata,
},
"from": {
"resource": from_resource,
"resourcename": from_resourcename,
},
"to": [
{
"resourceid": to_resourceid,
"resource": to_resource,
"resourcename": to_resourcename,
"languagename": languagename,
"variables": to_variables,
"metadata": to_metadata,
}
],
"body": {
"message": message,
"subject": subject,
"options": {
"messagetype": messagetype
}
},
"schedule": {
"start": start_time,
"end": end_time,
"priority": priority,
"retryattempts": retry_attempts,
"retryinterval": retry_interval,
}
}
if webhook_url:
payload["job"].setdefault("options", {})["webhook"] = {"url": webhook_url}
return await client.post("/email/", payload)
async def get_job(
job_id: str,
job_type: str, # voice, text, email
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Get job details."""
client = get_client(environment, api_key, username, password, basic_auth)
return await client.get(f"/{job_type}/{job_id}")
async def list_jobs(
job_type: str, # voice, text, email
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
page: int = 1,
page_size: int = 10,
status: JobStatus = JobStatus.ALL,
) -> List[Dict[str, Any]]:
"""List jobs."""
client = get_client(environment, api_key, username, password, basic_auth)
params = {
"page": page,
"pagesize": page_size,
"status": status.value,
}
return await client.get(f"/{job_type}/list/", params=params)
async def cancel_job(
job_id: str,
job_type: str, # voice, text, email
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Cancel a job."""
client = get_client(environment, api_key, username, password, basic_auth)
return await client.delete(f"/{job_type}/{job_id}")
async def pause_resume_job(
job_id: str,
job_type: str, # voice, text, email
action: str, # pause or resume
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Pause or resume a job."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {
"jobid": job_id,
"action": action
}
return await client.put(f"/{job_type}/", payload)
async def send_followup_message(
job_id: str,
resource: str,
message: str,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Send a follow-up text message."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {
"jobid": job_id,
"resource": resource,
"message": message
}
return await client.post("/text/msg/recipient/", payload)
from typing import Optional, List, Dict, Any
from ..api import IvrnetClient
from ..models import Environment
def get_client(
environment: Environment,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> IvrnetClient:
return IvrnetClient(environment, api_key, username, password, basic_auth)
async def clear_email_bounce_list(
resources: Optional[List[str]] = None,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Clear email bounce list."""
client = get_client(environment, api_key, username, password, basic_auth)
if resources:
payload = {"to": [{"resource": r} for r in resources]}
return await client.delete("/email/bouncelist/clearlist/", data=payload)
else:
return await client.delete("/email/bouncelist/cleardomain/")
async def upload_text_optin_list(
resources: List[str],
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Upload text opt-in list."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {"to": [{"resource": r} for r in resources]}
return await client.post("/text/optin/", payload)
from typing import Optional, List, Dict, Any
from ..api import IvrnetClient
from ..models import Environment
def get_client(
environment: Environment,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> IvrnetClient:
return IvrnetClient(environment, api_key, username, password, basic_auth)
async def create_voice_file_placeholder(
filename: str,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Create a voice file placeholder."""
client = get_client(environment, api_key, username, password, basic_auth)
payload = {"filename": filename}
return await client.post("/voice/audio/accesscode/", payload)
async def list_voice_files(
status: str = "completed", # completed or incomplete
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""List voice files."""
client = get_client(environment, api_key, username, password, basic_auth)
if status == "incomplete":
return await client.get("/voice/audio/list/accesscode/")
else:
return await client.get("/voice/audio/list/")
async def delete_voice_file(
file_id: str,
environment: Environment = Environment.PRODUCTION_CA,
api_key: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
basic_auth: Optional[str] = None,
) -> Dict[str, Any]:
"""Delete a voice file."""
client = get_client(environment, api_key, username, password, basic_auth)
return await client.delete(f"/voice/audio/file/{file_id}")
import pytest
from src.tools import jobs
from src.api import IvrnetClient
from src.models import Environment
def test_environment_enum():
assert Environment.PRODUCTION_CA == "production-ca"
def test_ivrnet_client_requires_auth():
with pytest.raises(ValueError):
IvrnetClient()
@pytest.mark.asyncio
async def test_create_email_job_webhook(monkeypatch):
captured = {}
class DummyClient:
async def post(self, endpoint, payload):
captured["endpoint"] = endpoint
captured["payload"] = payload
return {"ok": True}
monkeypatch.setattr(jobs, "get_client", lambda *a, **k: DummyClient())
await jobs.create_email_job(
from_resource="from",
to_resource="to",
languagename="en",
message={"en": "Hello"},
subject={"en": "Subject"},
start_time="2020-01-01T00:00:00Z",
end_time="2020-01-01T01:00:00Z",
webhook_url="https://example.com/hook",
)
assert captured["endpoint"] == "/email/"
assert captured["payload"]["job"]["options"]["webhook"]["url"] == "https://example.com/hook"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment