Skip to content

User Sender Domain API Documentation

User-area sender domain management endpoints for creating, listing, retrieving, updating, deleting, verifying, and inspecting the DNS records of from-address domains owned by the authenticated user.

These are the user-owned, list-bound sender domains visible at /app/user/senderdomains/, with per-domain DNS records (CNAME / A / MX / TXT) for SPF/DMARC, an MFROM/return-path subdomain, an optional tracking subdomain, and a verification flow that performs live DNS lookups. They are distinct from the email-gateway delivery domains managed via the emailgateway.*domain* endpoints, and from the admin-only sender.domains endpoint, which returns a flat cross-tenant dump used for PowerMTA provisioning.

List User Sender Domains

GET /api/v1/user.senderdomains

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

Returns only user-owned, addressable sender domains. The synthetic group-default sender domain (DomainID=0) that the model normally prepends is intentionally excluded — every entry in the response has a real DomainID and is valid for Get / Update / Delete / Verify / DNS.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.list
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
bash
curl -X GET "https://example.com/api/v1/user.senderdomains?APIKey=your-api-key"
json
{
  "Success": true,
  "SenderDomains": [
    {
      "DomainID": "12",
      "SenderDomain": "example.com",
      "UserID": "1",
      "CreatedAt": "2026-04-25 22:54:00",
      "Status": "Enabled",
      "VerificationMeta": {
        "DNSRecords": {
          "sl.example.com": ["CNAME", "tracking-host.octeth.example.", true]
        }
      },
      "PolicyMeta": [],
      "Options": {
        "LinkTracking": 1,
        "OpenTracking": 1,
        "UnsubscribeLink": 0,
        "CustomSubdomain": "sl",
        "CustomTrackPrefix": "track",
        "TrackPrefixDisabled": false
      }
    }
  ]
}
json
{
  "Errors": [
    {
      "Code": 0,
      "Message": "Authentication failed"
    }
  ]
}
txt
(none — this endpoint has no business-logic error codes; an empty result returns SenderDomains: [])

Get a Sender Domain

GET /api/v1/user.senderdomain

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
DomainIDIntegerYesThe ID of the sender domain to retrieve. Must be owned by the calling user
bash
curl -X GET "https://example.com/api/v1/user.senderdomain?APIKey=your-api-key&DomainID=12"
json
{
  "Success": true,
  "SenderDomain": {
    "DomainID": "12",
    "SenderDomain": "example.com",
    "UserID": "1",
    "CreatedAt": "2026-04-25 22:54:00",
    "Status": "Enabled",
    "VerificationMeta": {
      "DNSRecords": {
        "sl.example.com": ["CNAME", "tracking-host.octeth.example.", true]
      }
    },
    "PolicyMeta": [],
    "Options": {
      "LinkTracking": 1,
      "OpenTracking": 1,
      "UnsubscribeLink": 0,
      "CustomSubdomain": "sl",
      "CustomTrackPrefix": "track",
      "TrackPrefixDisabled": false
    }
  }
}
json
{
  "Errors": [
    {
      "Code": 2,
      "Message": "Sender domain not found"
    }
  ]
}
txt
1: Missing DomainID parameter
2: Sender domain not found (also returned when the domain exists but is owned by another user — to avoid leaking ownership)

Create a Sender Domain

POST /api/v1/user.senderdomain

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

The new domain starts in Status='Approval Pending'. The response includes the auto-generated VerificationMeta.DNSRecords array — these are the CNAME / A / MX / TXT records the user must add to their DNS zone before the domain can be verified.

If a (SenderDomain, UserID) pair already exists (e.g. it was previously soft-deleted), the model uses ON DUPLICATE KEY UPDATE to refresh the row's Status to Approval Pending rather than failing.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.create
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SenderDomainStringYesThe domain name to register (e.g. example.com). Cannot be localhost
LinkTrackingBooleanNoWhether to enable click tracking on links sent from this domain. Default: true
OpenTrackingBooleanNoWhether to enable open tracking. Default: true
UnsubscribeLinkBooleanNoWhether to inject the unsubscribe link automatically. Default: false
CustomSubdomainStringNoOverride the default MFROM/return-path subdomain. Letters, digits, and hyphens only; max 32 chars; no leading/trailing hyphens
CustomTrackPrefixStringNoOverride the default click-tracking subdomain prefix. Same character rules as CustomSubdomain
bash
curl -X POST https://example.com/api/v1/user.senderdomain \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "user.senderdomain.create",
    "APIKey": "your-api-key",
    "SenderDomain": "example.com",
    "LinkTracking": true,
    "OpenTracking": true,
    "UnsubscribeLink": false
  }'
