Upspace Connect API
A JSON REST API for sending WhatsApp messages, managing contacts and templates, and triggering automations from your own systems. Available on plans that include API access. Base URL: https://upspaceconnect.dev/api/v1
Quick start
- Connect WhatsApp in Integrations.
- Create an API key under Developers → API keys with the scopes you need. The full key is shown once; we store only a hash.
- Send your first message:
curl -X POST "https://upspaceconnect.dev/api/v1/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-0001" \
-d '{"to":"+23052512345","text":"Hello from our shop 👋"}'Free-form text only reaches a customer within 24 hours of their last message. Outside that window, send an approved template.
Authentication
Send your key as a bearer token: Authorization: Bearer uc_live_xxxxxxxxxxxxxxxxxxxxxxxx. Keys start with uc_live_ (or uc_test_ in development), belong to one workspace, can be given an expiry date and can be revoked at any time. Every request also checks that the workspace has an active, paid subscription whose plan includes the API.
Call the API from your server only. Requests made from a web browser (anything that sends an Origin header) are refused with 403 BROWSER_REQUESTS_NOT_ALLOWED, because a key in website or app code can be copied by any visitor. From a website, send the request from your backend (PHP/WordPress, Node.js, Python…). Ready-to-paste examples are under Developers → Connect your website.
Check your key
GET/me· scope any
Confirms the key works and returns the workspace, the key's permissions (and any it's missing), and whether the plan can send messages. It's the safest first call from a new integration.
curl -X GET "https://upspaceconnect.dev/api/v1/me" \
-H "Authorization: Bearer $API_KEY"| Scope | Allows |
|---|---|
messages:send | Send text, media and template messages |
messages:read | Read a message and its delivery status |
contacts:read | List and read contacts |
contacts:write | Create and update contacts, tags and consent |
templates:read | List message templates and their approval status |
automations:trigger | Start automations with your own data |
Messages
POST/messages· scope messages:send
Provide exactly one of text, template or media. Phone numbers are E.164 (for example +23052512345); numbers without a country code are read in your workspace's country. Returns 202 with the queued message; delivery updates arrive as webhooks.
Send a template
curl -X POST "https://upspaceconnect.dev/api/v1/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-ready" \
-d '{"to":"+23052512345","template":{"name":"order_ready","language":"en","variables":["Aisha","#1042"]}}'Send media
media.type is image, document, video or audio; media.url must be a public HTTPS URL.
curl -X POST "https://upspaceconnect.dev/api/v1/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"+23052512345","media":{"type":"document","url":"https://example.com/invoice-1042.pdf","filename":"invoice-1042.pdf","caption":"Your invoice"}}'GET/messages/{id}· scope messages:read
Status is one of queued, processing, sent, delivered, read or failed. Failed messages include error.code and a plain-language error.message.
Templates
GET/templates?status=APPROVED· scope templates:read
Lists templates synced from Meta with their category, status, body and number of variables. Create and edit templates in the app; they are submitted to Meta for review automatically.
curl -X GET "https://upspaceconnect.dev/api/v1/templates?status=APPROVED" \
-H "Authorization: Bearer $API_KEY"Contacts
GET/contacts?q=&tag_id=&limit=50&cursor=· scope contacts:read
POST/contacts· scope contacts:write
GET/contacts/{id}· scope contacts:read
PATCH/contacts/{id}· scope contacts:write
Record marketing consent with marketing_consent and consent_source. Contacts who opt out (for example by sending STOP) are never sent marketing templates, even through the API.
curl -X POST "https://upspaceconnect.dev/api/v1/contacts" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: contact-L-2231" \
-d '{"phone":"+23052512345","first_name":"Aisha","tags":["VIP"],"marketing_consent":true,"consent_source":"Checkout form","custom_fields":{"loyalty_id":"L-2231"}}'Automations
POST/automations/{id}/trigger· scope automations:trigger
Starts a published automation whose trigger is “API”. Everything in data is available in messages as {{api.field}} variables, with fallbacks like {{api.name|there}}. Returns 202 with the execution; send the same Idempotency-Key to avoid starting it twice.
curl -X POST "https://upspaceconnect.dev/api/v1/automations/aut_xxxxxxxx/trigger" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{"contact":{"phone":"+23052512345"},"data":{"order":"1042","pickup":"Friday"}}'Generic webhook URL
Automations with a “Webhook” trigger get a private URL (https://upspaceconnect.dev/hooks/hk_…) that accepts any JSON object — useful for form builders and no-code tools that can't set headers. The contact's phone is read from the path you configure (default contact.phone) and the payload is available as {{webhook.*}}. Treat the URL as a secret; regenerate it from the automation if it leaks.
Webhooks
Add HTTPS endpoints under Developers → Webhooks and choose events. Each delivery is a POST with a JSON body { id, type, created_at, workspace_id, data } and these headers:
Upspace-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">Upspace-Event: the event typeUpspace-Delivery: a unique delivery id (use it to ignore repeats)
Events: message.received, message.sent, message.delivered, message.read, message.failed, contact.created, contact.updated, automation.started, automation.completed, automation.failed.
Respond with any 2xx within 10 seconds. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours; every attempt is visible in the delivery log, where you can also resend.
Verify the signature
import crypto from "node:crypto";
// Use the raw request body (a string), not a re-serialized object.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// verify(rawBody, req.headers["upspace-signature"], process.env.WEBHOOK_SECRET)Idempotency
POST endpoints accept an Idempotency-Key header: 1–255 characters of letters, numbers and . _ : -. Repeating a request with the same key and body within 24 hours returns the original response (with Idempotent-Replayed: true) instead of acting twice. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSED; a repeat while the first request is still running returns 409 IDEMPOTENCY_IN_PROGRESS.
Errors
Errors share one format, and every response carries an X-Request-Id header to quote to support:
{
"error": {
"code": "TEMPLATE_REQUIRED",
"message": "More than 24 hours have passed since this customer last wrote. Send an approved template.",
"request_id": "req_…"
}
}| Status | Common codes |
|---|---|
| 400 | INVALID_JSON, INVALID_IDEMPOTENCY_KEY |
| 422 | INVALID_REQUEST, INVALID_PHONE_NUMBER, TEMPLATE_REQUIRED, CONTACT_OPTED_OUT, IDEMPOTENCY_KEY_REUSED |
| 401 | UNAUTHENTICATED, INVALID_API_KEY |
| 402 | SUBSCRIPTION_INACTIVE, FEATURE_NOT_INCLUDED, PLAN_LIMIT_REACHED |
| 403 | INSUFFICIENT_SCOPE, WORKSPACE_SUSPENDED, BROWSER_REQUESTS_NOT_ALLOWED |
| 404 / 409 | NOT_FOUND, WHATSAPP_NOT_CONNECTED, IDEMPOTENCY_IN_PROGRESS |
| 413 / 429 | PAYLOAD_TOO_LARGE, RATE_LIMITED, TOO_MANY_FAILED_ATTEMPTS (both with Retry-After) |
Rate limits
Each key may make 120 requests per minute (600 on plans with priority processing), and each IP address 300 per minute across all keys. After 20 requests with an invalid key, an IP address is blocked for up to 10 minutes (429 TOO_MANY_FAILED_ATTEMPTS). Responses include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds), plus X-Quota-Used and X-Quota-Limit for the monthly allowance; when you exceed a limit you get 429 with Retry-After in seconds. Monthly API request allowances depend on your plan and are shown in Billing. Request bodies can be up to 256 KB. WhatsApp itself also limits how many new customers a number can message per day; failures from Meta are reported on the message.