GreenArrow Email Software Documentation

Mailgun-Compatible Messages API

GreenArrow CloudMTA

This feature is only available on GreenArrow CloudMTA.

Contact us

Contact us if you need support for a field or an API that is not listed here.

Overview

The Mailgun-compatible Messages API lets software that was written for Mailgun’s message sending API send email through GreenArrow. GreenArrow translates each request and submits the message to the HTTP Submission API.

These endpoints are available:

Endpoint Purpose
POST /v3/{domain_name}/messages Send a message that GreenArrow builds from the parameters
POST /v3/{domain_name}/messages.mime Send a MIME message that you build

This page documents /messages and the rules that apply to both endpoints. The Mailgun-Compatible MIME Messages API page documents what is different for /messages.mime.

A request that is not a batch sends one message to all of its recipients. A batch request (with recipient-variables or an X-Mailgun-Recipient-Variables header) sends one message to each to recipient, with the variables of that recipient. See Batch Sending.

Send each request to the hostname of the Mailgun compatibility API of your GreenArrow CloudMTA. The examples on this page use mg.api.example.com.

Read Differences from Mailgun before you move an integration from Mailgun. Some requests that Mailgun accepts are refused here.

The Mail Class

Each message uses the Mail Class that the mailclass option of the compatibility_api_mailgun directive names. The bounce address of this Mail Class is the envelope sender (MAIL FROM) of each message. The other settings of the Mail Class apply too, for example its click and open tracking settings when the request does not have o:tracking, o:tracking-clicks or o:tracking-opens.

GreenArrow records the IP address of your client as the source address of the message. When the http_submission_api_received_header directive is show_source, the Received header of the message shows it.

Authentication

Authenticate with HTTP Basic authentication. The username is api, and the password is a GreenArrow API key.

curl -s --user "api:$API_KEY" \
  https://mg.api.example.com/v3/mail.example.com/messages \
  -F from='[email protected]' \
  -F to='[email protected]' \
  -F subject='Hello' \
  -F text='Hello!'

GreenArrow checks the credentials first: before it reads the request body, and before it checks a parameter. A request with no credentials, with a username other than api, or with a key that is not valid gets the 401 response, also when the request has other errors.

The 401 response has the plain-text body Forbidden, with Content-Type: text/plain; charset=utf-8 and the header WWW-Authenticate: Basic realm="MG API", as on Mailgun.

Requests and Responses

Request format

  • Send the parameters in a multipart/form-data body (curl -F) or in an application/x-www-form-urlencoded body. A request with an attachment must use multipart/form-data. A body of another type is refused with 400.
  • The body can be compressed with gzip. Set the header Content-Encoding: gzip.
  • GreenArrow does not read the query string. Send each parameter in the body.
  • To give a parameter more than one value (for example two to addresses), send the parameter one time for each value.
  • attachment and inline must be file parts of a multipart/form-data body. Each other parameter must be a plain value. A plain value of attachment or inline, and a file part of another parameter (for example html sent as a file), is refused with 400.
  • The fixed values of the options (yes, no, true, false and htmlonly) are accepted in any letter case, for example YES or HtmlOnly.
  • A parameter that this page does not list is refused with 400. See Unsupported Parameters.

The domain in the path

{domain_name} is the sending domain. The domain of the from address must be equal to it.

A {domain_name} that is not a valid domain name, or that has no dot (for example localhost or ip_pools), gets the 401 response. Mailgun gives the same answer for a domain that is not a domain of the account.

The answers for a domain that is not ready to send depend on how your GreenArrow CloudMTA uses Sending Domains:

  • When GreenArrow uses Sending Domains, a Sending Domain whose DNS records are verified and whose sending is disabled (sending_enabled is false) gets the 403 response.
  • When GreenArrow also requires that the From domain of each message is a verified Sending Domain, a {domain_name} that is not a Sending Domain gets the 401 response. A Sending Domain whose DNS records are not verified also gets the 401 response.

When GreenArrow does not use Sending Domains in these ways, it accepts a message from a domain that is not a Sending Domain.

Success

A message that GreenArrow queued gets a 200 response with Content-Type: application/json; charset=utf-8:

{"id": "<[email protected]>", "message": "Queued. Thank you."}