json
{
  "Success": true,
  "DomainID": 12,
  "SenderDomain": {
    "DomainID": "12",
    "SenderDomain": "example.com",
    "UserID": "1",
    "CreatedAt": "2026-04-26 09:30:00",
    "Status": "Approval Pending",
    "VerificationMeta": {
      "DNSRecords": {
        "sl.example.com": ["CNAME", "tracking-host.octeth.example."],
        "track-sl.example.com": ["CNAME", "tracking-host.octeth.example."]
      }
    },
    "PolicyMeta": [],
    "Options": {
      "LinkTracking": 1,
      "OpenTracking": 1,
      "UnsubscribeLink": 0
    }
  }
}
json
{
  "Errors": [
    {
      "Code": 5,
      "Message": "Sender domain limit reached for your account"
    }
  ]
}
txt
1: Missing SenderDomain parameter
4: localhost is not a valid sender domain
5: Sender domain limit reached for your account (returned with HTTP 403)
6: Invalid CustomSubdomain or CustomTrackPrefix (letters, digits, and hyphens only; max 32 chars; no leading/trailing hyphens)
7: Sender domain create process failed (returned with HTTP 400)
8: Sender domain could not be retrieved after creation (returned with HTTP 500)

Update a Sender Domain

PATCH /api/v1/user.senderdomain

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported
  • PATCH requests must send all parameters in a JSON body (Content-Type: application/json). The dispatcher does not parse query-string or form-encoded payloads for PATCH, so ?DomainID=... and ?APIKey=... will be ignored — include them in the JSON body alongside the other update fields.

This is a partial update. Only fields explicitly present in the request body are modified — omitted fields keep their currently stored value (the API does not reset them to defaults).

If the request changes any DNS-affecting field (CustomSubdomain, CustomTrackPrefix, or UseTrackingSubdomain / TrackPrefixDisabled), the server automatically:

  1. Regenerates the VerificationMeta.DNSRecords template via RegenerateDNSRecords.
  2. Resets Status to Approval Pending (the previous DNS verification is no longer valid for the new subdomain layout).
  3. Invalidates the Redis verification cache for this domain.
  4. Sets DnsRegenerated: true in the response so the caller knows DNS records changed.

The user must then update their DNS zone with the new records and call user.senderdomain.verify to re-verify.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.update
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
DomainIDIntegerYesThe ID of the sender domain to update. Must be owned by the calling user
LinkTrackingBooleanNoToggle click tracking. Omitted: stored value preserved
OpenTrackingBooleanNoToggle open tracking. Omitted: stored value preserved
UnsubscribeLinkBooleanNoToggle automatic unsubscribe-link injection. Omitted: stored value preserved
UseTrackingSubdomainBooleanNoWhether to use a separate tracking subdomain. Internally stored as the inverse TrackPrefixDisabled flag
CustomSubdomainStringNoNew override for the MFROM/return-path subdomain. Letters, digits, and hyphens only; max 32 chars; no leading/trailing hyphens. Triggers DNS regeneration
CustomTrackPrefixStringNoNew override for the tracking subdomain prefix. Same character rules. Triggers DNS regeneration
bash
curl -X PATCH https://example.com/api/v1/user.senderdomain \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "user.senderdomain.update",
    "APIKey": "your-api-key",
    "DomainID": 12,
    "CustomSubdomain": "em",
    "UseTrackingSubdomain": true
  }'
json
{
  "Success": true,
  "DnsRegenerated": true,
  "SenderDomain": {
    "DomainID": "12",
    "SenderDomain": "example.com",
    "UserID": "1",
    "CreatedAt": "2026-04-25 22:54:00",
    "Status": "Approval Pending",
    "VerificationMeta": {
      "DNSRecords": {
        "em.example.com": ["CNAME", "tracking-host.octeth.example."],
        "track-em.example.com": ["CNAME", "tracking-host.octeth.example."]
      }
    },
    "PolicyMeta": [],
    "Options": {
      "LinkTracking": 1,
      "OpenTracking": 1,
      "UnsubscribeLink": 0,
      "CustomSubdomain": "em",
      "TrackPrefixDisabled": false
    }
  }
}
json
{
  "Errors": [
    {
      "Code": 6,
      "Message": "Invalid CustomSubdomain. Letters, digits, and hyphens only; max 32 chars; no leading/trailing hyphens"
    }
  ]
}
txt
1: Missing DomainID parameter
2: Sender domain not found (also returned when the domain exists but is owned by another user)
6: Invalid CustomSubdomain or CustomTrackPrefix (letters, digits, and hyphens only; max 32 chars; no leading/trailing hyphens)
7: Sender domain could not be retrieved after update (returned with HTTP 500)

