Techvein OneKey for developers

Send WhatsApp from your own software, through one key.

OneKey sits between your system and WhatsApp. You talk to a small JSON API over HTTPS; OneKey takes care of the number, the approved templates, the 24-hour rule, consent and delivery — and tells you what happened by webhook. Three ways in, depending on what you are building.

Website Chat on the page, or on WhatsApp

Paste one script tag for an assistant that answers parents on your site, and a "Chat on WhatsApp" button that lands in the right inbox.

publishable key · no server needed
ERP & CRM Notices, OTPs, reminders from your backend

Server-to-server: sync the roll, send templates in bulk, reply inside the window, receive delivery and reply webhooks.

secret key · webhooks
CMS WordPress, Drupal, any site builder

The widget and the button go in the theme; a notice you publish can go out as a broadcast from a small server hook.

script tag · optional batch send
Base URL
https://whatsapp.techvein.in/v1

Replace the host with the one you were given for production.

Auth
Authorization: Bearer tvk_…

A secret key stays on your server. A publishable key (tvp_…) may sit in a web page and can only chat.

Format
application/json

Phone numbers in E.164 (+919876543210); a bare 10-digit Indian number is accepted and normalised.

Where keys come from
Console → Integrations

A school issues its own keys from its console; a platform or partner gets one from Techvein. Shown once — only a hash is kept.

Quick start — a real message in five minutes

  1. Get a key with the right scopes. For sending you need messages:send; add messages:read to poll status and webhooks:manage to be told instead of polling. Ask for the smallest set that does the job — admin is for Techvein's own tooling.
  2. Pick an approved template. Outside a 24-hour window WhatsApp only allows templates Meta has approved. List what is ready on your number:
    curl https://whatsapp.techvein.in/v1/templates \
      -H "Authorization: Bearer tvk_live_…"
    Each template lists its variables and the languages it is approved in.
  3. Send one.
    curl -X POST https://whatsapp.techvein.in/v1/messages \
      -H "Authorization: Bearer tvk_live_…" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: fee-2026-09-4410" \
      -d '{
        "to": "+919876543210",
        "template": "fee_due_reminder",
        "variables": { "parent_name": "Mrs. Sharma", "student_name": "Aarav",
                       "amount": "12,400", "due_date": "12 Sep 2026" }
      }'
    The answer is 202 — accepted, not yet delivered:
    { "id": "msg_01J…", "status": "queued",
      "sender": { "number": "+919065513341", "display_name": "TatvaOS" },
      "template": { "name": "fee_due_reminder", "language": "en", "category": "utility" },
      "conversation": { "id": "cnv_01J…", "window_expires_at": null, "billable": true },
      "scheduled_for": null, "held_for": null }
    Everything that can fail fast — no approved template on this number, an opted-out contact, a closed window, a blown quota — fails here, synchronously, with a named error (see Errors). Send the same request twice with the same Idempotency-Key and you get the same message back, not a second one.
  4. Learn what happened. Either poll GET /v1/messages/msg_01J…, or register a webhook once and receive every change:
    curl -X POST https://whatsapp.techvein.in/v1/webhooks \
      -H "Authorization: Bearer tvk_live_…" -H "Content-Type: application/json" \
      -d '{ "url": "https://erp.example.in/hooks/onekey",
            "events": ["message.status", "message.received"] }'
    The response carries the signing secret — shown once. Statuses arrive in order: submitted → sent → delivered → read, or failed with a reason.
