10xSTATION

10xstation Messaging API: client integration

How a client system (first: Minhaj Kids) receives Messenger and Instagram messages from 10xstation and sends replies back. 10xstation holds the Meta connection; your system never talks to Meta directly.

Credentials

Everything is in your 10xstation account at https://app.10xstation.com/app/developers:

Value Example Where to keep it
API key oyk_7Ygqqp8XjnHMzny2 env, e.g. TENX_API_KEY
API secret oys_… (43+ chars) env, e.g. TENX_API_SECRET. It's secret and shown once; a workspace owner can Rotate API secret to get a new one.
Workspace ID 1 arrives as clientId on every event

Set your Forwarding webhook URL on the same page, e.g. https://minhaj.kids/api/integrations/10xstation/webhook, then use Send signed test event to check your endpoint.

If the secret leaks, rotate it. The old secret stops working immediately in both directions.

Forwarding and sending require an active plan (trial, paid, or within 7 days of a failed payment). While billing is inactive, inbound messages are still stored and are forwarded once billing is active again.

1. Receiving events (10xstation → you)

For every inbound customer message on a connected Facebook Page (Messenger), its linked Instagram professional account, or a connected WhatsApp number, 10xstation sends:

POST <your forwarding URL>
Content-Type: application/json
User-Agent: 10xstation-Webhooks/1.0
X-10xstation-Event-Id: evt_wz3lEvqO5g0vwJ0P
X-10xstation-Timestamp: 1789648798            (unix seconds)
X-10xstation-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>

Payload

{
  "id": "evt_wz3lEvqO5g0vwJ0P",
  "type": "message.received",
  "clientId": "1",
  "channel": "messenger",
  "direction": "inbound",
  "pageId": "1111111111",
  "igAccountId": null,
  "senderId": "25123456789012345",
  "senderName": "Aisha Rahman",
  "messageId": "msg_6VUXwf1xiDy8xcs8",
  "mid": "m_AbC…",
  "text": "Assalamu alaikum, is there space in the Saturday class?",
  "attachments": [],
  "postback": null,
  "timestamp": "2026-09-17T12:39:57.857Z",
  "raw": { "sender": { "id": "…" }, "recipient": { "id": "…" }, "timestamp": 1789648797857, "message": { "mid": "…", "text": "…" } }
}
Field Notes
channel "messenger", "instagram" or "whatsapp"
senderId Page-scoped ID (PSID) for Messenger, Instagram-scoped ID (IGSID) for Instagram. Stable per person per Page / IG account. Use it as the contact key and as recipientId when replying.
senderName Name, or @username on Instagram when no name is available. May be null (privacy settings, lookup failure).
igAccountId Set only for Instagram events.
text Message text, or the button title for a postback. May be null for attachment-only messages.
attachments[] { "type": "image" | "video" | "audio" | "file" | "share" | "story_mention" | …, "url": string | null }. Meta's URLs expire, so download anything you want to keep.
postback { "title", "payload" } when the customer tapped a button, otherwise null.
raw Meta's original messaging item, for fields not normalised above.
type message.received, plus on WhatsApp message.echo, message.history and message.status (below). A test type is sent when staff click "Send signed test event". Ignore unknown types.
kind Message type: text, image, video, audio, document, sticker, location, contacts, interactive, button, reaction.

Replies sent from Meta Business Suite and messages you send through the API are not forwarded back to you.

WhatsApp events

WhatsApp events add these fields:

{
  "type": "message.received",
  "channel": "whatsapp",
  "direction": "inbound",
  "pageId": null,
  "whatsapp": { "phoneNumberId": "109876543210987", "wabaId": "102030405060708", "displayPhoneNumber": "447700900000" },
  "senderId": "BSUID_2f1c…",
  "senderPhone": "447911123456",
  "senderUsername": null,
  "senderName": "Aisha Rahman",
  "kind": "image",
  "text": "Her certificate",
  "attachments": [
    { "type": "image", "url": null, "mediaId": "904050607", "mediaUrl": "https://app.10xstation.com/api/v1/media/904050607", "mimeType": "image/jpeg", "filename": null }
  ]
}
Field Notes
senderId The business-scoped user ID (BSUID), WhatsApp's stable identifier for that person and your business. Use it as the contact key and as recipientId. Falls back to the phone number when WhatsApp doesn't send a BSUID.
senderPhone Digits only, no +. WhatsApp may omit it for people who use a username and haven't messaged recently, so don't rely on it alone.
senderUsername WhatsApp username, when the person uses one.
whatsapp Which of your numbers the message arrived on.
attachments[].mediaUrl Download it from 10xstation with a signed GET (below). WhatsApp's own links need our access token and expire after 5 minutes, so url is always null.

Extra WhatsApp event types:

type When Notes
message.echo The business replied from the WhatsApp Business app on the phone (coexistence numbers) direction: "outbound". Mirror it so your inbox matches the phone.
message.history Chat history imported when a coexistence number is connected direction is inbound or outbound. One event per message, limited to the last 90 days. Ignore these if you don't want history.
message.status WhatsApp reported a delivery receipt for a message you sent status: { "value": "sent" | "delivered" | "read" | "failed", "errors": [...] }, messageId is the id returned when you sent it.

