Workflow API

Send email, make voice calls and run multi-step customer journeys, for example an SMS, then an email, then a call if there’s no response, by submitting your customers’ details to a Workflow instance.

POST one or more records to https://wfa.everymessage.com/rest/v1/records/submit. Each record is a contact plus any data the message needs. The instance you name decides what happens to each record. Keep the workflowRecordId we return, and use /records/query to see the record’s progress and the SMS and email it sent.

How it works

  • Instance: a journey we set up with you, made of actions such as send an email, send an SMS, make a voice call, wait, or check a reply. You refer to it by name, for example payment_reminder.
  • Record: one contact, identified by your own primaryReference, with their phone number, email address, name and address.
  • Variables: extra values for this record, such as a balance or an appointment time. Their names match the placeholders in the instance’s messages.

The Workflow API runs on its own address, wfa.everymessage.com, and uses its own API key and secret with HTTP Basic authentication (key as the username, secret as the password). Contact us to set up an instance and get your keys. See also business email and AI voice assistants.

To have a journey call your own systems, for live data lookups, SMS replies and delivery receipts, see Workflow webhooks.

Always check responseStatusCode. The API returns HTTP 200 even when a request is refused. The real result is responseStatusCode in the response body: 200 means success; anything else comes with a responseStatusReason and a responseStatusMessage.

{
  "apiName": "everymessage Workflow Web API",
  "apiVersion": "1.1.2",
  "apiTime": "11 October 2026 13:53:41",
  "apiState": "RUNNING",
  "responseStatusCode": 401,
  "responseStatusReason": "UNAUTHORIZED",
  "responseStatusMessage": "Access denied.",
  "processingTime": 16
}

Submit records

POSThttps://wfa.everymessage.com/rest/v1/records/submit

Records are stored and processed straight away, or at the scheduled time if you set one.

Request

Email and SMS reminder

{
  "instanceName": "payment_reminder",
  "records": [
    {
      "primaryReference": "CUST-1042",
      "firstName": "Sam",
      "phoneNumber": "07700900123",
      "emailAddress": "sam@example.com",
      "variables": [
        {
          "name": "balance",
          "value": "345.22"
        },
        {
          "name": "due_date",
          "value": "16/11/2026"
        }
      ]
    }
  ]
}

Several records

{
  "instanceName": "payment_reminder",
  "batchName": "November reminders",
  "records": [
    {
      "primaryReference": "CUST-1042",
      "firstName": "Sam",
      "phoneNumber": "07700900123",
      "emailAddress": "sam@example.com",
      "variables": [
        {
          "name": "balance",
          "value": "345.22"
        },
        {
          "name": "due_date",
          "value": "16/11/2026"
        }
      ]
    },
    {
      "primaryReference": "CUST-1043",
      "firstName": "Priya",
      "phoneNumber": "07700900456",
      "emailAddress": "priya@example.com",
      "variables": [
        {
          "name": "balance",
          "value": "120.00"
        },
        {
          "name": "due_date",
          "value": "18/11/2026"
        }
      ]
    }
  ]
}

Scheduled email

{
  "instanceName": "newsletter_email",
  "batchName": "Winter newsletter",
  "scheduledRunRangeStart": "2026-11-02T09:00:00",
  "scheduledRunRangeEnd": "2026-11-02T17:00:00",
  "records": [
    {
      "primaryReference": "CUST-1042",
      "emailAddress": "sam@example.com",
      "firstName": "Sam"
    },
    {
      "primaryReference": "CUST-1043",
      "emailAddress": "priya@example.com",
      "firstName": "Priya"
    }
  ]
}

Voice call

{
  "instanceName": "appointment_call",
  "records": [
    {
      "primaryReference": "APT-5531",
      "title": "Mr",
      "surname": "Jones",
      "phoneNumber": "07700900789",
      "scheduledRun": "2026-11-03T10:00:00",
      "variables": [
        {
          "name": "appointment_time",
          "value": "11:30"
        },
        {
          "name": "clinic",
          "value": "Burnham"
        }
      ]
    }
  ]
}

Request body

FieldTypeDescription
instanceName requiredstringThe instance that will process these records.
records requiredarrayOne or more records. See Record.
batchNamestringA name for this group of records, shown in reports. Only used when you submit more than one record.
deletePendingRecordsbooleanSet true to delete any existing records that are still waiting to be processed. Default false.
scheduledRunRangeStartdate-timeStart processing the records at this time, for example 2026-11-02T09:00:00. If omitted, each record’s own scheduledRun is used, or it is processed immediately.
scheduledRunRangeEnddate-timeFinish processing the records by this time. Use with scheduledRunRangeStart to spread a large send across a period.

