We're a startup — currently pending official Meta approval to launch.

API Reference · v1.0.0

aulm API

WhatsApp messaging over HTTP — QR/session based, no Meta approval required.

Notice: AULM is not the official WhatsApp Business Platform

AULM is built on an unofficial WhatsApp Web session layer. We are not affiliated with Meta or WhatsApp and we do not provide the WhatsApp Business API (WABA). Use the service only for legitimate, opt-in communication. Spam, bulk marketing, or unusual sending behaviour may result in your number being restricted by WhatsApp — we do not guarantee delivery, ban-immunity, or Meta approval. For official business use cases, follow Meta's WhatsApp Business API requirements.

Introduction

aulm is a WhatsApp messaging API. You connect a phone by scanning a QR code in the dashboard, then send messages and receive inbound events over HTTP. There is no Meta business verification, no template approval, no WABA.

Why aulm

  • Refund on failure. Transient upstream failures refund your quota slot automatically.
  • Traceable. Every response carries x-request-id. Log it; quote it.
  • Honest rate limits. Send endpoints stamp retry-after on every 429.
  • Signed webhooks. Published retry ladder — delivery is our problem, not yours.

Quick Install

Paste this into your app builder to install AULM. Then add your keys from the dashboard.

Prompt for Lovable
Build this in Lovable Cloud: put the send helper and webhook receiver in Supabase edge functions, and store AULM_API_KEY / AULM_SESSION_ID / AULM_WEBHOOK_SECRET as Lovable secrets (never in the frontend).

Integrate the AULM WhatsApp API (https://api.aulm.io) into this app so I can send (and optionally receive) WhatsApp messages.

SECRETS — store these server-side only, never in client/browser code. I will paste the values after you set up the placeholders:
- AULM_API_KEY        (format: alm_sk_live_xxxxxxxx… ; use alm_sk_test_… while testing)
- AULM_SESSION_ID     (the WhatsApp number's session id from my aulm dashboard)
- AULM_WEBHOOK_SECRET (only needed if I receive inbound messages)

1) SEND A TEXT MESSAGE (server-side helper):
   POST https://api.aulm.io/v1/messages/send-text
   Headers: Authorization: Bearer ${AULM_API_KEY}   |   Content-Type: application/json
   Body: { "session_id": "${AULM_SESSION_ID}", "to": "+<E164 number>", "body": "<text>" }
   On success (201) you get { data: { message_id, status }, request_id }.
   On error, read error.code and handle these especially:
     429 rate_too_fast        -> sending too fast; wait and retry (honor the Retry-After header)
     429 warmup_daily_limit   -> new number's daily cap reached; try later
     422 recipient_not_on_whatsapp -> number isn't on WhatsApp; don't retry
     403 recipient_opted_out  -> recipient replied STOP; never message them again
     409 session_not_ready    -> the number isn't connected yet
   Add a simple retry with backoff for 429/502/503/504 only.

2) SEND AN IMAGE (optional helper):
   POST https://api.aulm.io/v1/messages/send-image
   Body: { "session_id": "${AULM_SESSION_ID}", "to": "+<E164>", "image": "<https URL or data:image/...;base64,...>", "caption": "<optional>" }

3) RECEIVE INBOUND / STATUS (optional — only if I want to handle replies/delivery):
   Create ONE public POST endpoint (e.g. /webhooks/aulm). For each request:
   - Read the raw request body BYTES (do not re-serialize JSON before verifying).
   - Read header x-aulm-signature = "t=<unix>,v1=<hex>".
   - Compute HMAC_SHA256(AULM_WEBHOOK_SECRET, `${t}.${rawBody}`) and compare to v1 using a constant-time compare. Reject (401) on mismatch, or if t is older than ~5 minutes.
   - Then branch on header x-aulm-event:
       inbound_message  -> body.message has { from, body, type, media_url, ... }
       message_status   -> body has { message_id, status }  (sent|delivered|read|played|failed)
   - Respond 200 quickly (do heavy work async). Use header x-aulm-delivery-id for idempotency (it can repeat on retries).
   Then I'll paste this endpoint's URL into my aulm dashboard.

RULES:
- Never expose AULM_API_KEY to the browser; all aulm calls go through my backend.
- Use alm_sk_test_ keys while building (responses include "simulated": true) and switch to alm_sk_live_ for production.
- Phone numbers must be E.164 (e.g. +14155551234).

Set up with AI

Developer AI assistants (Claude, ChatGPT, Cursor) work best when given a clear specification. Copy the prompt below into your assistant to get correct, production-ready aulm code for your project immediately.