Downloading WhatsApp media

GET https://app.10xstation.com/api/v1/media/<mediaId>
X-10xstation-Key: <API key>
X-10xstation-Timestamp: <unix seconds>
X-10xstation-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.")>

The signature covers an empty body, so the signed string ends with the dot. The response is the file itself with its Content-Type. 10xstation doesn't store media: it is fetched from Meta when you ask, and WhatsApp deletes it after about 30 days, so download anything you need to keep.

Verifying (required)

Verify against the raw body before parsing JSON, reject anything older than 5 minutes, and use a constant-time comparison.

// Next.js route handler: app/api/integrations/10xstation/webhook/route.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export async function POST(request) {
  const raw = await request.text();
  const timestamp = request.headers.get('x-10xstation-timestamp');
  const signature = request.headers.get('x-10xstation-signature') ?? '';

  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return new Response('stale', { status: 401 });
  }
  const expected = 'sha256=' + createHmac('sha256', process.env.TENX_API_SECRET)
    .update(`${timestamp}.${raw}`)
    .digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response('bad signature', { status: 401 });
  }

  const event = JSON.parse(raw);
  if (event.type !== 'message.received') return new Response('ignored', { status: 200 });

  // Idempotency: 10xstation may deliver the same event more than once.
  // Store event.id (or event.mid) with a unique constraint and skip duplicates.
  await saveInboundMessage(event); // map channel → conversation channel, senderId → contact key
  return new Response('ok', { status: 200 });
}

Delivery rules

  • Respond 2xx within 10 seconds. Do slow work after acknowledging.
  • Any other status, a redirect, a timeout or a network error counts as a failure. Retries happen after 30s, 2m, 10m, 30m, 1h, 3h, 6h and 12h, then the event is marked failed and 10xstation staff can replay it.
  • Delivery is at least once, so deduplicate on id. Order is not guaranteed; sort by timestamp.

2. Sending messages (you → 10xstation → Meta)

POST https://app.10xstation.com/api/v1/messages
Content-Type: application/json
X-10xstation-Key: <API key>
X-10xstation-Timestamp: <unix seconds>
X-10xstation-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
Idempotency-Key: <optional, up to 200 chars, e.g. your outbound message UUID>

Body:

{ "channel": "messenger", "recipientId": "25123456789012345", "text": "Wa alaykum assalam! Yes, two places left." }
Field Required Notes
channel yes "messenger", "instagram" or "whatsapp"
recipientId yes The senderId from an inbound event. On WhatsApp you may also pass a phone number in international format, but only with a template.
text one of Messenger ≤ 2000 characters; Instagram ≤ 1000 bytes UTF-8; WhatsApp ≤ 4096
attachment one of { "type": "image" | "video" | "audio" | "file", "url": "https://…" } (publicly reachable HTTPS URL). On WhatsApp the types are image, video, audio, document, sticker, plus optional caption and filename.
template one of WhatsApp only. An approved template: { "name": "order_ready", "language": { "code": "en_US" }, "components": [...] }, in Meta's own shape.
pageId only if your workspace has several Pages Which connected Page to send from
phoneNumberId only if your workspace has several WhatsApp numbers Which number to send from
tag no "HUMAN_AGENT", Messenger and Instagram only. Only accepted once 10xstation has Meta's Human Agent approval (see window rules).

Send exactly one of text, attachment or template.

WhatsApp template example — this both re-opens a conversation after 24 hours and starts a new one:

{
  "channel": "whatsapp",
  "recipientId": "447911123456",
  "template": {
    "name": "class_reminder",
    "language": { "code": "en_US" },
    "components": [
      { "type": "body", "parameters": [{ "type": "text", "text": "Aisha" }, { "type": "text", "text": "Saturday 10am" }] }
    ]
  }
}

Templates are created and approved in the 10xstation account under Templates (or in Meta's WhatsApp Manager). components is optional when the template has no variables.

import { createHmac, randomUUID } from 'node:crypto';

export async function sendVia10xstation({ channel, recipientId, text }) {
  const body = JSON.stringify({ channel, recipientId, text });
  const timestamp = Math.floor(Date.now() / 1000);
  const signature = 'sha256=' + createHmac('sha256', process.env.TENX_API_SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  const res = await fetch('https://app.10xstation.com/api/v1/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-10xstation-Key': process.env.TENX_API_KEY,
      'X-10xstation-Timestamp': String(timestamp),
      'X-10xstation-Signature': signature,
      'Idempotency-Key': randomUUID(),
    },
    body, // must be the exact string that was signed
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error.message), { code: json.error.code, status: res.status });
  return json;
}

Success

201 Created (or 200 OK when an Idempotency-Key replays an earlier successful send):

{ "id": "msg_Q1w2E3r4T5y6U7i8", "mid": "m_…", "conversationId": 42, "channel": "messenger", "recipientId": "25123456789012345", "sentAt": "2026-09-17T12:41:02.114Z", "replayed": false }

Errors