id is the Message-ID of the message. See The Message-ID.

Errors

The body of an error response is JSON with one field, message, except the plain-text 401 response:

{"message": "from parameter is missing"}

Status Meaning
400 The request is not valid, or it asks for something that GreenArrow does not support. See Error Messages.
401 The credentials are missing or not valid, or {domain_name} is not a domain that can send. The body is Forbidden. See Authentication.
403 The Sending Domain of {domain_name} is verified, but its sending is disabled. The message is Mailgun’s text for a domain that is not verified: Domain {domain_name} is not allowed to send: The domain is unverified and requires DNS configuration. Log in to your control panel to view required DNS records.
413 The request body is larger than 26,214,400 bytes after decompression. See Limits.
429 GreenArrow is busy. It could not inject the message within 30 seconds, because message injection is throttled. The message was not queued. Send it again after 60 seconds.
500 GreenArrow could not handle the request. The message is GreenArrow backend error. Contact GreenArrow support.

GreenArrow waits up to 60 seconds for the HTTP Submission API to queue the message. When this time passes, the answer is the 500 response, and the message can be queued or not.

When a request has more than one error, the response gives one of them.

Error Messages

These are the messages of the 400, 413 and 429 responses. When GreenArrow and Mailgun refuse a request for the same reason, the message is Mailgun’s. {name} is the name of the parameter as sent, and {value} is its value.

Case Message
from is missing. from parameter is missing
to is missing. to parameter is missing
subject is missing. subject parameter is missing
The request has no text, html or amp-html. Need at least one of 'text', 'html', 'amp-html' or 'template' parameters specified
amp-html without text or html. amp-html parameter needs text or html
from is not one address, or a to, cc or bcc value is not an address list. {name} parameter is not a valid address. please check documentation
from is sent more than one time. from parameter must have one value
The domain of from is not {domain_name}. from parameter must have the domain {domain_name}
A name is an encoded-word in a charset that GreenArrow does not decode. {name} parameter has an unknown charset: {charset}
More than 1,000 recipients. too many recipients: the limit is 1000
The values of the o:, h: and v: parameters are more than 16,384 bytes. Send options (parameters starting with o:, h:, or v:) are limited to 16 kB total
An unknown parameter, a template parameter, or a parameter that is not supported. {name} parameter is not supported
An option that is not supported, with a value other than no or false. o:dkim with no or false. {name} parameter value {value} is not supported
recipient-variables is not a JSON object or null. 'recipient-variables' parameter is not a valid JSON
h:X-Mailgun-Recipient-Variables, or the X-Mailgun-Recipient-Variables header of the /messages.mime document, is not a JSON object or null. X-Mailgun-Recipient-Variables is not a valid JSON
A batch with cc or bcc. cc and bcc parameters cannot be used with recipient-variables
A batch with a line break (CR or LF) in a recipient variable of a to address. recipient-variables has a line break in a value for {address}
A batch in which the Click Tracking ID of a recipient is more than 300 characters. o:tag and v: values are limited to 300 characters in the Click Tracking ID, got {length} for {address}
A /messages.mime batch whose document has a Message-ID that is not a valid msg-id in angle brackets, such as <[email protected]>. message parameter has a Message-ID that is not a valid Message-ID
An option value that is not valid, for example o:tracking=maybe. {name} parameter has an invalid value: {value}
o:deliverytime is not a date. {value} is not an RFC-2822 compliant date
o:deliverytime is more than 7 days after the request. scheduled delivery time must not be farther than 168h0m0s from now
o:deliver-within is less than 5 minutes or more than 24 hours. {duration} is the value in the form 4m0s. invalid deliver-within period: want=[5m0s;24h0m0s], got={duration}
o:deliver-within has another format. invalid deliver-within format
o:secondary-dkim or o:secondary-dkim-public is not signing_domain/selector. {name} parameter must be signing_domain/selector
o:secondary-dkim-public without o:secondary-dkim. o:secondary-dkim-public parameter needs o:secondary-dkim
o:sending-ip is not an IP address of this system. o:sending-ip parameter is not an IP address of this system
o:sending-ip with o:sending-ip-pool. o:sending-ip and o:sending-ip-pool parameters cannot be used together
o:sending-ip-pool is not the pool ID of a Route. o:sending-ip-pool parameter is not a valid pool ID
An h: header that is refused, that is sent more than one time, or whose name is not valid. {name} parameter is not allowed
A line break (CR or LF) in an h: value, or in from, to or cc. A fold in h:X-Mailgun-Recipient-Variables is not a line break. {name} parameter has a line break
A header that is too long for a line and has no space where it can be folded. {name} parameter is too long for a header line
h:Message-ID is not a valid Message-ID. h:Message-ID parameter is not a valid Message-ID
A control character in an o:tag value, in a v: value, or in a string of h:X-Mailgun-Variables. {name} parameter has a control character
The Click Tracking ID is more than 300 characters. o:tag and v: values are limited to 300 characters in the Click Tracking ID, got {length}
A file part where a plain value is expected. {name} parameter must not be a file
A plain value of attachment or inline. {name} parameter is not a file
An attachment or inline filename that GreenArrow cannot use. {name} parameter has an invalid filename: {filename}
An attachment or inline file that is empty, or that is the one character 0. {name} parameter has an empty file: {filename}
A body that cannot be decoded, for example a body that is not valid gzip or form data. invalid request body
A body larger than 26,214,400 bytes after decompression (413). request body is too large: the limit is 26214400 bytes
GreenArrow could not inject the message within 30 seconds (429). Too many requests, please try again after 60 seconds