AI System Prompt

You are helping me integrate the aulm WhatsApp API into my project...

ABOUT

aulm sends WhatsApp messages via an HTTP API. It is unofficial...

Recommended for Cursor, Claude 3.5 Sonnet, & GPT-4o

Authentication

Every request takes a Bearer token in the Authorization header. Keys are generated in the dashboard and match the regex ^alm_sk_(live|test)_[0-9a-f]{64}$.

  • alm_sk_live_… — real sends. Billable. Counts against your plan quota.
  • alm_sk_test_… — every call is simulated. The messaging service is never invoked, no WhatsApp message is sent, the response shape is identical to live with simulated: true and message_id shaped sim_<hex>. Test sends are never billed.

Keys may also be restricted to specific session IDs. Requests against a non-whitelisted session return 403 session_not_allowed.

bash
Authorization: Bearer alm_sk_live_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

Quickstart — first message in under 60 seconds

1

Connect a session.

Go to the dashboard → Sessions → New session → scan the QR code with the WhatsApp app on the phone you want to send from. Wait until status is connected.

2

Grab the session ID and an API key.

Copy the session UUID from the dashboard. Generate an alm_sk_test_… key first — you can ship the same code with alm_sk_live_… later.

3

Send.

bash
curl -X POST https://api.aulm.io/v1/messages/send-text \
-H "Authorization: Bearer alm_sk_test_..." \
-H "Content-Type: application/json" \
-d '{"session_id":"<SESSION_ID>","to":"+14155551234","body":"Hello!"}'

Sessions are dashboard-only.

There is no public API endpoint to create a session, pair it via QR, or disconnect it. All session lifecycle is managed in the aulm dashboard. The API only operates on sessions that are already connected. GET /v1/sessions/{id}/status is read-only.

Base URL & versioning

All endpoints are served from a single base URL. The path is versioned with /v1/. Breaking changes get a new path prefix; additive changes (new optional fields, new response headers) ship under /v1/.

text
https://api.aulm.io

Responses & request IDs