{ "error": { "code": "…", "message": "…", "metaCode": 190 } } (metaCode only when Meta returned an error).

HTTP code Meaning / what to do
400 invalid_request Fix the body.
400 page_id_required Workspace has several Pages or WhatsApp numbers; pass pageId or phoneNumberId.
401 missing_api_key, invalid_api_key, signature_missing, signature_stale, signature_mismatch Check key, secret, clock (±5 min) and that you sign the exact bytes you send.
404 connection_not_found No active Page (or no linked Instagram account) for this channel.
404 conversation_not_found This person never messaged the business, so Meta forbids messaging them. On WhatsApp, send an approved template to a phone number instead.
409 reconnect_needed Meta rejected the token (password change, removed access, expired). A Page admin must reconnect. Don't retry until then.
402 subscription_inactive No active plan (trial ended, cancelled, or payment failed more than 7 days ago). An owner must update billing.
413 payload_too_large Body over 64 KB.
422 outside_messaging_window More than 24 h since the customer's last message. Wait for them to write again.
422 human_agent_not_approved HUMAN_AGENT was requested but isn't enabled.
400 template_invalid WhatsApp rejected the template name, language or parameters. Check it is approved and that every variable has a value.
429 rate_limited More than 60 requests/minute for your workspace; honour Retry-After.
429 meta_rate_limited Meta throttled the Page. Retry later with backoff.
502 meta_error Meta refused or failed. Safe to retry with the same Idempotency-Key.
503 not_configured 10xstation side misconfigured. Contact 10xstation.

3. Messaging window rules (Meta policy)

Messenger and Instagram

  • Standard window: you may reply for 24 hours after the customer's most recent message. Each new customer message restarts it.
  • After 24 hours: only the HUMAN_AGENT tag allows a reply, up to 7 days after the customer's last message. It is for a human replying manually to a request that couldn't be answered in 24 hours, never for automation or promotions, and requires Meta approval for the 10xstation app. Until approved, the API returns human_agent_not_approved.
  • You can never start a conversation. The customer must message first (conversation_not_found).
  • 10xstation enforces these rules before calling Meta, so a refused send costs you nothing.

WhatsApp

  • Customer service window: free-form text and media are allowed for 24 hours after the customer's last message. Each new message restarts it.
  • After 24 hours, or to start a conversation: only an approved template. Meta must approve each template (usually within minutes), and you must have the person's opt-in to receive WhatsApp messages from the business.
  • Meta charges for WhatsApp messages directly, under its own per-message pricing, to the card the business adds in WhatsApp Manager. It is separate from the 10xstation subscription.
  • Coexistence numbers (also used in the WhatsApp Business app) are limited by WhatsApp to 20 messages per second, and don't support group chats, broadcast lists, or disappearing and view-once messages.

4. Connecting, reconnecting, disconnecting

  • Connect: a workspace owner clicks Connect Facebook & Instagram under Connections in the 10xstation account (or opens a single-use link from 10xstation, valid 7 days). Sign in with a Facebook account that administers the Page, share your business and Page, and choose the Page. The linked Instagram professional account is picked up automatically. Your plan sets how many Pages you can connect. For Instagram, also turn on Allow access to messages (Instagram app → Settings → Messages and story replies → Message controls → Connected tools).
  • Reconnect: when sends return reconnect_needed, click Connect again and choose the same Page. History and IDs are kept, and it doesn't use an extra Page from your plan.
  • Disconnect: click Disconnect under Connections, or remove the 10xstation app in Facebook Settings → Business Integrations (Meta Business Suite → Settings → Integrations). 10xstation unsubscribes from the Page and deletes its tokens.
  • WhatsApp: under Connections choose Use my WhatsApp Business app number (coexistence — the app on the phone keeps working, and contacts and up to 90 days of chats are imported) or Connect a new number. A WhatsApp number counts as one channel on your plan, like a Page. Disconnecting deletes 10xstation' token, the synced contacts and the template mirror; the number itself and its WhatsApp history stay with the business.

5. Data held by 10xstation

Messages are kept by 10xstation for 90 days, then deleted. Media files are never stored — they are fetched from Meta on request. Your system is the long-term record. See https://app.10xstation.com/privacy.

Mapping suggestions for Minhaj

10xstation Minhaj inbox
channel (messenger / instagram / whatsapp) conversation channel (alongside whatsapp_web, sms)
senderId contact external ID (the wa_id equivalent), unique per (channel, pageId / igAccountId / whatsapp.phoneNumberId)
senderPhone contact phone number on WhatsApp — match existing contacts on it, but key the conversation on senderId
senderName contact display name if empty
id unique inbound event key
messageId / mid message external ID
/api/v1/messages response id outbound message external ID
message.echo message sent by staff from the WhatsApp Business app — store as outbound, don't re-send
message.history imported past message — store with its original timestamp, or ignore
message.status update the delivery state of the outbound message with that messageId

Minhaj's WhatsApp moves to 10xstation through coexistence: the existing number keeps working in the WhatsApp Business app, and 10xstation forwards its messages with channel: "whatsapp" alongside Messenger and Instagram.