The /messages.mime endpoint has more messages for its message parameter. See The Document.

The HTTP Submission API also checks each message. When it refuses a message, the response is a 400 with the message of the HTTP Submission API.

Limits

Limit Value When the request is over the limit
Request body 25 MB (26,214,400 bytes), after decompression. The form encoding and the multipart overhead count. 413
Recipients 1,000, across to, cc and bcc, after duplicate addresses are removed. 400
Options 16 KB (16,384 bytes): the values of all o:, h: and v: parameters together. The names of the parameters do not count. 400
Click Tracking ID 300 characters. See The Click Tracking ID. 400
o:deliverytime 7 days after the time of the request. 400
o:tag 10 tags of up to 128 characters each. GreenArrow drops the other tags. See o:tag.
Header line 998 bytes. GreenArrow folds a longer header at its spaces. 400 for a header that has no space where it can be folded
Time to send the request body 120 seconds from the start of the request.  
Time to queue the message 60 seconds from when GreenArrow has read the request body. A 500. The message may or may not be queued.

Send a Message

POST /v3/{domain_name}/messages

GreenArrow builds the message from the parameters, and submits it to the HTTP Submission API. All of its recipients get one message, except in a batch.

Parameters

from

string

/

required

The sender, as an address ([email protected]) or with a name (Example Team <[email protected]>). It becomes the From header.

The value must have one address. The domain of the address must be equal to {domain_name}. The letter case does not matter.

A name that is an RFC 2047 encoded-word (=?UTF-8?B?...?= <[email protected]>) is decoded. GreenArrow decodes the charsets UTF-8, ISO-8859-1 and US-ASCII, and refuses an encoded-word in another charset with 400.

GreenArrow gives the address to the HTTP Submission API as from_email, and the name as from_name.

to

string

/

required

/

may be repeated

The recipients. Each value is an address, an address with a name, or a list of these with commas between them. Names are decoded as for from.

A value that is not an address list is refused with 400, for example a JSON array (["[email protected]","[email protected]"]). To send to more than one address, send to one time for each value, or put commas between the addresses.

All of the to addresses go into the To header of the message. In a batch, the To header of each message has only its recipient. GreenArrow removes an address that is already in to, cc or bcc, so no recipient gets two copies of the message. It compares whole addresses, the letter case does not matter, and it keeps the first occurrence, in the order to, cc, bcc.

cc

string

/

optional

/

may be repeated

The Cc recipients. The values have the same form as to. A batch with cc or bcc is refused.

bcc

string

/

optional

/

may be repeated

The Bcc recipients. The values have the same form as to. The message does not show them. A name in a bcc value is not used.

subject

string

/

required

The subject of the message. An empty value gives an empty subject.

text

string

/

optional

The text part of the message.

html

string

/

optional

The HTML part of the message.

amp-html

string

/

optional

