Skip to content
Splashify Pro
Docs

Send Authentication Template

Send a WhatsApp authentication template — typically a one-time password (OTP) flow with a copy-code or one-tap autofill button. Authentication templates have their own Meta-approved structure (no header, fixed body, special button) and a separate (lower) per-message price than utility / marketing.

These are the right pick for: login OTPs, signup verification, transaction confirmation codes, password resets.

Endpoint

POST

https://api.splashifypro.com/api/v1/public/message

Headers

The value is your raw sk_live_… key — Splashify accepts it as-is, you do not need to base64-encode it. The Basic prefix is required. See Authentication.

Before your first send

The endpoint silently returns errors that look generic if any of these are missing — verify all four:

  1. WABA configured. Connect your WhatsApp Business Account in Settings → WhatsApp Setup. Without it the API returns 400 "WhatsApp Business Account not configured".
  2. Template APPROVED by Meta and synced into your workspace. Check Templates — the row must show APPROVED, not PENDING / REJECTED. Newly created templates can take a few minutes to a few hours for Meta review.
  3. Plan supports Public API. Growth, Advanced, or Enterprise — not Starter. See Plan availability.
  4. Wallet has balance. Authentication-category sends are billed per message. Top up at Billing → Wallet if you see 402 "Insufficient balance".

Request body

json
{
  "fullPhoneNumber": "+919028883545",
  "callbackData": "auth_attempt_42",
  "type": "Template",
  "template": {
    "name": "itk_auth_one_tap",
    "bodyValues": ["483920"]
  }
}

languageCode, template_category, and buttonValues are all filled in for you:

  • languageCode and template_category are read from your stored template by name.
  • buttonValues["0"] is auto-filled from bodyValues[0] for authentication templates — the OTP is always the same in both, so there's no reason to repeat it. You only need to send buttonValues if (rarely) you want the copy / one-tap value to differ from the message body.

The structure of buttonValues is {"<button_index>": ["value", ...]}. For an authentication template the OTP button is always at index 0, and the value array has one entry — the OTP code itself.

You don't construct the Meta envelope. The backend reads your stored template metadata, automatically picks sub_type: "url" (one-tap) vs "copy_code" based on how the template was approved, and builds the full Cloud-API payload for you. You only supply the values.

Examples

cURL

bash
curl -X POST https://api.splashifypro.com/api/v1/public/message \
  -H "Authorization: Basic YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fullPhoneNumber": "+919028883545",
    "callbackData": "login_attempt_42",
    "type": "Template",
    "template": {
      "name": "itk_auth_one_tap",
      "bodyValues": ["483920"]
    }
  }'

Node.js / TypeScript

text
async function sendLoginOtp(phone: string, otp: string) {
  const res = await fetch(
    "https://api.splashifypro.com/api/v1/public/message",
    {
      method: "POST",
      headers: {
        "Authorization": `Basic ${process.env.SPLASHIFY_API_KEY!}`,
        "Content-Type":  "application/json",
      },
      body: JSON.stringify({
        fullPhoneNumber: phone,            // "+919028883545"
        callbackData:    `login:${phone}`, // echoed in webhooks
        type: "Template",
        template: {
          name:       "itk_auth_one_tap",
          bodyValues: [otp],
        },
      }),
    },
  );

  const data = await res.json();
  if (!data.result) throw new Error(`${res.status}: ${data.message}`);
  return data.id; // Splashify message ID — correlate with delivery webhooks
}

Python

python
import os, requests

def send_login_otp(phone: str, otp: str) -> str:
    r = requests.post(
        "https://api.splashifypro.com/api/v1/public/message",
        headers={
            "Authorization": f"Basic {os.environ['SPLASHIFY_API_KEY']}",
            "Content-Type":  "application/json",
        },
        json={
            "fullPhoneNumber": phone,
            "callbackData":    f"login:{phone}",
            "type": "Template",
            "template": {
                "name":       "itk_auth_one_tap",
                "bodyValues": [otp],
            },
        },
        timeout=30,
    )
    data = r.json()
    if not data.get("result"):
        raise RuntimeError(f"{r.status_code}: {data.get('message')}")
    return data["id"]

Successful response — 200 OK

json
{
  "result": true,
  "message": "Message created successfully",
  "id": "b90f02bd-1316-49ce-b5d0-f6be14fce2b8"
}

The id is a Splashify message UUID — store it to correlate with message.sent / message.delivered / message.read / message.failed webhooks (see Webhooks).

Error responses

The endpoint returns granular errors — read the message field, don't just check the status code.

Diagnosing a 502 from a calling proxy (Supabase Edge Function, Cloudflare Worker, etc.)

If the calling layer reports a generic 5xx (e.g. Supabase Auth says "Unexpected status code returned from hook: 502"), that error is from the proxy, not Splashify. The actual Splashify response is the body of the upstream call. Log it:

text
const res = await fetch("https://api.splashifypro.com/api/v1/public/message", { /* ... */ });
if (!res.ok) {
  const body = await res.text();
  console.error("Splashify send failed", { status: res.status, body });
  // body will contain { result: false, message: "..." } — that's the real error
  return new Response(body, { status: res.status });
}

