TrackMailer — Documentation
Getting started
1. Create and verify one Cloud account. 2. Select or create the organization that owns the integration. 3. Create a project for the website, shop or brand whose data must stay together. 4. Open the product workspace and confirm that it is enabled before copying credentials or sending production traffic.
That is the complete account setup. The remaining sections are implementation guides. Credentials are always project-scoped; server secrets must never be placed in browser code, public repositories or client-side environment variables.
Before you integrate
Transactional sending is live only for enabled organizations with an active subscription or service entitlement, a verified sender domain and an active Live key. Contact writes are available with the contacts:write scope. Sandbox requests validate the same project contracts without storing contacts or delivering mail.
Create and store an API key
Open TrackMailer → API keys in the Cloud workspace. An organization owner or administrator selects Sandbox or Live, the smallest required scopes, allowed sender domains and an expiry of 1–365 days. The complete token is shown once; store it in the calling server's secret manager.
Set TRACKMAILER_API_URL to https://us-east4-markengroup-cloud-services.cloudfunctions.net/trackmailerApi. Use contacts:write for contacts, transactional:send for transactional delivery and sequences:enroll for newsletter-group sequences. Store the token on your server; rotate it by verifying a replacement before revoking the old key. Send Authorization: Bearer <token> with JSON requests.
Create or update a contact
POST the contacts.upsert operation to $TRACKMAILER_API_URL/v1/requests. A Live key persists the project-scoped contact; a Sandbox key returns status=validated, sent=false and persisted=false. Email identifies the contact inside the project, while externalId can hold the stable identifier from your source system.
Attributes must match fields previously defined in the workspace. Consent requires status, source and an ISO-8601 timestamp that is not in the future. A successful Live response contains status=stored, contactId and revision.
curl --request POST "$TRACKMAILER_API_URL/v1/requests" \
--header "Authorization: Bearer $TRACKMAILER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"operation": "contacts.upsert",
"contact": {
"email": "alex@example.com",
"name": "Alex Rivera",
"externalId": "crm_123",
"attributes": {},
"listIds": [
"LIST_ID"
],
"consent": {
"status": "subscribed",
"source": "checkout",
"recordedAt": "2026-09-08T15:00:00Z"
}
}
}'Synchronize ModelBoost account preferences
Use contacts.sync only from the trusted ModelBoostMDE backend with a contacts:write key. externalId is the stable Firebase UID; email is the current address and previousEmail is the optional lookup hint during an address change. listIds is the complete desired set of project-owned recipient groups, so omitted IDs are removed. Regular audience groups created in the workspace and explicit newsletter lists are accepted; other list kinds are rejected.
The consent source must be modelboost_account_preferences. A newer explicit subscribed action can reverse a normal unsubscribe for the selected groups. Bounce, complaint and unsolicited-marketing suppressions remain effective. Identical retries return the existing contactId and revision; an older preference timestamp or conflicting identity returns HTTP 409.
{
"code": "curl --request POST \"$TRACKMAILER_API_URL/v1/requests\" \\\n --header \"Authorization: Bearer $TRACKMAILER_API_KEY\" \\\n --header \"Content-Type: application/json\" \\\n --data '{\n \"operation\": \"contacts.sync\",\n \"contact\": {\n \"externalId\": \"FIREBASE_UID\",\n \"email\": \"alex.new@example.com\",\n \"previousEmail\": \"alex@example.com\",\n \"name\": \"Alex Rivera\",\n \"listIds\": [\n \"RECIPIENT_GROUP_ID\"\n ],\n \"consent\": {\n \"status\": \"subscribed\",\n \"source\": \"modelboost_account_preferences\",\n \"recordedAt\": \"2026-09-22T15:00:00Z\"\n }\n }\n}'",
"language": "bash",
"codeExamples": [
{
"id": "curl",
"label": "cURL",
"language": "bash",
"code": "curl --request POST \"$TRACKMAILER_API_URL/v1/requests\" \\\n --header \"Authorization: Bearer $TRACKMAILER_API_KEY\" \\\n --header \"Content-Type: application/json\" \\\n --data '{\n \"operation\": \"contacts.sync\",\n \"contact\": {\n \"externalId\": \"FIREBASE_UID\",\n \"email\": \"alex.new@example.com\",\n \"previousEmail\": \"alex@example.com\",\n \"name\": \"Alex Rivera\",\n \"listIds\": [\n \"RECIPIENT_GROUP_ID\"\n ],\n \"consent\": {\n \"status\": \"subscribed\",\n \"source\": \"modelboost_account_preferences\",\n \"recordedAt\": \"2026-09-22T15:00:00Z\"\n }\n }\n}'"
},
{
"id": "node",
"label": "Node.js",
"language": "javascript",
"code": "const response = await fetch(`${process.env.TRACKMAILER_API_URL}/v1/requests`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${process.env.TRACKMAILER_API_KEY}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n \"operation\": \"contacts.sync\",\n \"contact\": {\n \"externalId\": \"FIREBASE_UID\",\n \"email\": \"alex.new@example.com\",\n \"previousEmail\": \"alex@example.com\",\n \"name\": \"Alex Rivera\",\n \"listIds\": [\n \"RECIPIENT_GROUP_ID\"\n ],\n \"consent\": {\n \"status\": \"subscribed\",\n \"source\": \"modelboost_account_preferences\",\n \"recordedAt\": \"2026-09-22T15:00:00Z\"\n }\n }\n }),\n});\n\nif (!response.ok) throw new Error(`Request failed: ${response.status}`);\nconsole.log(await response.json());"
},
{
"id": "python",
"label": "Python",
"language": "python",
"code": "import os\nimport requests\n\npayload = {\n \"operation\": \"contacts.sync\",\n \"contact\": {\n \"externalId\": \"FIREBASE_UID\",\n \"email\": \"alex.new@example.com\",\n \"previousEmail\": \"alex@example.com\",\n \"name\": \"Alex Rivera\",\n \"listIds\": [\n \"RECIPIENT_GROUP_ID\",\n ],\n \"consent\": {\n \"status\": \"subscribed\",\n \"source\": \"modelboost_account_preferences\",\n \"recordedAt\": \"2026-09-22T15:00:00Z\",\n },\n },\n}\n\nresponse = requests.post(\n f\"{os.environ['TRACKMAILER_API_URL']}/v1/requests\",\n headers={\n \"Authorization\": f\"Bearer {os.environ['TRACKMAILER_API_KEY']}\",\n \"Content-Type\": \"application/json\",\n },\n json=payload,\n timeout=10,\n)\nresponse.raise_for_status()\nprint(response.json())"
},
{
"id": "php",
"label": "PHP",
"language": "php",
"code": "<?php\n\n$payload = [\n 'operation' => 'contacts.sync',\n 'contact' => [\n 'externalId' => 'FIREBASE_UID',\n 'email' => 'alex.new@example.com',\n 'previousEmail' => 'alex@example.com',\n 'name' => 'Alex Rivera',\n 'listIds' => [\n 'RECIPIENT_GROUP_ID',\n ],\n 'consent' => [\n 'status' => 'subscribed',\n 'source' => 'modelboost_account_preferences',\n 'recordedAt' => '2026-09-22T15:00:00Z',\n ],\n ],\n];\n$curl = curl_init(getenv('TRACKMAILER_API_URL') . '/v1/requests');\n\ncurl_setopt_array($curl, [\n CURLOPT_POST => true,\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_HTTPHEADER => [\n 'Authorization: Bearer ' . getenv('TRACKMAILER_API_KEY'),\n 'Content-Type: application/json',\n ],\n CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),\n CURLOPT_TIMEOUT => 10,\n]);\n\n$response = curl_exec($curl);\nif ($response === false) {\n throw new RuntimeException(curl_error($curl));\n}\n$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);\ncurl_close($curl);\nif ($status >= 400) {\n throw new RuntimeException(\"Request failed: $status\");\n}\necho $response;"
}
]
}Add to or remove from a list
Create the manual list under TrackMailer → Audiences → Groups and copy its displayed list ID. contacts.upsert remains intentionally additive: subsequent listIds are merged with existing membership, and an empty array does not remove prior memberships.
The narrowly scoped contacts.sync operation is the replace-membership contract for the ModelBoostMDE account-preferences integration. Other integrations must continue to use contacts.upsert or the dashboard and must not assume additive upsert removes memberships.
Unsubscribe and suppression
To record an unsubscribe from your own preference center, send contacts.upsert with consent.status=unsubscribed, the source and the recorded time. This suppresses the contact; it is different from merely removing a list membership. Existing unsubscribed, bounced or complained contacts are never reactivated by a later import.
curl --request POST "$TRACKMAILER_API_URL/v1/requests" \
--header "Authorization: Bearer $TRACKMAILER_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"operation": "contacts.upsert",
"contact": {
"email": "alex@example.com",
"name": "Alex Rivera",
"attributes": {},
"listIds": [],
"consent": {
"status": "unsubscribed",
"source": "preference-center",
"recordedAt": "2026-09-08T16:00:00Z"
}
}
}'Prepare a sender domain
The workspace automatically accepts the domain of your verified login email and its subdomains; no platform-admin approval is required for setup. Add the exact sender host, such as mail.example.com, under TrackMailer → Sender domains. Publish the project ownership TXT plus the DKIM, MAIL FROM MX and SPF records shown in the workspace, then wait for the complete verification to pass. Use the displayed 64-character senderDomainId in keys and send requests.
Use a dedicated subdomain for each relevant brand or stream. Every sender host still requires its own project proof and complete verification; an API key can only send through senderDomainIds explicitly attached to it.
Anti-Spam Health: DKIM, SPF and DMARC
TrackMailer reports domain readiness, authentication and delivery signals. Copy DNS values exactly and coordinate DMARC policy with the organization's mail administrator. Authentication reduces avoidable classification problems but cannot guarantee inbox placement.
Send a transactional email
POST from a trusted server to $TRACKMAILER_API_URL/v1/email/send with a Live key that has transactional:send and allows the selected senderDomainId. The request supports 1–20 unique recipients, HTML and plain text, an optional reply-to address and the purposes receipt, security or notification.
Set a unique Idempotency-Key header of 8–128 characters for the logical message. Repeating the same key and identical body returns the original messageId without creating another delivery; reusing it with different content returns HTTP 409 idempotency_conflict. A new accepted request returns HTTP 202 with status=queued.
curl --request POST "$TRACKMAILER_API_URL/v1/email/send" \
--header "Authorization: Bearer $TRACKMAILER_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: order_8472_confirmation_v1" \
--data '{
"senderDomainId": "SENDER_DOMAIN_ID",
"fromLocalPart": "orders",
"fromName": "Example Shop",
"to": [
{
"email": "alex@example.com",
"name": "Alex Rivera"
}
],
"replyTo": {
"email": "support@example.com",
"name": "Support"
},
"subject": "We received your order",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"purpose": "receipt",
"tags": {
"MessageType": "order_confirmation",
"Environment": "production"
}
}'Validate before sending
Use a Sandbox key against /v1/requests for contacts.upsert, contacts.sync, transactional.send, sequence.enroll. Sandbox validates schema, scope and project resources but never stores, sends, enrolls or schedules. Production delivery endpoints require a Live key.
Responses, status and retries
Store requestId and messageId from every accepted send. Inspect the TrackMailer Sending view for queued, sent, delivered, bounced, complained, suppressed, failed or uncertain outcomes. HTTP 202 means durably queued, not delivered and not guaranteed to reach the inbox.
Retry 429 after the Retry-After value. Retry transient 503 responses with bounded exponential backoff and the same idempotency key and body. Do not retry 400 validation errors, 401 invalid keys, 403 scope/sender denials, 402 billing or entitlement failures, or 409 sender/suppression/idempotency conflicts until their cause is corrected.