The AMP part of the message. A message with amp-html must also have text or html.

attachment

file

/

optional

/

may be repeated

A file to attach. The filename of the part is the name of the attachment. See Attachments and Inline Images.

inline

file

/

optional

/

may be repeated

An image or another file to show in the HTML part. The filename of the part is its Content-ID, so the HTML refers to photo.jpg as <img src="cid:photo.jpg">. See Attachments and Inline Images.

o:tag

string

/

optional

/

may be repeated

A tag of the message. GreenArrow keeps the first 10 tags, in request order, and drops a tag of more than 128 characters. A tag that GreenArrow drops does not count toward the 10. These limits do not cause an error. The tags go into the Click Tracking ID.

o:dkim

string

/

optional

yes and true are accepted, and GreenArrow signs the message as usual. no and false are refused with 400, because GreenArrow always signs.

o:secondary-dkim

string

/

optional

A second DKIM key to sign the message with, as signing_domain/selector (for example example.com/s1). The domain and the selector must each be a valid domain name. The selector can have more than one label, as in s1.2026.

The message gets the usual DKIM signature of its From domain and a second signature with this key. GreenArrow sets this with an X-GreenArrow-DKIM header.

GreenArrow does not check that the key exists. When it does not exist, GreenArrow logs an error and sends the message without the second signature. The response is 200.

o:secondary-dkim-public

string

/

optional

The domain and selector of the second signature, as signing_domain/selector. The signature uses the private key of o:secondary-dkim. This parameter needs o:secondary-dkim.

o:deliverytime

string

/

optional

The time of the first delivery attempt. The value is an RFC 2822 date with a numeric zone (Thu, 17 Sep 2026 14:02:33 +0000) or a zone name (Thu, 17 Sep 2026 14:02:33 UTC), or a number of Unix seconds.

The zone names are UT, UTC, GMT, EST, EDT, CST, CDT, MST, MDT, PST and PDT, in any letter case. Another zone name, an RFC 3339 date, and other text are refused with 400.

A time in the past counts as the time of the request. A time more than 7 days after the request is refused with 400.

GreenArrow makes the first delivery attempt at this time, or up to scheduled_send_jitter (1 minute by default) later. It uses the X-GreenArrow-SendNoEarlierThan header, or with o:deliver-within the X-GreenArrow-Schedule header.

The Date header of the message is the time of the request, as on Mailgun.

o:deliver-within

string

/

optional

The time window for the delivery of the message, as hours and minutes: 1h30m, 30m or 24h. The value must be from 5m to 24h. The window starts at o:deliverytime, or at the time of the request when there is no o:deliverytime. GreenArrow delivers the message only in this window. It uses the X-GreenArrow-Schedule header.

o:testmode

string

/

optional

Not supported. no and false are accepted and ignored. Another value is refused with 400.

o:tracking

string

/

optional

Click and open tracking: yes, true, no, false or htmlonly. htmlonly tracks the clicks in the HTML and AMP parts, but not in the text part, and tracks opens. GreenArrow sets this with the X-GreenArrow-Click-Tracking-Do header.

o:tracking-clicks

string

/

optional

Click tracking: yes, true, no, false or htmlonly. It takes precedence over o:tracking for clicks. GreenArrow sets this with the X-GreenArrow-TrackClicks header.

o:tracking-opens

string

/

optional

Open tracking: yes, true, no or false. It takes precedence over o:tracking for opens. GreenArrow sets this with the X-GreenArrow-TrackOpens header.

o:require-tls

string

/

optional

Not supported. no and false are accepted and ignored. Another value is refused with 400.

o:skip-verification

string

/

optional

Not supported. no and false are accepted and ignored. Another value is refused with 400.

o:sending-ip

string

/

optional

An IP address to send the message from. It must be the IPv4 address of an IP Address VirtualMTA of this system. An IPv4-mapped IPv6 address (::ffff:192.0.2.1) selects the VirtualMTA of its IPv4 address. Another value is refused with 400. GreenArrow sets the VirtualMTA with the X-GreenArrow-MtaID header.

A VirtualMTA that redirects its mail does not count. When more than one VirtualMTA has the address, GreenArrow uses the one that was created first.

