Documentation

WhatsApp Direct Connection

How to link a number by QR, send messages, receive events in your system and avoid restrictions from Meta.

Updated 12 November 2026

Contents

Overview

Direct Connection links an ordinary WhatsApp number (personal or WhatsApp Business) to our API by scanning a QR code, just like linking WhatsApp Web. From then on you send messages and receive what your customers write, from n8n, your CRM or your own code.

What you get:

  • an access key to send messages and check the status of your numbers;

  • a webhook: every message received and every status change reaches the address you choose, signed;

  • the customer portal, where you link the number and follow your usage.

All requests go to:

https://wa.automate-ai.digital

Know the risk before you start. Direct Connection is not Meta's official channel. Meta may restrict a number it considers automated, and that is outside our control. Follow the good practices and, for high volumes or campaigns, use the Official API.

Getting started

In the portal, under My products, click Link this number. A QR code appears. On your phone, open WhatsApp, go to Linked devices → Link a device and scan the code.

The code stays active for 5 minutes. If it expires, click Link again. The number shows as connected as soon as the scan completes.

The phone does not need to stay online: the connection works as one more device on your account.

2. Set up the webhook and the key

In the portal, under Technical integration:

  1. Webhook: enter the address that will receive events. The portal shows the webhook secret once; it is what verifies the signature. Keep it.

  2. Access key: generate the key. It is also shown only once.

Authentication

Every request carries the key in the x-api-key header:

curl https://wa.automate-ai.digital/instances/NUMBER_ID \
  -H "x-api-key: wak_xxxxxxxx_xxxxxxxx"

Every response has the same shape:

{ "ok": true, "data": { "...": "..." } }
{ "ok": false, "error": { "code": "INSTANCE_NOT_FOUND", "message": "…", "retryable": false } }

The NUMBER_ID (instanceId) comes in every event your webhook receives.

A new key invalidates the previous one the moment it is generated. Anything using the old key stops working until it is updated.

Sending messages

curl -X POST https://wa.automate-ai.digital/instances/NUMBER_ID/messages \
  -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{
    "to": { "kind": "contact", "id": "351912345678" },
    "content": { "type": "text", "body": "Hi! Your order is on its way." },
    "idempotencyKey": "order-1234-confirmation"
  }'
  • to.id is the number in international format, without + or spaces. For a group: { "kind": "group", "id": "GROUP_ID" }.

  • idempotencyKey (optional, recommended): if the same request is repeated with the same key, the message is not sent twice.

  • replyTo (optional): the providerMessageId of the message you are replying to, so it shows as a quote.

The response is 202 with the message id:

{ "ok": true, "data": { "messageId": "44444444-…", "queuedAt": "2026-11-12T10:00:00.000Z" } }

202 means the message entered the queue, not that it went out. The outcome arrives later on the webhook, as message.status.

Messages go out in the order they come in, with a small random gap between them. That is on purpose: bursts are what leads Meta to restrict numbers.

Content types

type

Fields

text

body (up to 4096 characters)

image

media, caption

video

media, caption

audio

media, voiceNote (true sends it as a voice note)

document

media, filename, caption

location

latitude, longitude, name, address

media is { "kind": "url", "url": "https://…" }, or { "kind": "base64", "data": "…", "mimeType": "image/png" }.

Buttons do not exist on this channel. A request with "type": "buttons" fails with CAPABILITY_UNAVAILABLE. The API does not turn them into a numbered list, because the customer's answer would arrive as free text and a flow waiting for the tap would wait forever. For buttons, use the Official API.

Nor do templates. With Direct Connection you can write to anyone at any time, with no templates and no 24-hour window. Always use /messages.

Groups

The number can send and receive messages in groups it belongs to. So that your flow is not flooded with every group conversation, only messages from the groups you authorise reach the webhook.

  1. List the number's groups:

curl https://wa.automate-ai.digital/instances/NUMBER_ID/groups -H "x-api-key: $KEY"
{ "ok": true, "data": [{ "id": "120363012345678901", "name": "Sales team" }] }
  1. Authorise the ones you want to receive. The list replaces the previous one, and takes effect immediately:

curl -X PATCH https://wa.automate-ai.digital/instances/NUMBER_ID \
  -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{ "allowedGroups": ["120363012345678901"] }'

Group messages arrive with via, the group, and from, who wrote.

Events

Each event reaches your webhook as a POST with a JSON body. This channel has three types:

type

When

message.received

someone wrote

message.status

a send changed status: sent or failed

connection.updated

the number's status changed

Ignore the types you do not know, answering 2xx. The list will grow, and a flow that assumes "if it is not A, it is B" treats a new type as something it is not.

message.received

