Messaging providers

Send a store's text and WhatsApp messages through your SMS or WhatsApp service.

A messaging provider sends a store's text messages, WhatsApp messages, or both: sign-in codes, order updates, gift cards, abandoned checkout reminders and campaigns. Tringify writes each message and calls your app once per message; your app sends it through the merchant's account with your service.

Declare it#

json
"providers": [
  {
    "service_type": "sms",
    "protocol_version": "1",
    "operations": ["send"],
    "endpoints": { "send": "https://example.app/messages/send" },
    "supports_test_mode": false
  },
  {
    "service_type": "whatsapp",
    "protocol_version": "1",
    "operations": ["send"],
    "endpoints": { "send": "https://example.app/messages/send" },
    "supports_test_mode": false
  }
]

Declare sms, whatsapp or both. Both can use the same endpoint: each request names its channel. Messaging needs no Store API scopes.

How a store uses it#

  1. The merchant installs your app and connects their account in your app's pages.

  2. Under Apps → Service Providers, they choose your app as the store's SMS provider, WhatsApp provider, or both. A store has one active provider per channel.

  3. Under Marketing → Messages, they choose the channel each message goes by. Text messages are the default.

Choosing your app doesn't prove the account works. Check credentials, senders and templates when the merchant saves them, and refuse sends you can't make.

The send request#

Tringify POSTs one JSON message to your send endpoint and waits up to 30 seconds for the answer. Use a shorter timeout for your own call to the service so you can always answer.

json
{
  "message_id": "c5653f18-d74c-4cb4-997b-8fbccda25f53",
  "channel": "whatsapp",
  "to": "+919812345678",
  "body": "Your order #1042 has shipped.",
  "category": "transactional",
  "template_type": "order_shipped",
  "variables": { "order_number": "#1042" },
  "store_id": "31289f25-9ddd-4521-9bcf-a1953edbec84",
  "store_name": "Example Store"
}

Field

Meaning

message_id

Unique per message and the same on every retry. Use it to send each message once.

channel

sms or whatsapp. Send by that channel only.

to

The recipient's number in E.164 format, such as +919812345678.

body

The finished text. Keep its characters exactly.

category

transactional or marketing. Follow your service's and the destination country's rules for each.

template_type

The kind of message, for picking a pre-approved template where your service requires one, as WhatsApp and Indian SMS senders do: login_otp, order_confirmation, order_shipped, order_delivered, order_cancelled, order_updated, order_status_update, order_tracking_code, payment_refunded, payment_failed, gift_card_delivery, restock_notification, review_request, checkout_recovery or promotional_campaign.

variables

Text values for the template's placeholders.

store_id

The store. Check it against X-Tringify-Store-ID.

store_name

For display only.

Verify each request#

Requests to your endpoints are signed exactly like app webhooks, so one function verifies both. Read the raw body before parsing it. X-Tringify-Signature has the form t=<timestamp>,v1=<hex digest> and may carry more than one v1 value: compute HMAC-SHA256 with your app's webhook signing key over <timestamp>.<raw body>, compare it in constant time with each v1 value, accept the request if any one matches, and reject a timestamp more than five minutes from your clock, in either direction. The payment protocol reference has a worked example to test your verifier against, and explains signing-key rotation: today a new key replaces the old one immediately, so deploy it right after generating it; a coming change adds a 72-hour overlap during which requests carry a v1 for both the current and the previous key.

Header

Use

X-Tringify-Signature

t=<timestamp>,v1=<hex HMAC-SHA256 of timestamp.raw body>.

X-Tringify-Timestamp

Unix seconds; equals t. Reject requests more than five minutes from your clock, in either direction.

X-Tringify-App-ID

Must be your app ID.

X-Tringify-Store-ID

Must equal store_id in the body.

X-Tringify-Request-ID

For your logs. Not an idempotency key.

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTringifyRequest(rawBody, headers, signingKey, appId) {
  const timestamp = headers.get("X-Tringify-Timestamp") || "";
  const parts = (headers.get("X-Tringify-Signature") || "").split(",");
  // One v1 per signing key: accept the request if any of them matches.
  const signatures = parts.filter((part) => /^v1=[a-f0-9]{64}$/.test(part))
    .map((part) => Buffer.from(part.slice(3), "hex"));
  if (!/^\d{10}$/.test(timestamp) || parts[0] !== `t=${timestamp}` || signatures.length === 0 ||
      Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 ||
      headers.get("X-Tringify-App-ID") !== appId) return false;
  const expected = createHmac("sha256", signingKey).update(`${timestamp}.`).update(rawBody).digest();
  return signatures.some((signature) => timingSafeEqual(expected, signature));
}

Answer#

json
{ "success": true, "provider_name": "example-sms", "provider_message_id": "msg-784213" }
json
{
  "success": false,
  "provider_name": "example-sms",
  "error_code": "TEMPLATE_NOT_MAPPED",
  "error_message": "Map this message type to an approved template.",
  "retryable": false
}

Field

Meaning

success

Required. true only once your service has accepted the message.

provider_message_id

Your service's reference for an accepted message.

provider_name

Optional name of your service.

error_code, error_message

Why it failed, without credentials, phone numbers or message text. Merchants see them.

retryable

On failure: false stops retries; true or leaving it out lets Tringify retry.

Answer with HTTP 200 whenever you return this JSON, including failures. Accepted is not delivered: protocol 1 has no way to report delivery back to Tringify, so never describe an accepted message as delivered.

Retries#

If your answer says it can be retried, or your endpoint fails, times out or returns something unreadable, Tringify tries again with the same message_id: up to 10 attempts in all, starting 30 seconds apart and growing to 15 minutes apart.

  • Claim each (store_id, message_id) atomically before calling your service, and store the outcome, so concurrent and repeated requests send once and get the same answer.

  • If you can't tell whether your service accepted a message, for example after a timeout, don't send it again. Answer success: false, error_code: "SUBMISSION_UNKNOWN", retryable: false, and check your service's reports.

  • Before each attempt Tringify checks that your app is still the store's provider for that channel. Messages queued for a provider the merchant removed or replaced are not sent.

Test#

Case

Expected

Wrong or old signature

No call to your service.

The same message_id twice, at once or after a restart

One send; both answers carry the same provider_message_id.

Missing template or variable

A clear failure with retryable: false, before calling your service.

Timeout after sending

SUBMISSION_UNKNOWN, no second send.

Provider removed in Store Admin

Queued messages stop.

For a real send, use a number you're allowed to message and an approved sender and template, on a development store.