Routes (o:sending-ip-pool) are the recommended way to choose the IP addresses that send a message. Use o:sending-ip for cases such as a delivery test on one IP address. A request cannot have both.

o:sending-ip-pool

string

/

optional

The pool ID of the Route that sends the message. A pool ID is 24 hexadecimal characters that GreenArrow makes from the ID of the Route; see Identifiers. The name of a Route and its numeric ID are not accepted.

GreenArrow gives the Route ID to the HTTP Submission API as route. A value that is not a pool ID, a Route that does not exist, and a system that does not have Routes enabled are refused with 400.

o:tracking-pixel-location-top

string

/

optional

Not supported. no and false are accepted and ignored. Another value is refused with 400.

h:{header}

string

/

optional

A header of the message, for example h:Reply-To. The value is written as given, and is folded at its spaces when it is too long for one line. Send each header one time only.

h:Message-ID sets the Message-ID of the message. See The Message-ID. For h:X-Mailgun-Variables, see The X-Mailgun-Variables Header.

These headers are refused with 400:

  • The headers that control GreenArrow: X-GreenArrow-*, X-Virtual-MTA, X-Schedule, X-Job, X-CampaignID and X-Mailer-Info.
  • The headers that GreenArrow builds from the parameters: From, To, Cc, Subject, Date, MIME-Version, Content-Type and Content-Transfer-Encoding.
  • Bcc, Return-Path, Authentication-Results and Sender.

The letter case of a header name does not matter. A name that is not a valid header field name is refused with 400.

v:{name}

string

/

optional

A variable of the message, for example v:customer_id. The value is always a string, also when it holds JSON. When a variable is sent more than one time, its last value is used. The variables go into the Click Tracking ID and the X-Mailgun-Variables header.

recipient-variables

JSON object or null

/

optional

The variables of each recipient, by address, for example {"[email protected]": {"first_name": "Alice"}}. The request is then a batch: each to recipient gets its own message. See Batch Sending. When it is sent more than one time, its last value is used.

template, t:version, t:text, t:variables and some o: options are not supported. See Unsupported Parameters.

Repeated and Empty Values

When a parameter that has one value is sent more than one time, GreenArrow does this:

Parameter When it is sent more than one time
subject The values are joined with one space.
text Each value is written followed by a line break.
html, amp-html The values are joined with <br> and a line break.
v:{name}, and each o: option other than o:tag The last value is used.
recipient-variables The last value is used.
from, h:{header} Refused with 400.

An empty text, html or amp-html value counts as absent. An empty subject gives an empty subject, an empty ordinary h: value gives an empty header, an empty v: value gives the value "", and an empty o:tag gives the tag "". An empty recipient-variables or h:X-Mailgun-Recipient-Variables value is refused because it is not valid JSON.

Attachments and Inline Images

Send each attachment and inline file as a file part of a multipart/form-data body, for example -F [email protected] with curl.

  • Content type. GreenArrow uses the Content-Type of the part. When the part has no type, a type that is not valid, or application/octet-stream, GreenArrow uses the type of the filename’s extension, or application/octet-stream when the extension is not known.
  • Filenames. A filename cannot be blank, and cannot contain ", ', CR or LF.
  • Inline filenames. The filename of an inline part is its Content-ID, so it must also be an RFC 5322 dot-atom-text, with or without an @ and a dot-atom-text domain. Thus photo.jpg and [email protected] are accepted, and my photo.jpg and a name with characters that are not ASCII are refused.
  • Content. A file cannot be empty, and cannot be the one character 0.

The message has the attachment files first, then the inline files, each in request order.

Examples

A message with two recipients

curl -s --user "api:$API_KEY" \
  https://mg.api.example.com/v3/mail.example.com/messages \
  -F from='Example Team <[email protected]>' \
  -F to='Alice Adams <[email protected]>' \
  -F to='[email protected]' \
  -F subject='September update' \
  --form-string html='<html><body><p>Hello!</p></body></html>' \
  --form-string text='Hello!' \
  -F o:tag=september-update \
  -F o:tracking=false \
  -F h:Reply-To='[email protected]' \
  -F v:campaign_id=2026-09-update

200 OK:

{"id": "<[email protected]>", "message": "Queued. Thank you."}

The message has one To header with both recipients.

Use --form-string for text and HTML

