Build on STS Connect.
Create contacts, send messages and react to every event from your own systems: a REST API with account keys, and signed webhooks for everything that happens in the inbox.
Overview
Every account has the same REST API. Create and update contacts, open conversations, send messages and WhatsApp templates, and manage labels, all from your own systems.
- Base URL
https://app.stsconnect.chat- All endpoints live under
/api/v1/accounts/{account_id}- Send your key in the header
api_access_token
Your first call: create a contact
curl -X POST https://app.stsconnect.chat/api/v1/accounts/ACCOUNT_ID/contacts \
-H "api_access_token: sts_live_xxx" \
-H "Content-Type: application/json" \
-d '{"name": "Sara Haddad", "phone_number": "+971501234567"}'
Keys and scopes
An administrator creates keys in the inbox under Settings, API & webhooks, then Create key. Every key starts with sts_live_.
-
Two scopes: Read for reading data, Read and write for sending messages and changing records.
-
A key expires after 30, 90 or 365 days, or never.
-
Up to 25 active keys per account.
-
The key is shown once. We store only a hash of it, so a lost key cannot be shown again: revoke it with the Revoke button and create a new one.
-
A key acts for the account, not for a person.
Errors
-
A write with a Read key fails with 403 and the error code
api_key_read_only. -
An endpoint keys cannot use, for example managing webhooks or keys, fails with 403 and the error code
api_key_endpoint_not_allowed.
Limits
-
120 requests a minute per key, never more than the account limit.
-
300 requests a minute per account, 30 during the free trial. Fixed 60-second windows.
-
Every response carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset. -
Over the limit the answer is HTTP 429 with a
Retry-Afterheader and the body coderate_limited. -
Contact search: 100 requests a minute per account.
Webhooks
Add a webhook in the inbox under Settings, API & webhooks. Events are sent to your URL as they happen.
The body is the JSON of the resource with a top-level event key. Four events also include changed_attributes: conversation_updated, contact_updated, inbox_updated and conversation_status_changed.
The Send test button delivers a test event: { "event": "webhook_test", ... }
Events you can subscribe to
message_createdmessage_updatedconversation_createdconversation_updatedconversation_status_changedcontact_createdcontact_updatedinbox_createdinbox_updatedwebwidget_triggeredconversation_typing_onconversation_typing_off
Verify the signature
Every delivery carries four headers:
X-STS-Connect-Timestamp- Unix seconds.
X-STS-Connect-Signaturesha256=plus the hex HMAC-SHA256 of{timestamp}.{raw body}with the endpoint secret.X-STS-Connect-Delivery- A unique id per event. Use it to drop duplicates.
X-STS-Connect-Attempt- The attempt number.
-
The endpoint secret is under Show secret in the Endpoint secrets dialog of the webhook.
-
Compare in constant time, and reject timestamps older than 5 minutes. Our own n8n node does exactly this.
-
Always verify against the raw body, before JSON parsing.
Verify it in your language
PHP
<?php
$secret = 'ENDPOINT_SECRET';
$timestamp = $_SERVER['HTTP_X_STS_CONNECT_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_STS_CONNECT_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit('Stale timestamp');
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Bad signature');
}
// The delivery is genuine. Handle the event here.
Node.js
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = 'ENDPOINT_SECRET';
app.post('/webhooks/stsconnect', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-STS-Connect-Timestamp') ?? '';
const signature = req.get('X-STS-Connect-Signature') ?? '';
if (!/^\d+$/.test(ts)) return res.sendStatus(401);
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.sendStatus(401);
}
const expected = Buffer.from(
'sha256=' + crypto.createHmac('sha256', SECRET).update(ts + '.' + req.body).digest('hex')
);
const received = Buffer.from(signature);
if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
return res.sendStatus(401);
}
// The delivery is genuine. Handle the event here.
res.sendStatus(200);
});
Python
import hmac
import time
from hashlib import sha256
from flask import Flask, request
app = Flask(__name__)
SECRET = b'ENDPOINT_SECRET'
@app.post('/webhooks/stsconnect')
def stsconnect_webhook():
timestamp = request.headers.get('X-STS-Connect-Timestamp', '')
signature = request.headers.get('X-STS-Connect-Signature', '')
body = request.get_data()
ts = int(timestamp) if timestamp and timestamp.isdigit() else 0
if abs(time.time() - ts) > 300:
return ('Stale timestamp', 401)
expected = 'sha256=' + hmac.new(
SECRET, timestamp.encode() + b'.' + body, sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return ('Bad signature', 401)
# The delivery is genuine. Handle the event here.
return ('', 200)
Retries
-
Each attempt waits up to 5 seconds to connect and 5 seconds for a response, and at most 15 seconds in total; any 2xx answer counts as success.
-
Deliveries that fail temporarily are retried 5 times (6 attempts), after about 1, 4, 10, 20 and 25 minutes; every attempt is signed again with a new timestamp. Addresses that are invalid or not allowed are not retried.
-
Typing events are sent once and never retried.
-
If an endpoint keeps failing for 24 hours it is paused, and the administrators get one email. Press Resume to start it again.
-
While an endpoint is failing, up to 1,000 events are kept for up to 24 hours.
Main endpoints
The calls you will use most. Paths are relative to the base URL and prefix above.
-
POST
/contactsCreate a contact. -
GET
/contacts/search?q=Find contacts by name, email or phone. -
GET
/conversationsList conversations. -
POST
/conversationsOpen a conversation. -
POST
/conversations/{id}/messagesSend a message. A WhatsApp template goes intemplate_paramswithname, category, language, processed_params. -
POST
/conversations/{id}/toggle_statusResolve or reopen a conversation. -
GET
POST
/conversations/{id}/labelsRead or set the labels, with the body{"labels": [...]}.
n8n
The community package n8n-nodes-stsconnect adds two nodes: STS Connect for actions, and STS Connect Trigger for events. The trigger verifies signatures and drops stale deliveries for you.
Need another system? Tell us on WhatsApp
Start answering every channel today.
30 days free, no credit card. Connect your first channel in minutes.