Record

FieldTypeDescription
primaryReference requiredstringYour unique reference for this contact, such as a customer or account number.
secondaryReferencestringA second reference of your own.
title, firstName, middleName, surnamestringThe contact’s name, for personalising messages and calls.
genderstringUNKNOWN, MALE or FEMALE.
dateOfBirthdate-timeThe contact’s date of birth.
emailAddressstringEmail address used by email actions.
alternativeEmailAddressstringA second email address.
phoneNumberstringThe main number for SMS and voice calls, for example 07700900123 or 447700900123.
alternativePhoneNumberstringA second phone number.
mailingAddressLine1 … mailingAddressLine4stringPostal address lines, for letter actions. The recipient’s name is added automatically, so line 1 is usually the house or street.
mailingPostcodestringPostcode.
mailingCountryISOstring2-letter country code.
scheduledRundate-timeWhen to process this record. If omitted, it is processed immediately.
variablesarrayExtra values as {"name": "…", "value": "…"} pairs. Names must match the variables defined in the instance; values are always strings.

Response

{
  "apiName": "everymessage Workflow Web API",
  "apiVersion": "1.1.2",
  "apiTime": "11 October 2026 16:07:04",
  "apiState": "RUNNING",
  "responseStatusCode": 200,
  "responseStatusReason": "None",
  "processingTime": 31,
  "body": {
    "records": [
      {
        "primaryReference": "CUST-1042",
        "workflowRecordId": 23997
      },
      {
        "primaryReference": "CUST-1043",
        "workflowRecordId": 23998
      }
    ],
    "recordsSubmitted": 2,
    "recordsWithWarningFlag": 0
  }
}
FieldTypeDescription
responseStatusCodeinteger200 on success. See the note above.
responseStatusReasonstringNone on success, otherwise the reason, for example UNAUTHORIZED.
responseStatusMessagestringA description of the problem, when there is one.
processingTimeintegerTime taken on our side, in milliseconds.
body.recordsarrayOne entry per record: your primaryReference and our workflowRecordId. Keep the id to query the record later.
body.recordsSubmittedintegerNumber of records accepted.
body.recordsWithWarningFlagintegerNumber of records with a problem in their data, such as an invalid phone number or email address. They are still processed, because the instance may be able to use another channel, but actions using the invalid detail will fail.

Query records

POSThttps://wfa.everymessage.com/rest/v1/records/query

Returns the progress of records you submitted, and optionally the SMS and email each one sent.

Request

{
  "records": [
    23997
  ],
  "includeSmsInformation": true,
  "includeEmailInformation": true
}

Response

{
  "apiName": "everymessage Workflow Web API",
  "apiVersion": "1.1.2",
  "apiTime": "11 October 2026 16:21:04",
  "apiState": "RUNNING",
  "responseStatusCode": 200,
  "responseStatusReason": "None",
  "processingTime": 47,
  "body": {
    "records": [
      {
        "dataReference": "23997",
        "primaryReference": "CUST-1042",
        "processingStatus": "COMPLETED",
        "sentSmsMessages": [
          {
            "reference": "WF5-1-T-23997-1-0",
            "originator": "EVERYMSG",
            "to": "447700900123",
            "sent": true,
            "sentDate": "2026-10-11T10:52:18.52",
            "delivered": true,
            "deliveryCode": 0,
            "deliveryDate": "2026-10-11T10:53:22.14"
          }
        ],
        "sentEmailDocuments": [
          {
            "reference": "23997-1",
            "from": "accounts@example.com",
            "to": [
              "sam@example.com"
            ],
            "sent": true,
            "sentDate": "2026-10-11T10:52:22",
            "deliveryFailure": false
          }
        ]
      }
    ]
  }
}
ParameterTypeDescription
records requiredarrayOne or more workflowRecordId values from the submit response.
includeSmsInformationbooleanInclude the SMS each record sent.
includeEmailInformationbooleanInclude the email each record sent.

Record status

processingStatusMeaning
PENDINGWaiting to be processed, for example because it is scheduled.
RUNNINGThe journey is in progress.
COMPLETEDThe journey finished successfully.
FAILEDThe journey finished unsuccessfully.

sentSmsMessages