{
  "type": "message.received",
  "eventId": "0f0f6ab7-2a2f-4f0b-9b6a-9a5f7d5d31c2",
  "instanceId": "0d9b1b6f-6a2a-4a4c-9f66-3f6a3f0b6a2a",
  "provider": "baileys",
  "occurredAt": "2026-11-12T18:41:07.000Z",
  "providerMessageId": "3EB0C767D26B8A1F",
  "from": "351912345678",
  "pushName": "Maria",
  "content": { "type": "text", "body": "Hi, I'd like to know about my order" },
  "windowExpiresAt": null
}

content types:

type

Fields

text

body

media

mediaType (image, video, audio, document, sticker), storageUrl, storagePath, mimeType, filename, caption

location

latitude, longitude, name

reaction

emoji, targetMessageId

unsupported

providerType: a format the API cannot read yet

The media storageUrl is temporary: use it to download the file right away, not to store it.

If your number has message grouping turned on, consecutive texts from the same customer arrive as one event, joined by line breaks. That event carries every id in providerMessageIds.

message.status

On this channel you get sent (the message went out) and failed (it did not: failureCode says why). Each carries the messageId the API returned when you sent. "Delivered" and "read" are not sent on this channel.

connection.updated

Carries status: connecting, qr_required, connected, disconnected or logged_out.

  • disconnected: the connection dropped. The API reconnects on its own.

  • logged_out with requiresReauth: true: the number must be linked again in the portal, with a new QR. This happens, for example, when someone ends the session under Linked devices on the phone.

Verifying the signature

Every webhook request carries:

x-timestamp: 1794519022
x-signature: sha256=9c1f…

The signature is an HMAC-SHA256, with the webhook secret, over x-timestamp + "." + raw body. To verify it:

  1. reject the request if x-signature or x-timestamp is missing;

  2. reject it if x-timestamp is more than 5 minutes away from your clock;

  3. compute the HMAC over the raw body, byte for byte. If you re-serialise the JSON, the signature fails for some events;

  4. compare using a constant-time comparison.

const crypto = require("crypto");

function validSignature(secret, timestamp, rawBody, signature, nextSignature) {
  if (!signature || !timestamp) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = Buffer.from(
    "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex")
  );
  const matches = (h) => !!h && Buffer.from(h).length === expected.length && crypto.timingSafeEqual(Buffer.from(h), expected);
  return matches(signature) || matches(nextSignature);
}

Verify before you answer. If the signature fails, answer 401: the event is kept on our side and is not lost. If you answer 2xx before verifying, an event rejected afterwards is lost without a trace.

Changing the secret without losing events. During a change, every request also carries x-signature-next, signed with the new secret. Accept either signature. That way you can switch to the new secret whenever you like, with no window in which events fail.

Errors

In the request's response. These are fixed in the request itself:

HTTP

code

When

400

INVALID_PAYLOAD

the body is not right (the message says what)

401

UNAUTHORIZED

key missing, wrong or revoked

404

INSTANCE_NOT_FOUND

the number does not exist in your account

409

INSTANCE_NOT_CONNECTED

only on GET /groups: the number is not connected

On the webhook, in a message.status of failed. What depends on WhatsApp arrives here, after the 202:

failureCode

When

INSTANCE_NOT_CONNECTED

the number is not connected. The queue tries again before giving up

CAPABILITY_UNAVAILABLE

something this channel does not do (for example, buttons)

PROVIDER_REJECTED

WhatsApp refused or did not confirm the send. The reason is in failureReason

Event delivery

  • Answer 2xx as soon as you receive the event, before processing it. Each attempt has 10 seconds. An endpoint that only answers at the end of the flow risks receiving the same event again.

  • Deduplicate by eventId. Each event is delivered at least once and may arrive more than once.

  • Order by occurredAt, not by arrival order.

  • Retries: on a 5xx, a 429 or no answer, we retry up to 8 times over about 30 minutes. A 4xx (except 429) is not retried: the event is kept on our side.

Good practices

What Meta treats as abusive automation, and what leads it to restrict numbers:

  • messages to people who did not ask for them, especially in quantity and with the same text;

  • many messages in a row to contacts who never wrote to the number;

  • new numbers that start sending in volume straight away.

What helps:

  • reply to people who write to you, and send notices to customers who have already talked to you;

  • vary the text and avoid shortened links;

  • start slowly with a new number;

  • for campaigns, use the Official API with approved templates.

Usage and limits

  • Each number includes 3,000 sent messages per billing cycle. Received messages do not count.

  • Beyond that, you buy blocks of 1,000 messages in the portal.

  • Each number's usage is in the portal, under My products.

A note on senders. WhatsApp is starting to identify some conversations by an internal code instead of the phone number. When the sender's phone is not known, the message is stored but not sent to the webhook: the from field must be a number you can reply to, and that code is not.