Skip to content

Direct Send API

Send a transactional email immediately without creating a flow. Ideal for one-off emails like OTP codes, password resets, payment receipts, and order confirmations.


Endpoint

POST https://synapse-api.pyrx.tech/v1/send

Headers

HeaderRequiredDescription
Content-TypeYesapplication/json
X-WORKSPACE-IDYesYour workspace identifier
X-API-KEYYesA data-scoped API key

Request Body

json
{
"template_slug": "otp-verification",
"to": {
"user_id": "user_12345",
"email": "[email protected]"
},
"attributes": {
"otp_code": "123456",
"expiry_minutes": 10
},
"idempotency_key": "otp_user_12345_1711900800"
}

Parameters

FieldTypeRequiredDescription
template_slugstringYesThe slug of the email template to render
to.user_idstringYesThe contact's external identifier. Used to upsert the contact record.
to.emailstringYesThe recipient's email address.
to.first_namestringNoFirst name for NLT personalization.
to.last_namestringNoLast name for NLT personalization.
attributesobjectNoData available in NLT templates as trigger event attributes
idempotency_keystringNoPrevents duplicate sends
subjectstringNoThe subject for this send. Highest-priority source in the subject resolution chain — when set, it wins over the template's stored subject. When omitted, Synapse falls back to the template's stored subject, then to an empty subject.
from_emailstringNoOverrides the sender address for this send.
sender_namestringNoOverrides the sender display name for this send.
reply_tostringNoSets the Reply-To address for this send.
bccstring[]NoBCC recipients for this send.
content_typestringNotransactional or promotional. Controls CAN-SPAM footer behavior — see CAN-SPAM footer and suppression. Defaults to transactional (no footer) when omitted.
Note

Both to.user_id and to.email are required. The contact is upserted by user_id with the provided email and name fields.


Response

The response always contains exactly two fields:

FieldTypeDescription
email_log_idUUIDThe email log record created for this send. Query it via the Email Logs API.
statusstringsent, failed, or suppressed.

Sent (200 OK)

json
{
"email_log_id": "8f14e45f-ceea-467f-a83c-01a01ba3c5db",
"status": "sent"
}

Suppressed (200 OK)

status is suppressed when Synapse deliberately does not send the email — the log record is still written for audit. This happens when:

  • A required NLT expression in the template resolves to null (a required attribute is missing), or
  • The send is commercial but your workspace has no complete company address (see CAN-SPAM footer and suppression).
json
{
"email_log_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "suppressed"
}

The specific suppression reason is recorded on the email log's error_message (for example, suppressed:missing_company_address). Retrieve it via the Email Logs detail endpoint.

Failed (200 OK)

status is failed when the send reached a terminal state without delivering — the log record is still written for audit. Two causes are checked, in order:

json
{
"email_log_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "failed"
}

1. Unresolved placeholder

An unresolved placeholder is malformed personalization syntax left in the content that would otherwise ship to the recipient as literal garbage. Synapse refuses to send it. Two kinds are caught:

  • A bare «…» token (the guillemet characters U+00AB … U+00BB) — for example, the Smart Suggester inserted «first_name» and it was never filled in. A «…» is not a valid NLT expression. This is caught in both the rendered subject and the rendered body.
  • An unclosed { brace in an NLT expression — for example, a body containing Hi {the user's first name with no closing }. This would leak the raw {…} fragment to the recipient. It is caught in the rendered subject and the rendered body.

Either kind sets the email log's error_message to unresolved_placeholder; retrieve it via the Email Logs detail endpoint.

Warning

The «…» guard also decodes HTML entity forms of the guillemets before scanning — «/» (with or without the trailing ;) and numeric character references such as « / « all count as a placeholder, because a mail client would render them as a real guillemet. Always close every NLT expression's brace and fill in every «…» (use {the user's first_name} and the like) before sending. See NLT variables for the correct syntax.

Note

