Mailgun-Compatible Domains API
- Table of Contents
- Overview
- Authentication
- Requests and Responses
- The Domain Object
- Get a List of Domains
- Get a Single Domain
- Create a Domain
- Update a Domain
- Verify a Domain
- Delete a Domain
- Change or Remove the IP Pool of a Domain
- Identifiers
- Differences from Mailgun
This feature is only available on GreenArrow CloudMTA.
Contact us if you need support for a field or an API that is not listed here.
Overview
The Mailgun-compatible Domains API lets software that was written for Mailgun’s domains API manage GreenArrow Sending Domains. It uses the Sending Domains API to manage domains and the Check DNS API to verify them. A domain that you create here is an ordinary Sending Domain, and a Sending Domain that you create in another way is visible here.
These endpoints are available:
| Endpoint | Purpose |
|---|---|
GET /v4/domains |
Get a list of domains |
GET /v4/domains/{name} |
Get a single domain |
POST /v4/domains |
Create a domain |
PUT /v4/domains/{name} |
Update a domain |
PUT /v4/domains/{name}/verify |
Verify the DNS records of a domain |
DELETE /v3/domains/{name} |
Delete a domain |
DELETE /v3/domains/{name}/pool/ip_pool |
Change or remove the IP pool of a domain |
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.
To send GreenArrow events to software that receives Mailgun webhooks, see Mailgun-Compatible Webhooks.
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/v4/domains"
A request with no credentials or with a username other than api gets a 401 response. A request with a key that is not valid also gets a 401 response, unless GreenArrow refuses the request for another reason first: a request that is not valid can get its 400 or 404 response before the key is checked. Do not depend on the body of a 401 response; it can change in a later release.
Requests and Responses
Request format
- The create and update endpoints take their fields in a
multipart/form-databody (curl -F), as Mailgun documents. A body of another type, includingapplication/x-www-form-urlencoded, is refused with400. This is also true when the request has no fields. - The other endpoints take their parameters in the query string. They do not read a request body.
- A field of the body that is sent in the query string is refused with
400. - A create or update body may not be larger than 1 MiB. A body that is larger than 32 MiB can get a
502with an HTML body. - A field or parameter listed for an endpoint may be given only once.
- A boolean field accepts
true,yesor1, andfalse,noor0, in any letter case. - A field or a parameter that this page does not list is ignored.
Domain names
The {name} of a path is the domain name. Its letter case does not matter. A name that has only digits is never found (404).
Errors
An error response has a JSON body, except the plain-text 404 for a path that is not a path of the Mailgun API (see the table below), and the 502 for a body that is larger than 32 MiB. Most error bodies have one field, message:
{"message": "Domain not found"}
A 401 for a request that has no credentials that can be read has another body, as on Mailgun:
{"Error": "unauthorized"}
| Status | Meaning |
|---|---|
400 |
The request is not valid, or it asks for something that GreenArrow does not support. When the problem is one field, the message usually names it. |
401 |
The credentials are missing or not valid. |
404 |
No domain has this name, the path is not a path of the Mailgun API, or the path does not support the HTTP method of the request. As on Mailgun, a path that is not a path of the Mailgun API gets the plain-text body 404 page not found, and a wrong method gets {"error": "not found"}. |
429 |
The domain was verified less than one minute ago. See Verify a Domain. |
500 |
GreenArrow could not handle the request. The message is GreenArrow backend error. Contact GreenArrow support.
|
501 |
The path is a path of the Mailgun API that GreenArrow does not implement. |
502 |
The request body is larger than 32 MiB. |
When GreenArrow and Mailgun refuse a request for the same reason, the message is Mailgun’s. Other messages are GreenArrow’s own, for example a message about a feature that GreenArrow does not support. Use the status code to handle an error. Do not depend on the text of a message.
The Domain Object
Each endpoint that returns a domain returns it in this form.
|
id
string |
The identifier of the domain: 24 hexadecimal characters. See Identifiers. |
|
name
string |
The domain name. |
|
created_ string |
The time when the domain was created, in GMT, for example |
|
state
string |
|
|
is_ boolean |
|
|
tracking_ string |
The hostname of the click and open tracking links of the domain. |
|
web_ string |
The first label of |
|
message_ integer |
The time that GreenArrow keeps a message in its queue, in seconds. This is a setting of the GreenArrow installation, not of the domain. It is the queue_lifetime of |
|
type
string |
Always |
|
use_ boolean |
Always |
|
web_ string |
Always |
|
spam_ string |
Always |
|
require_ boolean |
Always |
|
skip_ boolean |
Always |
|
wildcard
boolean |
Always |
|
webhooks_ boolean |
Always |
|
encrypt_ boolean |
Always |
|
smtp_ string |
Always an empty string. |
|
subaccount_ string |
Always an empty string. |
|
archive_ null |
Always |
|
disabled
null |
Always |
Mailgun’s dkim_host, mailfrom_host and smtp_password fields are not returned.
Domain States
GreenArrow derives state from three attributes of the Sending Domain.
sending_enabled |
dns_verified |
dns_regression |
state |
is_disabled |
state filter value |
|---|---|---|---|---|---|
false |
any | any | inactive |
true |
disabled |
true |
false |
any | unverified |
false |
unverified |
true |
true |
false |
active |
false |
active |
true |
true |
true |
unverified |
false |
unverified |
- A domain whose DNS records were verified and later became wrong (a regression) reports
unverified, notactive. - A disabled domain reports the state
inactive, but thestatefilter of the list selects it with the valuedisabled.
DNS Records
The endpoints that return one domain also return sending_dns_records: the DNS records that you must create for the domain. The list endpoint does not return them.
|
sending_ array of hashes
|
|||||||||||||||
|
receiving_ array |
Always empty. |
||||||||||||||
The tracking record, when present, comes first, followed by the DKIM records. A Sending Domain that uses a Shared Tracking Domain has no tracking record of its own in this list.
Get a List of Domains
GET /v4/domains
Parameters
|
limit
integer /optional |
The maximum number of domains to return, from |
|
skip
integer /optional |
The number of domains to skip, |
|
state
string /optional |
Return only the domains in this state: |
|
sort
string /optional |
|
|
search
string /optional |
Return only the domains whose name contains this text. The letter case does not matter. |
|
authority
string /optional |
Not supported. A value that is not blank is refused with |
|
include_ string /optional |
Ignored. GreenArrow has no subaccounts. |
Response
|
total_ integer |
The number of domains that match the request, on all pages. |
|
items
array of hashes |
The domains of this page. Each one is a domain object. |
Example
curl -s --user "api:$API_KEY" \
"https://mg.api.example.com/v4/domains?limit=100&skip=0&state=active&sort=name:asc"
{
"total_count": 1,
"items": [
{
"archive_to": null,
"created_at": "Mon, 14 Sep 2026 09:12:33 GMT",
"encrypt_incoming_message": false,
"id": "387cb321000000000000002a",
"is_disabled": false,
"message_ttl": 172800,
"name": "mail.example.com",
"require_tls": false,
"skip_verification": false,
"smtp_login": "",
"spam_action": "disabled",
"state": "active",
"subaccount_id": "",
"tracking_host": "trk.mail.example.com",
"type": "custom",
"use_automatic_sender_security": true,
"webhooks_redact_pii": false,
"web_prefix": "trk",
"web_scheme": "https",
"wildcard": false,
"disabled": null
}
]
}
Get a Single Domain
GET /v4/domains/{name}
The response has the domain (domain object), its sending_dns_records and an empty receiving_dns_records (see DNS Records).
The DNS results in this response are the results of the last DNS check that GreenArrow made. This request does not check DNS. GreenArrow checks DNS when the domain is created and then every 15 minutes. Use Verify a Domain to check at once.
Example
curl -s --user "api:$API_KEY" "https://mg.api.example.com/v4/domains/mail.example.com"
{
"domain": {
"archive_to": null,
"created_at": "Mon, 14 Sep 2026 09:12:33 GMT",
"encrypt_incoming_message": false,
"id": "387cb321000000000000002a",
"is_disabled": false,
"message_ttl": 172800,
"name": "mail.example.com",
"require_tls": false,
"skip_verification": false,
"smtp_login": "",
"spam_action": "disabled",
"state": "active",
"subaccount_id": "",
"tracking_host": "trk.mail.example.com",
"type": "custom",
"use_automatic_sender_security": true,
"webhooks_redact_pii": false,
"web_prefix": "trk",
"web_scheme": "https",
"wildcard": false,
"disabled": null
},
"sending_dns_records": [
{
"record_type": "CNAME",
"name": "trk.mail.example.com",
"value": "trk.abc123def456.dnsforemail.com",
"valid": "valid",
"is_active": true,
"cached": ["trk.abc123def456.dnsforemail.com"]
},
{
"record_type": "CNAME",
"name": "ga._domainkey.mail.example.com",
"value": "ga.abc123def456.dnsforemail.com",
"valid": "valid",
"is_active": true,
"cached": ["ga.abc123def456.dnsforemail.com"]
},
{
"record_type": "CNAME",
"name": "ga2._domainkey.mail.example.com",
"value": "ga2.abc123def456.dnsforemail.com",
"valid": "valid",
"is_active": false,
"cached": ["ga2.abc123def456.dnsforemail.com"]
}
],
"receiving_dns_records": []
}
A domain that does not exist gets a 404:
{"message": "Domain not found"}
Create a Domain
POST /v4/domains
Supported Fields
|
name
string /required |
The domain name. It may not be the name of a domain that exists. |
|
dkim_ string /optional |
The selector of the first DKIM key of the domain. GreenArrow gives each domain two DKIM keys, so it makes the selector of the second key from this value: it adds The value may not be empty, and it may not be |
|
dkim_ string /optional |
The length of the DKIM keys in bits: |
|
web_ string /optional |
The first label of the tracking hostname of the domain. With The value may not contain a dot or an underscore, it may not be |
|
pool_ string /optional |
The identifier of the IP pool that sends the mail of this domain. In GreenArrow an IP pool is a Route. See Identifiers for how to make this value from the ID of a Route. A value that is not the identifier of a Route is refused with Without this field, the domain has no Route, and its mail uses the VirtualMTA of its Mail Class. |
|
use_ boolean /optional |
Ignored. The DKIM records of a domain that this API creates are always CNAME records. |
The DKIM selectors, the DKIM key size and web_prefix cannot be changed after the domain is created.
Unsupported Fields
GreenArrow does not act on these fields. For most of them, it accepts one value and refuses any other value with 400. The accepted value is the value that the domain object reports, or false for a field that the domain object does not have. Thus software that reads a domain and sends back each field that is not null does not fail. For archive_to, dkim_host_name, ips and smtp_password, GreenArrow accepts no value: leave the field out.
| Field | Accepted | Refused |
|---|---|---|
web_scheme |
https |
http |
spam_action |
disabled |
tag, block
|
require_tls |
false |
true |
skip_verification |
false |
true |
wildcard |
false |
true |
webhooks_redact_pii |
false |
true |
encrypt_incoming_message |
false |
true |
force_dkim_authority |
false |
true |
force_root_dkim_host |
false |
true |
message_ttl |
the message_ttl that the domain object reports |
any other number |
archive_to |
no value | any value, also an empty one |
dkim_host_name |
no value | any value, also an empty one |
ips |
no value | any value, also an empty one |
smtp_password |
no value | any value, also an empty one |
Mailgun’s default for web_scheme is http. An integration that sends web_scheme=http must change to https or leave the field out, because the tracking links of GreenArrow always use HTTPS.
A refused value gets a message like this one:
{"message": "skip_verification=true is not supported: GreenArrow accepts only skip_verification=false"}
A field for which no value is accepted gets a shorter message:
{"message": "archive_to is not supported"}
Response
The response has message (Domain DNS records have been created), the domain (domain object), its sending_dns_records and an empty receiving_dns_records.
GreenArrow checks the DNS records of a new domain before it answers. You have not created the records at that time, so the new domain is unverified and each record is invalid. Create the records that sending_dns_records lists, then verify the domain or wait for the next automatic check.
Example
curl -s --user "api:$API_KEY" -X POST "https://mg.api.example.com/v4/domains" \
-F name=mail.example.com \
-F dkim_selector=ga \
-F dkim_key_size=2048 \
-F web_prefix=trk \
-F pool_id=387cb322000000000000000b
{
"message": "Domain DNS records have been created",
"domain": {
"archive_to": null,
"created_at": "Wed, 16 Sep 2026 14:02:11 GMT",
"encrypt_incoming_message": false,
"id": "387cb321000000000000002a",
"is_disabled": false,
"message_ttl": 172800,
"name": "mail.example.com",
"require_tls": false,
"skip_verification": false,
"smtp_login": "",
"spam_action": "disabled",
"state": "unverified",
"subaccount_id": "",
"tracking_host": "trk.mail.example.com",
"type": "custom",
"use_automatic_sender_security": true,
"webhooks_redact_pii": false,
"web_prefix": "trk",
"web_scheme": "https",
"wildcard": false,
"disabled": null
},
"sending_dns_records": [
{
"record_type": "CNAME",
"name": "trk.mail.example.com",
"value": "trk.abc123def456.dnsforemail.com",
"valid": "invalid",
"is_active": true,
"cached": []
},
{
"record_type": "CNAME",
"name": "ga._domainkey.mail.example.com",
"value": "ga.abc123def456.dnsforemail.com",
"valid": "invalid",
"is_active": false,
"cached": []
},
{
"record_type": "CNAME",
"name": "ga2._domainkey.mail.example.com",
"value": "ga2.abc123def456.dnsforemail.com",
"valid": "invalid",
"is_active": false,
"cached": []
}
],
"receiving_dns_records": []
}
A domain name that exists gets a 400:
{"message": "while creating domain: domain already exists"}
Update a Domain
PUT /v4/domains/{name}
No setting of a domain can be changed through this endpoint. The endpoint exists so that software that sends an update with the values that a domain already has does not fail. It checks the fields of the request and returns the domain as it is.
Fields
|
web_ string /optional |
Accepted only if it equals the |
|
use_ boolean /optional |
|
|
mailfrom_ string /optional |
Not supported. Any value, also an empty one, is refused with |
The fields web_scheme, spam_action, require_tls, skip_verification, wildcard, webhooks_redact_pii, message_ttl, archive_to and smtp_password have the same rules as on create. See Unsupported Fields.
The request needs a multipart/form-data body, also when it has no fields.
Response
The response has message (Domain has been updated), the domain (domain object), its sending_dns_records and an empty receiving_dns_records. As on Get a Single Domain, the DNS results are those of the last DNS check.
Example
curl -s --user "api:$API_KEY" -X PUT "https://mg.api.example.com/v4/domains/mail.example.com" \
-F require_tls=false \
-F web_prefix=trk
The response is the response of Get a Single Domain with one more field at the start:
{
"message": "Domain has been updated",
"domain": { ... },
"sending_dns_records": [ ... ],
"receiving_dns_records": []
}
Verify a Domain
PUT /v4/domains/{name}/verify
Checks the DNS records of the domain now, and returns the domain with the new results. The request has no fields.
The response has message (Domain DNS records have been updated), the domain (domain object), its sending_dns_records and an empty receiving_dns_records.
Rate Limit
A domain can be verified once in 60 seconds. This is the limit of the Check DNS API, so an immediate check of the same domain name through that API counts too. The DNS check that GreenArrow makes when a domain is created, the check that it makes every 15 minutes and a recheck from the web interface do not count: you can verify a new domain at once.
A request that is over the limit does not check DNS. It gets a 429 response:
{"message": "Too many verification attempts for this domain, please try again in 42 seconds"}
The response has these headers:
| Header | Value |
|---|---|
X-RateLimit-Limit |
1 |
X-RateLimit-Remaining |
0 |
X-RateLimit-Reset |
The time when the domain can be verified again, as a Unix time in seconds |
Example
curl -s --user "api:$API_KEY" -X PUT "https://mg.api.example.com/v4/domains/mail.example.com/verify"
In this response, one DKIM record is still missing:
{
"message": "Domain DNS records have been updated",
"domain": {
"name": "mail.example.com",
"state": "unverified",
...
},
"sending_dns_records": [
{
"record_type": "CNAME",
"name": "trk.mail.example.com",
"value": "trk.abc123def456.dnsforemail.com",
"valid": "valid",
"is_active": true,
"cached": ["trk.abc123def456.dnsforemail.com"]
},
{
"record_type": "CNAME",
"name": "ga._domainkey.mail.example.com",
"value": "ga.abc123def456.dnsforemail.com",
"valid": "valid",
"is_active": false,
"cached": ["ga.abc123def456.dnsforemail.com"]
},
{
"record_type": "CNAME",
"name": "ga2._domainkey.mail.example.com",
"value": "ga2.abc123def456.dnsforemail.com",
"valid": "invalid",
"is_active": false,
"cached": []
}
],
"receiving_dns_records": []
}
Delete a Domain
DELETE /v3/domains/{name}
Deletes the domain. The request has no fields.
{"message": "Domain will be deleted in the background"}
The message is Mailgun’s. GreenArrow deletes the domain before it answers, so the domain is gone when you get this response.
A domain that is disabled (is_disabled is true) is not deleted, as on Mailgun. The request gets a 400:
{"message": "Domain is disabled and cannot be deleted"}
Example
curl -s --user "api:$API_KEY" -X DELETE "https://mg.api.example.com/v3/domains/mail.example.com"
Change or Remove the IP Pool of a Domain
DELETE /v3/domains/{name}/pool/ip_pool
This is Mailgun’s endpoint that unlinks a domain from its IP pool. In GreenArrow an IP pool is a Route.
Parameters
|
pool_ string /optional |
The identifier of the IP pool that replaces the current one. See Identifiers. A value that is not the identifier of a Route is refused with Without this parameter, the domain is left with no Route, and its mail uses the VirtualMTA of its Mail Class. Mailgun’s description of this endpoint can be read as if a replacement pool is required. It is optional. |
|
ip
string /optional |
Not supported. A request that has this parameter is refused with |
The last part of the path must be ip_pool. Mailgun also accepts all or an IP address there. GreenArrow refuses those with 400.
Response
{"message": "success"}
Example
curl -s --user "api:$API_KEY" -X DELETE \
"https://mg.api.example.com/v3/domains/mail.example.com/pool/ip_pool?pool_id=387cb322000000000000000b"
Identifiers
A Mailgun identifier is a string of 24 hexadecimal characters. GreenArrow makes such an identifier from the integer ID of its own object:
- The first 8 characters give the type of the object:
387cb321for a domain (a Sending Domain) and387cb322for an IP pool (a Route). - The last 16 characters are the GreenArrow ID in hexadecimal, with zeros at the start.
| GreenArrow object | Mailgun identifier |
|---|---|
| Sending Domain 42 | 387cb321000000000000002a |
| Route 11 | 387cb322000000000000000b |
To use a Route as the pool_id of a request, get its ID from the Routes API and write it in this form.
An identifier that Mailgun made is never the identifier of a GreenArrow object. A pool_id from your Mailgun account is refused with 400.
Differences from Mailgun
DNS records. Mailgun asks for MX records, a TXT record for SPF, a TXT record for one DKIM key, and a CNAME record for tracking. For a domain that this API creates, GreenArrow asks for CNAME records only: one for click, open and bounce tracking, and one for each of its two DKIM keys. A Sending Domain that was created in another way can have TXT records for its DKIM keys. Software that expects Mailgun’s record types must be changed to create the records that sending_dns_records lists. Contact us if you need the records in Mailgun’s form.
Tracking links always use HTTPS. web_scheme is always https. A request with web_scheme=http, which is Mailgun’s default, is refused.
Settings that cannot be changed. web_prefix, the DKIM selectors and the DKIM key size of a domain are set when the domain is created. The update endpoint changes nothing.
Two DKIM keys. Each domain has two DKIM keys and two selectors. See dkim_selector in Create a Domain for how the second selector is made. mx is not a valid selector and not a valid web_prefix.
DNS regression. A domain whose verified DNS records later became wrong reports the state unverified.
Get and verify. Getting a domain returns the stored results of the last DNS check. Verifying a domain starts a new check, as does creating one.
Verification is limited. A domain can be verified once in 60 seconds.
Deletion is immediate. The domain is deleted before the response, although the message says “in the background”.
The replacement pool is optional. Removing the IP pool of a domain needs no pool_id.
TLS and other fixed settings. require_tls, skip_verification, wildcard, spam_action, webhooks_redact_pii and message_ttl have one fixed value each. See Unsupported Fields.
No incoming mail features. receiving_dns_records is always empty, and spam_action, archive_to, encrypt_incoming_message, smtp_login and smtp_password have no function.
Fields that are not returned. The domain object has no dkim_host, mailfrom_host or smtp_password.
Identifiers. The id of a domain and a pool_id have Mailgun’s form, but GreenArrow makes them. See Identifiers.
Error messages. Some 400 messages are GreenArrow’s own, because Mailgun has no such error, or because Mailgun’s message has data of a Mailgun account. See Requests and Responses.
Endpoints that are not available. Only the /v4 endpoints read domains: GET /v3/domains and GET /v3/domains/{name} are not served. GET /v3/domains/{name}/ips, POST /v3/domains/{name}/ips and DELETE /v3/domains/{name}/ips/{ip} are not available. Mailgun’s domain keys (DKIM) endpoints are not part of this API. GET /v3/ip_pools and POST /v3/ip_pools are on the Mailgun-Compatible IP Pool API page. Mailgun’s other IP pool endpoints are not available yet. Manage IP pools with the Routes API.
