TrueEngageDocsStart free

Send a transactional message

Send an OTP or confirmation to one customer through a transactional endpoint.

Updated 18 Oct 2026 3 min read

Use a transactional endpoint for one-to-one messages the customer is waiting for, such as OTPs, order confirmations and password resets. These sends skip frequency caps and do-not-disturb, and they go out at high priority.

POST https://api.true-engage.com/v1/transactional/{slug}/send

Before you start

  1. In the console, go to Engage, then Transactional, and create an endpoint. It is tied to a template and has a slug.
  2. Note the slug shown there. It goes in the URL.
  3. Create a server key with the campaigns.create_manage scope. See API keys.

The endpoint's settings decide whether it also bypasses opt-out. By default it does not, so consent is still respected.

Request body

Field Type Required Notes
idempotencyKey string Yes Up to 200 characters. A repeat within 24 hours returns the first result and sends nothing new.
userId string One of userId or profileId Your external id for the customer. Preferred.
profileId string (UUID) One of userId or profileId The TrueEngage profile id, when you have it.
to string Conditional An email address or E.164 phone number. Use it when the profile has no address, or with an unknown userId to create the profile. Up to 320 characters.
variables object Conditional Values for the template, such as { "otp": "483920" }. Every variable the endpoint marks as required must be present.
deviceUid string No The originating device. Honoured only if the endpoint accepts it and the device belongs to this user.
platform string No android, ios or web.

Example

curl -X POST https://api.true-engage.com/v1/transactional/login-otp/send \
  -u "test_xxxxxxxx:YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "C10093",
    "variables": { "otp": "483920", "expiryMinutes": 10 },
    "idempotencyKey": "otp-C10093-1760779800"
  }'
const auth = Buffer.from(`${process.env.TE_APP_KEY}:${process.env.TE_SECRET}`).toString('base64');

const res = await fetch('https://api.true-engage.com/v1/transactional/login-otp/send', {
  method: 'POST',
  headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    userId: 'C10093',
    variables: { otp: '483920', expiryMinutes: 10 },
    idempotencyKey: `otp-C10093-${Date.now()}`,
  }),
});

Response

Status 202 Accepted. The message is queued for delivery.

{
  "requestId": "0b1c6f2e-6a1d-4c58-9f47-2b7d6d1f8a10",
  "data": { "dispatchJobId": "7c4f1d52-9b0e-4f3a-8f7e-1a2b3c4d5e6f", "deduplicated": false }
}

deduplicated: true means this idempotencyKey was already used and nothing new was sent.

Accepted does not mean delivered. Delivery results appear on the user's profile timeline and in Analytics.

Errors

Status Code Cause
404 TRANSACTIONAL_ENDPOINT_NOT_FOUND No endpoint with this slug in the key's environment.
422 TRANSACTIONAL_VARIABLES_MISSING A required variable is absent. data.missing lists them.
404 USER_NOT_FOUND No profile matches, and no to was given to create one.
429 RATE_LIMITED Over 6,000/min per key, or over the endpoint's own per-minute limit. Honour Retry-After.
401 or 403 INVALID_API_KEY, INSUFFICIENT_SCOPE, KEY_KIND_NOT_ALLOWED Credential problem. Only server keys may call this endpoint.

See Errors.

Tips

  • Build the idempotencyKey from something stable in your system, such as the OTP request id, so a retry never sends a second SMS.
  • Use Test keys while you build. In Test, only people on the allowlist receive messages. See Test and Live.