FieldTypeDescription
referencestringOur reference for the SMS.
originator, tostringWho it was sent from and to.
sent, sentDateboolean, date-timeWhether and when it was sent.
delivered, deliveryCode, deliveryDateboolean, integer, date-timeThe delivery result; deliveryCode 0 means no error. Left out until the network reports back.

sentEmailDocuments

FieldTypeDescription
referencestringOur reference for the email.
from, tostring, arraySender address and recipient addresses.
sent, sentDateboolean, date-timeWhether and when it was sent. Left out until known.
deliveryFailure, deliveryFailureDate, deliveryFailureMessageboolean, date-time, stringSet if the email bounced; the message is usually the receiving mail server’s reason.

Fields that are not known yet are left out, so treat them all as optional. dataReference is the workflowRecordId, and may be returned as a string.

Code examples

Both examples read the Workflow keys from EM_WORKFLOW_KEY and EM_WORKFLOW_SECRET. C# needs .NET 8 or later; Python needs 3.10 or later and pip install httpx.

Submit a record

C#

// Submit a record to an everymessage Workflow instance (.NET 8 or later)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

var apiKey = Environment.GetEnvironmentVariable("EM_WORKFLOW_KEY")!;
var secretKey = Environment.GetEnvironmentVariable("EM_WORKFLOW_SECRET")!;

using var http = new HttpClient { BaseAddress = new Uri("https://wfa.everymessage.com/rest/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{secretKey}")));

var json = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

// The instance decides what happens: email, SMS, a voice call, or a sequence of them.
var request = new SubmitRequest(
    InstanceName: "payment_reminder",
    Records:
    [
        new Record(
            PrimaryReference: "CUST-1042",
            FirstName: "Sam",
            PhoneNumber: "07700900123",
            EmailAddress: "sam@example.com",
            Variables: [new("balance", "345.22"), new("due_date", "16/11/2026")])
    ]);

using var response = await http.PostAsJsonAsync("records/submit", request, json);
response.EnsureSuccessStatusCode();

var result = await response.Content.ReadFromJsonAsync<Envelope<SubmitBody>>(json)
             ?? throw new InvalidOperationException("Empty response from everymessage");

// The API returns HTTP 200 even on errors: check responseStatusCode in the body.
if (result.ResponseStatusCode != 200)
    throw new InvalidOperationException(
        $"Submit failed: {result.ResponseStatusReason} {result.ResponseStatusMessage}");

// Keep workflowRecordId: it is how you query the record later.
foreach (var r in result.Body!.Records)
    Console.WriteLine($"{r.PrimaryReference}: workflow record {r.WorkflowRecordId}");

Console.WriteLine($"{result.Body.RecordsSubmitted} submitted, {result.Body.RecordsWithWarningFlag} with warnings");

record SubmitRequest(string InstanceName, IReadOnlyList<Record> Records,
    string? BatchName = null, bool DeletePendingRecords = false);
record Record(string PrimaryReference, string? FirstName = null, string? Surname = null,
    string? PhoneNumber = null, string? EmailAddress = null, DateTime? ScheduledRun = null,
    IReadOnlyList<Variable>? Variables = null);
record Variable(string Name, string Value);

record Envelope<T>(int ResponseStatusCode, string? ResponseStatusReason,
    string? ResponseStatusMessage, T? Body);
record SubmitBody(IReadOnlyList<SubmittedRecord> Records, int RecordsSubmitted, int RecordsWithWarningFlag);
record SubmittedRecord(string PrimaryReference, long WorkflowRecordId);

Python

"""Submit a record to an everymessage Workflow instance (Python 3.10+, pip install httpx)."""
import os

import httpx

# The instance decides what happens: email, SMS, a voice call, or a sequence of them.
payload = {
    "instanceName": "payment_reminder",
    "records": [
        {
            "primaryReference": "CUST-1042",
            "firstName": "Sam",
            "phoneNumber": "07700900123",
            "emailAddress": "sam@example.com",
            "variables": [
                {"name": "balance", "value": "345.22"},
                {"name": "due_date", "value": "16/11/2026"},
            ],
        }
    ],
}

with httpx.Client(
    base_url="https://wfa.everymessage.com/rest/v1",
    auth=(os.environ["EM_WORKFLOW_KEY"], os.environ["EM_WORKFLOW_SECRET"]),
    timeout=30,
) as client:
    response = client.post("/records/submit", json=payload)
    response.raise_for_status()
    result = response.json()

# The API returns HTTP 200 even on errors: check responseStatusCode in the body.
if result.get("responseStatusCode") != 200:
    raise RuntimeError(f"Submit failed: {result.get('responseStatusReason')} "
                       f"{result.get('responseStatusMessage', '')}")

