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. One cause is an unresolved placeholder in the rendered subject or body:

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

An unresolved placeholder is a bare «…» token (the guillemet characters U+00AB … U+00BB) left in the content — for example, the Smart Suggester inserted «first_name» and it was never filled in. A «…» is not a valid NLT expression: it would ship to the recipient as literal «first_name» garbage, so Synapse refuses to send it. The email log's error_message reads unresolved_placeholder; retrieve it via the Email Logs detail endpoint.

Warning

The placeholder 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. Fill in every «…» (use {the user's first_name} and the like) before sending. See NLT variables for the correct syntax.

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)