With -F, curl reads a value that starts with < as the name of a file to read, and a value that starts with @ as a file to upload. Thus -F html='<html>...' does not send the HTML. Use --form-string for a text or html value that starts with < or @.

To read a body part from a file, use -F html='<body.html'. This sends the contents of the file as a plain value. Do not use -F [email protected]: it sends a file part, which is refused with 400.

A message with an attachment and an inline image

curl -s --user "api:$API_KEY" \
  https://mg.api.example.com/v3/mail.example.com/messages \
  -F from='[email protected]' \
  -F to='[email protected]' \
  -F subject='Your report' \
  --form-string html='<p>Your report is attached.</p><img src="cid:logo.png">' \
  -F text='Your report is attached.' \
  -F [email protected] \
  -F [email protected]

A scheduled message

This message is delivered on 18 September 2026 from 09:00 to 11:00 UTC:

curl -s --user "api:$API_KEY" \
  https://mg.api.example.com/v3/mail.example.com/messages \
  -F from='[email protected]' \
  -F to='[email protected]' \
  -F subject='Good morning' \
  -F text='Good morning!' \
  -F o:deliverytime='Fri, 18 Sep 2026 09:00:00 +0000' \
  -F o:deliver-within=2h

The Message-ID

The id of a 200 response is the Message-ID of the message, with angle brackets.

  • With h:Message-ID, the Message-ID is that value. GreenArrow adds the angle brackets when they are missing. The value must be a valid msg-id (RFC 5322): a dot-atom-text, @, and a dot-atom-text or a domain literal ([192.0.2.1]). Another value, for example hello, is refused with 400.
  • Without h:Message-ID, GreenArrow makes a Message-ID in Mailgun’s form: <{time}.{random}@{domain_name}>. {time} is the time of the request in UTC as YYYYMMDDhhmmss, and {random} is 16 lower-case hexadecimal digits. For example: <[email protected]>.

For /messages.mime, the Message-ID of the document comes first. See The Message-ID on that page.

Tags and Variables

The Click Tracking ID

GreenArrow writes the tags, the variables and the identity of each message as a JSON document in its X-GreenArrow-Click-Tracking-ID header. The events of the message have this document in their click_tracking_id field.

{"m":"3f2b8c1e-9d4a-4e6f-8a17-5c0d2e9b7a41","M":"<[email protected]>","t":["september-update"],"v":{"campaign_id":"2026-09-update"}}

Key Value
m A unique ID of the message: a version 4 UUID that GreenArrow makes. It is unique also when two messages have the same Message-ID.
M The Message-ID of the message: the id of the response.
t The tags (o:tag), in request order. It is not present when the message has no tags.
v The variables: the v: parameters, and the members of h:X-Mailgun-Variables when its value is a JSON object. The names are in byte order. It is not present when the message has no variables.

The document is compact JSON, with no spaces between tokens. Each character that is not ASCII is written as a \uXXXX escape. <, > and & are not escaped. A control character (U+0000 to U+001F, and U+007F) in a tag, in a v: value, or in a string of h:X-Mailgun-Variables is refused with 400.

The document must fit in 300 characters. GreenArrow refuses a request whose document is longer with 400. GreenArrow does not cut the variables, so the events always have all of them. The m and M values count toward the 300 characters. With a Message-ID that GreenArrow makes, the document without tags and variables is 85 characters plus the length of the domain name. The escapes of the JSON encoding count too.

The X-Mailgun-Variables Header

When a request has v: parameters, the message has an X-Mailgun-Variables header with the variables as a JSON object, in the form that Mailgun writes: with a space after each , and : between tokens, and the names in byte order. For example:

X-Mailgun-Variables: {"C": "3", "a": "1", "b": "2"}

As in the Click Tracking ID, each character that is not ASCII is written as a \uXXXX escape.

h:X-Mailgun-Variables. Without v: parameters, an h:X-Mailgun-Variables header is written as given. When its value is a JSON object, its members are also variables of the message, with their JSON types. A name that is in the header and in a v: parameter gets a JSON array of the two values, the value of the header first: {"a": ["from-header", "from-v"]}. A value that is not a JSON object gives no variables, and it is not an error.