Quiet hours (21:00–07:00 in the school's timezone) hold an ordinary send until morning and the response says so (held_for: "quiet_hours", scheduled_for). An OTP or an emergency passes straight through with "options": { "respect_quiet_hours": false } or "priority": "emergency".

Website — chat on the page, or on WhatsApp

Two doors for a school's own site, both safe to put in public HTML because they use a publishable key: it is hard-locked to bot:chat, so it can talk to the assistant and nothing else — never send a message, never read the roll, never see a conversation.

1 · The chat widget

Copy the tag from the school console (Integrations → Website widget), or write it by hand:

<script src="https://whatsapp.techvein.in/widget/onekey-widget.js"
        data-key="tvp_live_…"
        data-title="St. Mary's School, Kanpur"
        data-greeting="Ask me about fees, timetable or holidays."
        defer></script>

The widget calls POST /v1/bot/messages with the visitor's text and a session id; the assistant answers from the school's knowledge documents and, for a verified parent, from the roll. It is rate-limited per IP. When a visitor asks for a person or something out of scope, a ticket is raised into the school's Inbox; an admission enquiry becomes a structured Request the office answers from the console.

Student data (fees, attendance) needs a verified parent. On the web that is a one-time code sent to the parent's WhatsApp — the widget drives it, but the calls are public if you build your own UI:

POST /v1/identity/challenge  { "phone": "+919876543210" }   # OneKey sends the identity_otp template
POST /v1/identity/verify     { "phone": "+919876543210", "code": "482913" }
# → { "verified": true, "contact_id": "ctc_…", "identity_level": "L2" }

2 · The "Chat on WhatsApp" button or QR code

A link that opens WhatsApp with the school's code already typed, so OneKey knows which school the very first message is for — even on a number several schools share:

curl https://whatsapp.techvein.in/v1/deeplink -H "Authorization: Bearer tvk_live_…"
# → { "data": [ { "sender": "+919065513341",
#                 "url": "https://wa.me/919065513341?text=Hi%20%23SMK4821",
#                 "qr": "https://…/create-qr-code/?data=…" } ] }

<a href="https://wa.me/919065513341?text=Hi%20%23SMK4821">Chat with us on WhatsApp</a>

The visitor sends "Hi #SMK4821"; OneKey routes it to St. Mary's, opens a 24-hour window, and the assistant answers or a person replies from the Inbox. Print the QR on the notice board and the prospectus for the same effect.

ERP & CRM — from your backend

A server-side integration holds a secret key and does four things: keeps the roll in sync, sends, replies inside the window, and listens for webhooks. A platform that serves many schools (an ERP) holds one key and names the school on each call with on_behalf_of; each school has granted it that right (a delegation) from the console.

Keep the roll in sync

Push parents as they change — nightly, or on every edit. Records merge by external_ref, then by phone; a field you do not send is left alone; a parent who was erased under DPDP stays erased.

curl -X POST https://whatsapp.techvein.in/v1/contacts/sync \
  -H "Authorization: Bearer tvk_live_…" -H "Content-Type: application/json" \
  -H "X-On-Behalf-Of: SMK4821" \
  -d '{ "records": [
        { "external_ref": "TAT-PAR-4410", "phone": "+919876543210", "name": "Mrs. Sharma",
          "language": "hi",
          "attributes": { "student_name": "Aarav", "class": "5", "section": "A",
                          "fee_due_date": "2026-09-12", "fee_amount": "12,400", "fee_status": "unpaid" } }
      ] }'

on_behalf_of works as a body field, a query parameter, or the X-On-Behalf-Of header. Attributes are what automations and group rules filter on, so the roll's own fields decide who gets a fee reminder.

Send to many

One template, up to 10,000 recipients, each with its own variables. Every recipient becomes its own message with its own status.

curl -X POST https://whatsapp.techvein.in/v1/messages/batch \
  -H "Authorization: Bearer tvk_live_…" -H "Content-Type: application/json" \
  -d '{
    "on_behalf_of": "SMK4821",
    "template": "holiday_notice",
    "recipients": [
      { "to": "+919876543210", "variables": { "reason": "Diwali", "date": "20 Oct 2026" } },
      { "to": "+919876543211", "variables": { "reason": "Diwali", "date": "20 Oct 2026" } }
    ],
    "options": { "priority": "normal" }
  }'

Reply inside the window, send media

When a parent has written in (you will know from the message.received webhook), you have 24 hours in which free-form text and media are allowed — and free:

POST /v1/conversations/cnv_01J…/reply
{ "text": { "body": "Your receipt is attached." } }

POST /v1/messages
{ "to": "+919876543210",
  "media": { "url": "https://erp.example.in/receipts/482913.pdf",
             "caption": "Fee receipt", "filename": "receipt-482913.pdf" } }

The media URL must be reachable from the internet — Meta fetches it. Outside the window a media send is refused with outside_session_window; use a template that carries a media header instead.

Read back

GET /v1/messages?limit=50              # recent sends with status
GET /v1/messages/msg_01J…              # one message
GET /v1/conversations                   # threads, newest first, with window_expires_at
GET /v1/conversations/cnv_01J…         # the thread, both directions
GET /v1/media/med_01J…                 # a file a parent sent (OneKey keeps a copy)
GET /v1/usage                           # this month's counts and cost

CMS — WordPress, Drupal, or any site builder

A content site has no backend of its own to speak of, so the integration is mostly the website one, placed in the theme rather than in one page:

  1. Put the widget tag in the theme's footer (WordPress: Appearance → Theme File Editor → footer.php, or any "custom scripts" box a plugin gives you). It then appears on every page. The key is publishable, so it is fine in public HTML.
  2. Add the "Chat on WhatsApp" link to the header, the contact page and the admissions page — the wa.me URL from GET /v1/deeplink. It works on the phone as a tap and on the desktop as a QR.
  3. Optional — a notice you publish becomes a WhatsApp broadcast. A small server hook (a WordPress publish_post action, a Drupal rule, a Zapier step) calls POST /v1/messages/batch with an approved template such as holiday_notice and the recipients from GET /v1/contacts. This part needs a secret key, so it runs on a server, never in the theme.
Never put a tvk_ key in a page, a theme, or a client-side script. If one leaks, revoke it from the console (Integrations → Keys) and issue another; nothing using the old one will work from that moment.

Webhooks — how you find out what happened

Register once with POST /v1/webhooks; an empty events list subscribes to everything. Each delivery is a POST to your URL with a JSON body and three headers:

X-Techvein-Event-Type: message.status
X-Techvein-Event-Id:   evt_01J…
X-Techvein-Signature:  t=1757060000,v1=8f3c…    # HMAC-SHA256 of "<t>.<raw body>" with your secret
// Node — verify before you trust the body
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header) {
  const p = Object.fromEntries(header.split(",").map(s => s.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const mac = createHmac("sha256", secret)
    .update(`${p.t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(mac), Buffer.from(p.v1));
}
# Python
import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    p = dict(kv.split("=") for kv in header.split(","))
    if abs(time.time() - int(p["t"])) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{p['t']}.".encode() + raw_body,
                   hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, p["v1"])

Answer 2xx quickly and do the work afterwards; a non-2xx answer is retried on a published back-off schedule, and the same event_id can therefore arrive twice — make handling idempotent.

EventWhenBody (after the common event, tenant_id, tenant_code, event_id, occurred_at)
message.statusEvery change to a message you sentmessage: { id, status, to, failure_code, failure_reason }, plus the context you sent with it
message.receivedA parent wrote inconversation: { id, window_expires_at }, contact: { id, e164, name, identity_level, external_ref }, message: { id, type, text, media, sender }, routing: { resolved_via, handled_by }
conversation.opened · conversation.escalatedA new thread; a thread handed to a personThe conversation and contact
request.created · request.decidedA parent filed a leave / transport / admission request; the office answeredThe request, its fields in the order asked, the decision
template.approved · template.rejected · template.status_changedThe template you proposed movedThe template, its state, a note on rejection
contact.opted_out · ticket.created · invoice.issuedA STOP; a ticket for a person; the monthly statementThe contact / ticket / invoice

Templates — getting one approved

WhatsApp allows a business to start a conversation only with a template Meta has approved, and approval is per number and per language. OneKey tracks that pairing as a binding; nothing sends until a binding is approved. Propose a template from your system, or from the school console's composer:

curl -X POST https://whatsapp.techvein.in/v1/templates \
  -H "Authorization: Bearer tvk_live_…" -H "Content-Type: application/json" \
  -d '{ "name": "exam_hall_ticket", "category": "utility",
        "body": "Dear {{parent_name}}, the hall ticket for {{student_name}} is ready for collection from {{date}}.",
        "variables": ["parent_name", "student_name", "date"] }'
# → 201 { …, "state": "in_review", "status": "awaiting_binding",
#          "next": "Techvein reviews the template, submits it to Meta for this tenant's numbers, and marks it approved when Meta clears it." }
StateMeaningWho moves it on
in_reviewTechvein has itA Techvein operator reads it and submits it to Meta for approval
submittedIt is at Meta, with a pending binding per number and languageMeta — minutes to a few days
approvedIt can be sentYou receive template.approved
rejectedWith the reason in a noteYou receive template.rejected; fix and propose again

Rules that save a round trip: names are lowercase with underscores; a body may not start or end with a variable; authentication templates (OTPs) have a fixed shape Meta dictates; marketing needs an explicit opt-in from every recipient, utility and authentication are covered by enrolment. An approved template cannot be edited — propose a new one.

Errors — every refusal names itself

Every error is JSON with a stable code, a sentence for a person, and where it helps, a remedy and details:

{ "error": "outside_session_window",
  "message": "The 24-hour session with +919876543210 closed at 2026-09-04T11:02:00Z.",
  "remedy": "Send an approved template to reopen the conversation.",
  "details": { "suggested_templates": ["fee_due_reminder", "holiday_notice"] } }
HTTPCodeIt means
400invalid_request · invalid_phoneThe body is wrong; the message names the field
401unauthorizedMissing or unknown key
403insufficient_scope · no_delegation · ip_not_allowedThe key lacks the scope; the school has not delegated to your platform; the key is pinned to other IPs
409no_sender_availableThe tenant has no active WhatsApp number attached — ask Techvein
409template_not_approvedNo approved binding for that template on this number and language
409outside_session_windowText or media outside the 24-hour window — send a template
409contact_opted_outThe parent sent STOP for this category
409idempotency_conflict · idempotency_in_flightSame Idempotency-Key, different body; or the first request is still being processed
429quota_exceeded · rate_limitedDaily quota, or too many requests; retry_after_seconds says how long
502provider_errorWhatsApp refused or timed out; the message stays failed with the reason

Scopes

ScopeLets a key…
messages:sendSend templates and replies to parents
messages:readRead delivery status and history
conversations:read · conversations:writeRead threads and the inbox · reply, assign, hand back to the assistant
templates:read · templates:writeList approved templates · draft and propose templates
contacts:read · contacts:writeRead the roll and consent · sync the roll, record consent
numbers:readSee which number you send from; get deep links
webhooks:manageRegister and remove webhooks
bot:chat · bot:configureTalk to the assistant (what the widget uses) · change what it knows and how it behaves
adminEverything, bypassing the rest of this list — reserved for Techvein's own tooling

All endpoints

Read live from this server's OpenAPI document, so it is never out of date with the code that is running.

Loading /openapi.json…