Well-formed, balanced braces in an email HTML body — CSS rules like .btn{color:#1D9E75}, @media queries, or JSON-LD <script> blocks — are never mistaken for a placeholder and never block a send. Only an unclosed brace in the body is caught there.

2. Empty subject

Checked only after the placeholder check passes. If the resolved subject is empty or whitespace-only after the full subject resolution chain (request subject → template's stored subject), Synapse fails the send closed rather than deliver an email with a blank Subject: header:

json
{
"email_log_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "failed"
}

Sets the email log's error_message to render_suppressed:empty_subject; retrieve it via the Email Logs detail endpoint.

Note

An explicit "subject": "" in the request body does not count as a deliberate override that stops the chain — it's treated the same as omitting subject entirely, and falls through to the template's stored subject. Only a non-empty string, in either the request or the template, resolves the chain. Set a subject on the request or the template to avoid this.

Error Responses

StatusCodeDescription
400validation_errorMissing required fields
422literal_errorA field failed schema validation — for example, content_type set to a value other than transactional or promotional
401invalid_api_keyInvalid or missing API key
403insufficient_scopeAPI key lacks data scope
404template_not_foundTemplate slug does not exist or is inactive
404contact_not_foundNo contact found with the given external_id (and no email fallback)
409duplicate_sendIdempotency key already used
429rate_limit_exceededRate limit exceeded

Examples

OTP Verification

bash
curl -X POST https://synapse-api.pyrx.tech/v1/send \
-H "Content-Type: application/json" \
-H "X-WORKSPACE-ID: ws_k7x9m2p4" \
-H "X-API-KEY: psk_live_a1b2c3d4e5f67890abcdef1234567890" \
-d '{
"template_slug": "otp-verification",
"to": {
"user_id": "user_12345",
"email": "[email protected]"
},
"attributes": {
"otp_code": "847293",
"expiry_minutes": 10
},
"idempotency_key": "otp_user_12345_1711900800"
}'

Python

python
import requests
 
response = requests.post(
"https://synapse-api.pyrx.tech/v1/send",
headers={
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
json={
"template_slug": "otp-verification",
"to": {
"user_id": "user_12345",
"email": "[email protected]",
},
"attributes": {
"otp_code": "847293",
"expiry_minutes": 10,
},
"idempotency_key": f"otp_user_12345_{int(time.time())}",
},
)
 
result = response.json()
print(result["status"]) # "sent"

JavaScript

javascript
const response = await fetch("https://synapse-api.pyrx.tech/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
body: JSON.stringify({
template_slug: "otp-verification",
to: {
user_id: "user_12345",
email: "[email protected]",
},
attributes: {
otp_code: "847293",
expiry_minutes: 10,
},
idempotency_key: `otp_user_12345_${Math.floor(Date.now() / 1000)}`,
}),
});
 
const result = await response.json();
console.log(result.status); // "sent"

Priority Routing

Direct send emails are routed through the priority queue system based on the template's content_type:

Content TypePriority QueueUse Case
OTP/2FA templatesq.email.criticalNever delayed
Transactionalq.email.highDelivered within seconds
Promotionalq.email.lowMay be batched
Tip

For OTP and password reset emails, use direct send with a transactional template. These are routed to the critical priority queue with dedicated workers for sub-second delivery.


Synapse enforces CAN-SPAM at send time. The content_type field decides whether a send is treated as commercial. It accepts only transactional, promotional, or omission — any other value is rejected with 422 validation_error.

content_typeTreated asFooter + List-Unsubscribe
transactionalTransactionalNo — exempt
(omitted)TransactionalNo — exempt
promotionalCommercialYes — footer + one-click unsubscribe
(any other value)—Rejected with 422 before sending

/v1/send is the transactional send surface, so an omitted content_type is treated as transactional and stays exempt. Send promotional for genuinely commercial mail.

What a commercial send adds

When a send is commercial, Synapse automatically:

  • Appends a footer containing your workspace's physical mailing address and an unsubscribe link, immediately before the closing </body> tag.
  • Adds List-Unsubscribe and List-Unsubscribe-Post headers for one-click unsubscribe (see the Unsubscribe API).

You do not build or template any of this — pass content_type: "promotional" and Synapse injects it.

Fail-closed on a missing address

Warning

A commercial send from a workspace with no complete company address is suppressed — the response status is suppressed and no email is sent. A commercial email without a physical mailing address is itself a CAN-SPAM violation, so Synapse refuses to send it rather than ship a non-compliant message.

The required fields are your company name, address line 1, city, postal code, and country. Set them in Settings → General before sending commercial email. Transactional sends are never blocked by this check.

json
{
"email_log_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "suppressed"
}

The email log's error_message will read suppressed:missing_company_address.


Direct Send vs Flows

ConcernDirect SendFlows
TriggerExplicit API callAutomatic on matching event
TimingImmediateCan include wait steps
BranchingSingle emailMulti-step with conditions
Use caseOTP, receipts, alertsOnboarding, follow-ups, campaigns
IdempotencyPer-send keyPer-flow-trip (automatic)