When a /messages request has both h:X-Mailgun-Variables and v: parameters, the message has one X-Mailgun-Variables header with the merged variables. A value of h:X-Mailgun-Variables that is not a JSON object is then lost. Outside a batch, /messages.mime writes both headers, as Mailgun does.

Unfold the header before you parse it. GreenArrow folds each header that is too long for one line (998 bytes):

  • It folds the X-Mailgun-Variables header that it writes at the spaces between the JSON tokens. Only when one name or one string value is too long for a line does it fold at a space inside that string.
  • It folds an h:X-Mailgun-Variables header that it writes as given at any of its spaces, also inside a string value.

A fold puts a line break in front of a space. Thus the JSON is correct only when you unfold the header (RFC 5322) before you parse it. A reader that does not unfold the header can get a string value with a line break in it, or JSON that is not valid.

Batch Sending

A request with recipient-variables is a batch. GreenArrow sends one message to each to recipient. On /messages, the To header of each message has only that recipient. (On /messages.mime, the document keeps its own To header. See Batch Sending.) Each message has the variables of its recipient in place of its placeholders. GreenArrow submits a batch to the HTTP Submission API as one template. See Individualized Messages.

curl -s --user "api:$API_KEY" \
  https://mg.api.example.com/v3/mail.example.com/messages \
  -F from='Example Team <[email protected]>' \
  -F to='Alice Adams <[email protected]>' \
  -F to='[email protected]' \
  -F subject='Hey %recipient.first_name%, your plan renews soon' \
  --form-string text='Hi %recipient.first_name%, your plan renews on %recipient.renews_on%.' \
  -F recipient-variables='{"[email protected]": {"first_name": "Alice", "renews_on": "12 Oct 2026"}, "[email protected]": {"first_name": "Bob", "renews_on": "19 Oct 2026"}}' \
  -F v:campaign_id=2026-10-renewals

Each valid value of recipient-variables makes a batch, also {} and null. With {}, each recipient gets its own copy of the same message. On /messages, the recipients do not see each other’s addresses.

Placeholders. %recipient.{name}% is replaced with the value of the recipient’s variable {name}, and %recipient% with the address of the recipient. GreenArrow replaces the placeholders in the headers of the message (also From, Subject and the h: headers), in its text and HTML parts, and in the v: values. It does not replace them in attachments, in the name of an attachment, or in the variables of h:X-Mailgun-Variables. A placeholder whose variable the recipient does not have stays as it is. The simplemh_batch_unresolved_placeholders_total metric counts these placeholders. For the placeholders in the v: values, GreenArrow also logs the names, with the addresses of up to 10 recipients.

A value is written as Mailgun writes it:

Value in recipient-variables Text in the message
A string The string, with no escapes, also in an HTML part
A number The number as it is in the JSON, for example 1.50
true, false, null True, False, None
An object or an array Compact JSON, for example {"a":1}

%recipient.address.city% is not a nested name: it is replaced only by a variable called address.city.

The recipients. A key of recipient-variables matches a to address when the two addresses are equal. The letter case does not matter. A key that matches no to address is not used. A to address that has no key gets the message with its placeholders as they are. A key whose value is not an object gives no variables.

The X-Mailgun-Recipient-Variables header. h:X-Mailgun-Recipient-Variables, or an X-Mailgun-Recipient-Variables header in a /messages.mime document, also makes a batch. Its value has the same JSON form as recipient-variables. GreenArrow unfolds the header before it reads it. When a request gives more than one of these, the variables of each recipient merge name by name. For a name in more than one, recipient-variables wins over the headers of the document, and the headers of the document win over h:X-Mailgun-Recipient-Variables. A later header of the document wins over an earlier one. GreenArrow does not deliver the header, because the header has the variables of all of the recipients. h:X-Mailgun-Recipient-Variables counts toward the 16 KB limit of the options. recipient-variables does not.

The response, the Message-ID and the variables. The response is one 200, as for one message. Each message of the batch has the same Message-ID: the id of the response. Each recipient has its own Click Tracking ID: m is a unique ID of the recipient’s message, and v has the v: values with the recipient’s placeholders replaced. v also has the variables of h:X-Mailgun-Variables, with their placeholders as they are. When these sources supply variables, the recipient’s X-Mailgun-Variables header has the same variables as v. On /messages.mime too, that header replaces the message-level h:X-Mailgun-Variables header.