All /v1/* responses are JSON. Success bodies use { data: ..., request_id: ... }; errors use { error: { code, message } }.

Every response — success or error — sets x-request-id. Log it on your side and quote it to support; we can find the exact send in one query.

Send endpoints additionally stamp x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset (ISO 8601), and retry-after on 429s and on upstream-overload 503s.

Platform headers to ignore

x-deployment-idInjected by the hosting edge on 2xx responses. Not part of the aulm contract — ignore.

Reliability

When a send fails, two things matter: did it cost you a quota slot, and should you retry the same request. aulm publishes both answers explicitly.

Quota refund matrix

What happens to your reserved quota slot when a send fails. Transient = refunded and not billed. Client error = slot kept.

ConditionStatuserror.codeRefunded?Billable?Retry?
Upstream timeout504messaging_service_timeoutYesNoYes, same request
Network / unreachable502messaging_service_unreachableYesNoYes, same request
Upstream 5xx502messaging_service_errorYesNoYes, same request
Upstream auth failure502messaging_service_errorYesNoYes (ops alerted)
Upstream 429 (overload)503service_unavailableYesNoYes, honor Retry-After
Recipient unreachable (upstream 404)404invalid_requestNoYesNo — fix recipient
Session not ready (upstream 409/412/422)409session_not_readyNoYesNo — connect session first
Other client error (4xx)400invalid_requestNoYesNo — fix request

Webhook retries

After a failed attempt, the next attempt is scheduled per this table. Attempt count is 1-indexed and matches `x-aulm-attempt`. `Retry-After` from your response overrides the ladder (capped at 1h).

Attempt just failedNext retry
11 minute
25 minutes
330 minutes
42 hours
512 hours
6Dead-lettered
  • Default max attempts per delivery: 6.
  • Endpoint auto-disabled after 25 consecutive failures (across deliveries).
  • Per-attempt HTTP timeout: 10s.
  • Target first-delivery latency: ~60s.

Errors

Errors always come back as { error: { code, message } }. The HTTP status tells you the broad class; the error.code tells you the exact cause. See the full error-code index at the bottom of this page.

json
{
"error": {
"code": "session_not_ready",
"message": "Session is not ready to send messages. Connect the session first."
}
}

Send a text message

POST/v1/messages/send-text
Sends a WhatsApp text message from a connected session to a phone number in E.164 format. Consumes one quota slot. If the messaging service fails transiently (5xx / timeout / unreachable / upstream 429), the slot is refunded automatically and the send is not billable. See the `x-aulm-reliability.quota_refund_matrix` extension at the bottom of this spec for the full matrix. **Test mode:** if the API key is a `test` key or the session is a test fixture, the call is simulated — no WhatsApp message is sent, the response status is `201`, `simulated: true`, and the `message_id` is shaped `sim_<hex>`.

Responses

  • 201Message accepted by the messaging service.
  • 400
  • 401
  • 402
  • 403
  • 404
  • 409
  • 429
  • 500
  • 502
  • 503
  • 504
bash
curl -X POST https://api.aulm.io/v1/messages/send-text \
-H "Authorization: Bearer $AULM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "f1e2d3c4-5678-4abc-9def-0123456789ab",
"to": "+14155551234",
"body": "Hello from aulm!"
}'
Example response
json
{
"data": {
"message_id": "true_14155551234@c.us_3EB0...",
"session_id": "f1e2d3c4-5678-4abc-9def-0123456789ab",
"to": "+14155551234",
"status": "sent",
"simulated": false
},
"request_id": "9c8b7a6d-1234-4abc-def0-112233445566"
}

Send an image message

POST/v1/messages/send-image
Sends a WhatsApp image with an optional caption. The `image` field accepts either: - A base64 data URI: `data:image/(png|jpeg|gif|webp);base64,...` The encoded payload is capped at roughly **5 MB of decoded image** (the raw request field has an absolute character cap of ~8 MB to allow for base64 overhead). - An `https://` URL pointing to a publicly resolvable host. The server probes the URL's `Content-Type` and refuses non-image responses. **SSRF protection:** private, loopback, and link-local addresses are rejected. Same quota, refund, and test-mode behavior as `/v1/messages/send-text`.

Responses

  • 201Image accepted by the messaging service.
  • 400
  • 401
  • 402
  • 403
  • 404
  • 409
  • 429
  • 500
  • 502
  • 503
  • 504
bash
curl -X POST https://api.aulm.io/v1/messages/send-image \
-H "Authorization: Bearer $AULM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "f1e2d3c4-5678-4abc-9def-0123456789ab",
"to": "+14155551234",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
"caption": "Receipt attached"
}'
Example response
json
{
"data": {
"message_id": "true_14155551234@c.us_3EB0...",
"session_id": "f1e2d3c4-5678-4abc-9def-0123456789ab",
"to": "+14155551234",
"status": "sent",
"simulated": false
},
"request_id": "9c8b7a6d-1234-4abc-def0-112233445566"
}

Get session connection status

GET/v1/sessions/{id}/status
Returns the cached connection status for a session owned by the caller's API key. Does **not** consume quota and does **not** call the messaging service — it reads aulm's cached row. Sessions themselves are created and QR-paired in the dashboard. There is no public endpoint to create a session.

Responses

  • 200Current session status.
  • 400
  • 401
  • 403
  • 404
  • 500
bash
curl https://api.aulm.io/v1/sessions/<SESSION_ID>/status \
-H "Authorization: Bearer $AULM_API_KEY"
Example response
json
{
"data": {
"session_id": "f1e2d3c4-5678-4abc-9def-0123456789ab",
"status": "connected"
}
}

Webhooks

Register a single HTTPS endpoint in the dashboard. aulm POSTs JSON events to it with a signed header, retries failures per the published ladder, and gives you a stable x-aulm-delivery-id for idempotency.

Request headers

x-aulm-eventEvent name: inbound_message or message_status.
x-aulm-delivery-idUUID for this logical event. Use as your idempotency key — the same value may arrive across retries.
x-aulm-signaturet=<unix>,v1=<hex>. HMAC_SHA256(secret, `${t}.${rawBody}`). Verify over the raw bytes.
x-aulm-attempt1-indexed attempt number.

Verifying the signature

Always verify over the raw request body bytes — do not re-serialize the JSON.

javascript
const crypto = require("crypto");

function verifyAulmSignature(rawBody, header, secret) {
  // header looks like: "t=1716893742,v1=abc123..."
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=")),
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  // timing-safe compare
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1 ?? "", "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Events

EVENTx-aulm-event: inbound_message

A WhatsApp message was received on a connected session.

Delivered to your registered webhook endpoint. The `x-aulm-event` request header equals `inbound_message`. Test deliveries triggered from the dashboard include `"test": true` at the top level of the body.

Payload
json
{
"event": "inbound_message",
"delivery_id": "00000000-0000-4000-8000-000000000000",
"timestamp": "2026-05-28T10:15:42.000Z",
"session_id": "00000000-0000-4000-8000-000000000000",
"test": false,
"message": {
"id": "string",
"from": "string",
"to": "string",
"chat_id": "string",
"type": "string",
"body": "string",
"is_group": false,
"from_me": false,
"media_url": "https://example.com/...",
"received_at": "2026-05-28T10:15:42.000Z"
}
}
EVENTx-aulm-event: message_status

Delivery status changed for a message you sent.

Delivered to your registered webhook endpoint. The `x-aulm-event` request header equals `message_status`. The `status` field is one of: `sent`, `delivered`, `read`, `played`, `failed`.

Payload
json
{
"event": "message_status",
"delivery_id": "00000000-0000-4000-8000-000000000000",
"timestamp": "2026-05-28T10:15:42.000Z",
"session_id": "00000000-0000-4000-8000-000000000000",
"message_id": "string",
"status": "sent",
"from": "string",
"to": "string",
"status_at": "2026-05-28T10:15:42.000Z"
}

Number warm-up & rate limits

To protect numbers from being banned by WhatsApp, two per-number limits apply to live sends (on top of your plan's daily quota). Test-mode keys (alm_sk_test_…) simulate sends and bypass these guards.

Send velocity

A number may send at most 12 messages per minute. Bursting above this returns 429 rate_too_fast with a Retry-After header. Pace your sends.

Warm-up ramp

A newly-connected number's daily send limit grows over its first two weeks, then your plan quota applies:

Number age (since connected)Max sends/day
Day 0–220
Day 3–650
Day 7–13200
Day 14+Your plan's daily limit

Exceeding the warm-up cap returns 429 warmup_daily_limit. A new number on any plan is limited during warm-up regardless of plan size — this is intentional and protects deliverability.

Opt-out (STOP)

If a recipient replies STOP, UNSUBSCRIBE, CANCEL (or equivalent), they are added to your suppression list and further sends to them return 403 recipient_opted_out. This keeps block/report rates low.

Recipient validation

Before a live send, the recipient is checked against WhatsApp. Numbers that are valid E.164 format but not registered on WhatsApp return 422 recipient_not_on_whatsapp. Malformed phone numbers return 404 invalid_request. Both keep the reserved quota slot (no charge).

Plans & limits

Sessions per plan

PlanActive sessionsMessage limit
trial120 total across a fixed 5-day window (not per day)
starter11000 / day (UTC)
pro210000 / day (UTC)
business6100000 / day (UTC)

Trial is a fixed 5-day window with a monotonic counter — it ends at whichever comes first: hitting the cap or 5 days from signup. Daily plans reset at next UTC midnight.

New numbers are additionally limited by the warm-up ramp for their first ~2 weeks, regardless of plan.

Error-code index

Statuserror.codeMeaningQuota slot
400invalid_jsonRequest body is not valid JSON.
400invalid_requestValidation failed (missing field, bad phone, bad image).
401missing_authorizationNo Authorization header.
401invalid_authorizationAuthorization header isn't Bearer scheme.
401invalid_api_keyKey does not match any active record.
401api_key_revokedKey was revoked or soft-deleted.
401api_key_expiredKey passed its expiry date.
401account_deactivatedCustomer account is soft-deleted.
402subscription_requiredNo active plan and trial is over.
403session_not_allowedAPI key is restricted from this session.
403recipient_opted_outRecipient replied STOP/unsubscribe and is suppressed for this customer (live sends only).Kept
404session_not_foundSession UUID doesn't exist for this account.
404invalid_requestMalformed/invalid recipient (bad format).Kept
409session_not_readySession exists but is not connected.Kept
422recipient_not_on_whatsappRecipient number is valid but is not registered on WhatsApp (live sends only).Kept
429trial_quota_exceededHit your trial cap. See x-ratelimit-reset.
429daily_quota_exceededHit your daily cap. Resets at next UTC midnight.
429rate_too_fastPer-number send velocity exceeded (12/min). Honor Retry-After (live sends only).Kept
429warmup_daily_limitNew number's warm-up daily cap reached (ramps over ~14 days, live sends only).Kept
500internal_errorServer-side failure. Quote x-request-id to support.
502messaging_service_unreachableNetwork failure to upstream.Refunded
502messaging_service_errorUpstream returned 5xx or auth-failed.Refunded
503service_unavailableUpstream overload. Honor Retry-After.Refunded
504messaging_service_timeoutUpstream did not respond in time.Refunded