The most common cause of opaque 502s through a proxy is using the wrong endpoint — e.g. posting to a Meta Graph path such as /api/v25.0/{PHONE_NUMBER_ID}/messages with Bearer sk_live_…. We do not host that path. Workspace sk_live_… keys only authenticate on /api/v1/public/... with Authorization: Basic ….

Integration recipe: Supabase Auth send-sms-hook

A common login flow: Supabase Auth owns OTP generation, storage, and verification; Splashify just delivers the WhatsApp message. Wire it as a custom send-sms-hook Edge Function.

text
// supabase/functions/send-sms-hook/index.ts
import { serve } from "https://deno.land/std/http/server.ts";
import { createHmac } from "node:crypto";

const HOOK_SECRET = Deno.env.get("SEND_SMS_HOOK_SECRET")!;
const API_KEY     = Deno.env.get("SPLASHIFY_API_KEY")!;
const TEMPLATE    = Deno.env.get("SPLASHIFY_TEMPLATE_NAME") ?? "otp";
// Optional — only set if you need to override the language stored on the template.
const LANGUAGE    = Deno.env.get("SPLASHIFY_TEMPLATE_LANGUAGE");

serve(async (req) => {
  // 1. Verify Supabase Auth's signed payload (standard webhook HMAC).
  //    Skip this in dev only — never in production.
  const sig = req.headers.get("webhook-signature") ?? "";
  const raw = await req.text();
  const expected = createHmac("sha256", HOOK_SECRET).update(raw).digest("hex");
  if (sig !== expected) {
    return new Response("invalid signature", { status: 401 });
  }

  const { user, sms } = JSON.parse(raw) as {
    user: { id: string; phone: string };
    sms:  { otp: string; sms_type: string };
  };

  // 2. Send the WhatsApp auth template.
  const phone = user.phone.startsWith("+") ? user.phone : `+${user.phone}`;
  const splash = await fetch(
    "https://api.splashifypro.com/api/v1/public/message",
    {
      method: "POST",
      headers: {
        "Authorization": `Basic ${API_KEY}`,
        "Content-Type":  "application/json",
      },
      body: JSON.stringify({
        fullPhoneNumber: phone,
        callbackData:    `auth:${user.id}`,
        type: "Template",
        template: {
          name:       TEMPLATE,
          ...(LANGUAGE ? { languageCode: LANGUAGE } : {}),
          bodyValues: [sms.otp],
        },
      }),
    },
  );

  // 3. Surface the real error if the send fails — don't return generic 5xx.
  if (!splash.ok) {
    const body = await splash.text();
    console.error("Splashify send failed", { status: splash.status, body });
    return new Response(body, { status: splash.status });
  }

  return new Response(JSON.stringify({ ok: true }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
});

Set the secrets:

bash
supabase secrets set \
  SPLASHIFY_API_KEY=sk_live_your_rotated_key \
  SPLASHIFY_TEMPLATE_NAME=otp \
  SEND_SMS_HOOK_SECRET=$(openssl rand -hex 32)
# SPLASHIFY_TEMPLATE_LANGUAGE is optional — set it only to override
# the language stored on the template (e.g. for multi-language OTPs).

Then enable the hook in Supabase Dashboard → Authentication → Hooks → Send SMS hook with the URL of your deployed function and the same SEND_SMS_HOOK_SECRET.

You do not need SPLASHIFY_PHONE_NUMBER_ID. The Public API resolves the sending WABA from your workspace via the API key — there is no per-call phone-number-ID parameter on /api/v1/public/message.

On the client side, login is then just:

text
await supabase.auth.signInWithOtp({ phone: "+919028883545" });
// user types the 6 digits they read in WhatsApp:
await supabase.auth.verifyOtp({ phone, token, type: "sms" });

Supabase generates, stores, and verifies the OTP; this hook only delivers it.

Pricing & best practice

  • Authentication templates are billed at the auth-conversation rate (lower than utility/marketing) when sent via Meta-approved authentication shapes.
  • The OTP is reused for the copy / one-tap button automatically. WhatsApp needs the same value in the body and the button; for authentication templates the backend copies bodyValues[0] into buttonValues["0"] when you don't send one. Only pass buttonValues yourself if you have a deliberate reason for the two to differ.
  • Do not embed the OTP in the URL of any link button — Meta rejects auth templates with that pattern.
  • template_category is resolved from your stored template — pass "authentication" explicitly only if you want to lock the send so a silent re-categorisation by Meta surfaces as an explicit error instead of an accidental marketing-priced send.
  • Generate the OTP server-side (Supabase Auth, your own service, etc.), store its hash with a short TTL (typically 5–10 minutes), and verify on the way back. Don't trust the client to echo it.
  • Rate-limit OTP requests per phone in your own layer (e.g. 1 per 60 seconds per number) — WhatsApp also rate-limits OTP pairs and will block repeated sends to the same recipient.