Refused batches. A batch is refused with 400 when:

  • It has cc or bcc. Mailgun sends each cc and bcc recipient one copy of the message for each to recipient.
  • A variable of a to recipient has a line break (CR or LF).
  • The Click Tracking ID of one recipient is longer than 300 characters. The message names the recipient.
  • The value of recipient-variables or of the header is not a JSON object or null.

See Error Messages for the messages.

Unsupported Parameters

GreenArrow refuses these parameters with 400:

Parameter Message
template, t:version, t:text, t:variables, and each other t: parameter {name} parameter is not supported
o:deliverytime-optimize-period, o:time-zone-localize, o:archive-to, o:suppress-headers, o:inbox-placement-test-key {name} parameter is not supported
o:testmode, o:require-tls, o:skip-verification, o:tracking-pixel-location-top no and false are accepted and ignored. Another value: {name} parameter value {value} is not supported
o:dkim with no or false o:dkim parameter value {value} is not supported
A parameter that this page does not list, for example sender or o:foo {name} parameter is not supported

Differences from Mailgun

The From domain. The domain of from must be equal to {domain_name}. Mailgun accepts a From address of another domain.

Unknown parameters are refused. A parameter that this page does not list is refused with 400.

Refused headers. The h: headers that control GreenArrow (X-GreenArrow-*, X-Virtual-MTA, X-Schedule, X-Job, X-CampaignID and X-Mailer-Info), the headers that GreenArrow builds (From, To, Cc, Subject, Date, MIME-Version, Content-Type and Content-Transfer-Encoding), h:Bcc, h:Return-Path, h:Authentication-Results and h:Sender are refused.

Repeated values. A request with from or an h: header sent more than one time is refused. Mailgun writes both values.

subject is required. Mailgun accepts a request with no subject.

amp-html needs text or html. Mailgun accepts amp-html alone.

Attachments. An attachment or inline filename cannot contain " or '. An inline filename must be an RFC 5322 dot-atom-text, with or without an @ and a dot-atom-text domain: no spaces, and ASCII only. A file cannot be empty, and cannot be the one character 0.

Tags. When a request has more than 10 tags, GreenArrow keeps the first 10 in request order. Mailgun keeps 10, not in request order. A tag cannot contain a control character.

Variables. A v: value cannot contain a control character. The tags and the variables must fit in the 300-character Click Tracking ID. Mailgun cuts variables over 4 KB in its events. GreenArrow does not cut them.

X-Mailgun-Variables. A /messages request with both h:X-Mailgun-Variables and v: parameters gives one X-Mailgun-Variables header with the merged variables. Mailgun writes two headers.

The username. A username other than api is refused with 401. Mailgun accepts any username.

h:Message-ID. The value must be a valid msg-id. Mailgun accepts any value, for example hello.

o:deliverytime. It can be up to 7 days after the request. Mailgun allows 72 hours.

o:dkim. no and false are refused, because GreenArrow always signs.

o:secondary-dkim. A key that does not exist gives no second signature. It is not an error.

The 401 and 403 domain responses. These responses need GreenArrow’s Sending Domains to be in use. See “The domain in the path” in Requests and Responses. Mailgun answers 403 for a domain of the account that is not verified. GreenArrow answers 401 for a Sending Domain whose DNS records are not verified, and 403 for a verified Sending Domain whose sending is disabled.

No unsubscribe tracking. GreenArrow does not add a List-Unsubscribe header, and does not add an unsubscribe footer. It does not replace %unsubscribe_url%, %tag_unsubscribe_url% or %mailing_list_unsubscribe_url%. A List-Unsubscribe header that you send with h:List-Unsubscribe is delivered as given. A Mail Class that forces the automatic List-Unsubscribe header on adds one.

Encoded names. GreenArrow decodes the encoded-words of the charsets UTF-8, ISO-8859-1 and US-ASCII in from, to and cc. It refuses an encoded-word in another charset.

Batch sending. A batch with cc or bcc is refused. A recipient variable with a line break is refused. Mailgun accepts both. The multipart/mailgun-variables part is not supported.

Templates. template and the t: parameters are refused.