body = result["body"]
# Keep workflowRecordId: it is how you query the record later.
for r in body["records"]:
    print(f"{r['primaryReference']}: workflow record {r['workflowRecordId']}")
print(f"{body['recordsSubmitted']} submitted, {body['recordsWithWarningFlag']} with warnings")

Query records

C#

// Check the progress of Workflow records, with the SMS and email they sent (.NET 8 or later)
// Usage: dotnet run -- <workflowRecordId> [<workflowRecordId> ...]
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

var apiKey = Environment.GetEnvironmentVariable("EM_WORKFLOW_KEY")!;
var secretKey = Environment.GetEnvironmentVariable("EM_WORKFLOW_SECRET")!;

using var http = new HttpClient { BaseAddress = new Uri("https://wfa.everymessage.com/rest/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{secretKey}")));

var json = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString   // ids may arrive as "23996"
};

var query = new { records = args.Select(long.Parse).ToArray(), includeSmsInformation = true, includeEmailInformation = true };

using var response = await http.PostAsJsonAsync("records/query", query, json);
response.EnsureSuccessStatusCode();

var result = await response.Content.ReadFromJsonAsync<Envelope<QueryBody>>(json)
             ?? throw new InvalidOperationException("Empty response from everymessage");

if (result.ResponseStatusCode != 200)
    throw new InvalidOperationException(
        $"Query failed: {result.ResponseStatusReason} {result.ResponseStatusMessage}");

foreach (var r in result.Body!.Records)
{
    Console.WriteLine($"Record {r.DataReference} ({r.PrimaryReference}): {r.ProcessingStatus}");
    foreach (var s in r.SentSmsMessages ?? [])
        Console.WriteLine($"  SMS {s.Reference}: sent {s.Sent}, delivered {s.Delivered?.ToString() ?? "awaiting receipt"}");
    foreach (var e in r.SentEmailDocuments ?? [])
        Console.WriteLine($"  Email to {string.Join(", ", e.To ?? [])}: sent {e.Sent?.ToString() ?? "unknown"}" +
            (e.DeliveryFailure == true ? $", failed: {e.DeliveryFailureMessage}" : ""));
}

record Envelope<T>(int ResponseStatusCode, string? ResponseStatusReason,
    string? ResponseStatusMessage, T? Body);
record QueryBody(IReadOnlyList<WorkflowRecord> Records);
record WorkflowRecord(long DataReference, string? PrimaryReference, string ProcessingStatus,
    IReadOnlyList<SentSms>? SentSmsMessages, IReadOnlyList<SentEmail>? SentEmailDocuments);
record SentSms(string? Reference, bool Sent, DateTime? SentDate, bool? Delivered,
    int? DeliveryCode, DateTime? DeliveryDate);
record SentEmail(string? Reference, string? From, IReadOnlyList<string>? To, bool? Sent,
    bool? DeliveryFailure, string? DeliveryFailureMessage);

Python

"""Check the progress of Workflow records (Python 3.10+, pip install httpx).

Usage: python query_workflow.py <workflowRecordId> [<workflowRecordId> ...]
"""
import os
import sys

import httpx

query = {
    "records": [int(x) for x in sys.argv[1:]],
    "includeSmsInformation": True,
    "includeEmailInformation": True,
}

with httpx.Client(
    base_url="https://wfa.everymessage.com/rest/v1",
    auth=(os.environ["EM_WORKFLOW_KEY"], os.environ["EM_WORKFLOW_SECRET"]),
    timeout=30,
) as client:
    response = client.post("/records/query", json=query)
    response.raise_for_status()
    result = response.json()

if result.get("responseStatusCode") != 200:
    raise RuntimeError(f"Query failed: {result.get('responseStatusReason')} "
                       f"{result.get('responseStatusMessage', '')}")

for r in result["body"]["records"]:
    print(f"Record {r['dataReference']} ({r.get('primaryReference')}): {r['processingStatus']}")
    for s in r.get("sentSmsMessages") or []:
        print(f"  SMS {s.get('reference')}: sent {s.get('sent')}, "
              f"delivered {s.get('delivered', 'awaiting receipt')}")
    for e in r.get("sentEmailDocuments") or []:
        failed = f", failed: {e.get('deliveryFailureMessage')}" if e.get("deliveryFailure") else ""
        print(f"  Email to {', '.join(e.get('to') or [])}: sent {e.get('sent', 'unknown')}{failed}")