Delete a Sender Domain

DELETE /api/v1/user.senderdomain

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

Soft delete — the row is not removed from the database. The model sets Status='Deleted' and fires the Delete.SenderDomain plugin hook so listeners can run cleanup logic (e.g., remove the domain from active campaigns). Subsequent Get / List calls will not return the domain.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.delete
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
DomainIDIntegerYesThe ID of the sender domain to delete. Must be owned by the calling user
bash
curl -X DELETE "https://example.com/api/v1/user.senderdomain?APIKey=your-api-key&DomainID=12"
json
{
  "Success": true
}
json
{
  "Errors": [
    {
      "Code": 2,
      "Message": "Sender domain not found"
    }
  ]
}
txt
1: Missing DomainID parameter
2: Sender domain not found (also returned when the domain exists but is owned by another user)

Verify a Sender Domain

POST /api/v1/user.senderdomain.verify

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

Performs a live DNS lookup for each expected record (CNAME / A / MX / TXT). Results are cached for 60 seconds in Redis to avoid hammering DNS resolvers — calling this endpoint twice in quick succession will return the same outcome.

The endpoint persists Status based on the result (mirrors what the UI's edit page does on every load):

  • All records resolve correctly → Status = 'Enabled'
  • Any record fails → Status = 'Approval Pending'

The latest per-record verified flags are merged into the existing VerificationMeta (other keys are preserved) so subsequent Get / DNS calls reflect the same state shown in the UI. The endpoint short-circuits and skips the database write entirely when neither the Status nor the per-record verified flags have changed since the last probe.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.verify
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
DomainIDIntegerYesThe ID of the sender domain to verify. Must be owned by the calling user
bash
curl -X POST https://example.com/api/v1/user.senderdomain.verify \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "user.senderdomain.verify",
    "APIKey": "your-api-key",
    "DomainID": 12
  }'
json
{
  "Success": true,
  "DomainID": 12,
  "IsVerified": true,
  "Status": "Enabled",
  "DNSRecords": {
    "sl.example.com": ["CNAME", "tracking-host.octeth.example.", true],
    "track-sl.example.com": ["CNAME", "tracking-host.octeth.example.", true]
  }
}
json
{
  "Errors": [
    {
      "Code": 2,
      "Message": "Sender domain not found"
    }
  ]
}
txt
1: Missing DomainID parameter
2: Sender domain not found (also returned when the domain exists but is owned by another user)

Get DNS Records for a Sender Domain

GET /api/v1/user.senderdomain.dns

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: User.Update
  • Rate limit: 100 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

Read-only. Returns the stored VerificationMeta.DNSRecords (the records the user needs to add to their DNS zone), normalized to a structured shape: { host: { Type, Value, IsVerified } }.

IsVerified is tri-state:

  • null — this record has never been probed (e.g. domain was just created or its subdomain settings just changed)
  • true — the last probe by user.senderdomain.verify matched the expected value
  • false — the last probe ran but did not match (DNS not configured yet, or misconfigured)

The records are kept fresh automatically — when Update changes a DNS-affecting setting, the server regenerates them. If the stored records are missing/empty (legacy or just-created edge case), this endpoint falls back to regenerating the template purely for display, without persisting.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: user.senderdomain.dns
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
DomainIDIntegerYesThe ID of the sender domain. Must be owned by the calling user
bash
curl -X GET "https://example.com/api/v1/user.senderdomain.dns?APIKey=your-api-key&DomainID=12"
json
{
  "Success": true,
  "DomainID": 12,
  "SenderDomain": "example.com",
  "DNSRecords": {
    "sl.example.com": {
      "Type": "CNAME",
      "Value": "tracking-host.octeth.example.",
      "IsVerified": true
    },
    "track-sl.example.com": {
      "Type": "CNAME",
      "Value": "tracking-host.octeth.example.",
      "IsVerified": null
    }
  }
}
json
{
  "Errors": [
    {
      "Code": 2,
      "Message": "Sender domain not found"
    }
  ]
}
txt
1: Missing DomainID parameter
2: Sender domain not found (also returned when the domain exists but is owned by another user)

Any questions? Contact us.