Migration and reference
Migrate from Meta Cloud API
Move provider credentials, sending, webhook delivery, and operational controls from a direct Meta integration to Yanola.
What this guide covers
- Replace Meta access tokens in application code with a Yanola API key.
- Map Meta phone numbers to Agent Project-scoped Yanola Senders.
- Choose normalized or Meta-compatible webhook payloads during migration.
Choose the migration boundary
Yanola becomes the provider boundary between your application and Meta. Your application calls Yanola with a Yanola API key; Yanola owns the connected Meta credential, provider call, delivery reconciliation, and operational record.
WhatsApp product rules still apply. The migration removes provider credentials and Graph-version maintenance from application code, but it does not bypass number registration, template approval, the 24-hour Customer Service Window, or WhatsApp policy.
| Area | What changes | What stays familiar |
|---|---|---|
Authentication | A project- and environment-scoped YANOLA_API_KEY replaces the System User access token in application code. | Credentials remain server-side secrets. |
Sending | Use @yanola/whatsapp or REST /v1 instead of a versioned Graph messages endpoint. | Recipients, content types, templates, and asynchronous delivery remain recognizable. |
Sender | Pass the connected E.164 number as from instead of putting a Meta phone-number ID in the URL. | The same WhatsApp business number is still the customer-visible identity. |
Webhooks | Yanola configures, signs, retries, logs, and replays deliveries. | Meta-compatible mode retains the object → entry → changes → value parser shape. |
Operations | Yanola public IDs, logs, Test Mode, rate limits, scopes, and usage records become the control surface. | Final sent, delivered, read, and failed outcomes still arrive asynchronously. |
Billing | Live API sends are included in the Plan up to the Message Cap. | Meta bills your WhatsApp Business Account directly; providerAmount is null on included sends. |
Map direct Meta concepts to Yanola
| Direct Meta integration | Yanola equivalent | Migration action |
|---|---|---|
Meta app and WhatsApp Business Account context | Yanola Account plus Agent Project | Place each application boundary in the Agent Project that owns its keys, webhooks, logs, and Senders. |
System User access token | yalo_test_... or yalo_live_... API key | Store the copy-once key as YANOLA_API_KEY and select least-privilege scopes. |
PHONE_NUMBER_ID in /<PHONE_NUMBER_ID>/messages | E.164 from Sender | Resolve it once with client.senders.list and keep the phone number, not a Meta identifier. |
Graph API version | Stable Yanola REST /v1 contract | Remove Graph-version construction and upgrade the SDK or /v1 contract deliberately. |
wamid message ID | msg_... or msg_test_... public message ID | Use the Yanola ID for retrieval, mark-read, logs, support, and webhook correlation. |
Meta inbound media ID | med_... public media ID | Retrieve a short-lived signed Yanola download URL with client.media.retrieve. |
Meta App Secret and verification token | whsec_... endpoint signing secret | Verify yanola-signature; endpoint setup no longer uses your Meta verification challenge. |
Yanola public IDs are opaque. Never strip a prefix, derive a Meta resource ID, or send a msg_, med_, evt_, or conv_ value to Graph.
Verify the connected Sender and templates
Connect the WhatsApp number to the intended Agent Project before changing application traffic. A key can use only Senders in that project and, when configured, only the key's explicit Sender allowlist.
Compare the Yanola template catalog with the names, languages, and components your direct integration uses. Catalog entries remain Sender-specific because approval still belongs to the underlying WhatsApp business account.
import { WhatsAppClient } from "@yanola/whatsapp"
const apiKey = process.env.YANOLA_API_KEY
if (!apiKey) {
throw new Error("Missing YANOLA_API_KEY")
}
const yanola = new WhatsAppClient({ apiKey })
const senderPage = await yanola.senders.list({ limit: 100 })
const sender = senderPage.data.find(
(candidate) => candidate.phoneNumber === "+15557654321"
)
if (!sender?.apiEligible) {
throw new Error("Sender is not API-eligible")
}
const templatePage = await yanola.templates.list({
from: sender.phoneNumber,
refresh: true,
limit: 100,
})
console.log(
templatePage.data.map(({ name, languageCode, status }) => ({
name,
languageCode,
status,
}))
)Use refresh: true only with a Live Mode key and under the documented refresh limits. Test Mode reads the isolated cached catalog and never contacts Meta.
Replace the Graph send call
A direct Cloud API text send posts to graph.facebook.com/<GRAPH_VERSION>/<PHONE_NUMBER_ID>/messages with messaging_product, type, and a nested text object. With Yanola, the key supplies account, project, and environment context while from selects the Sender.
The SDK adds Bearer authentication and generates an idempotency key when you do not supply one. Keep an application-owned key when a queue or business operation can retry the same logical send.
import { WhatsAppClient } from "@yanola/whatsapp"
const apiKey = process.env.YANOLA_API_KEY
if (!apiKey) {
throw new Error("Missing YANOLA_API_KEY")
}
const yanola = new WhatsAppClient({ apiKey })
const accepted = await yanola.messages.sendText(
{
from: "+15557654321",
to: "+15551234567",
body: "Your order is ready",
previewUrl: false,
},
{
idempotencyKey: "order-A-1042-ready-v1",
}
)
console.log(accepted.id, accepted.status, accepted.requestId)Yanola returns 202 with status queued and a public message ID. Observe later message retrieval or signed webhook events for the final outcome.
Translate message operations
Non-template Live Mode sends still require an inbound message to the same Sender within the previous 24 hours. A 422 service_window_required response should route the business flow to an appropriate approved template, not to an automatic retry.
| Direct Meta pattern | Yanola SDK method | Important difference |
|---|---|---|
type: text with text.body | messages.sendText | Move text.body to body and pass the E.164 Sender as from. |
type: template with template.language.code | messages.sendTemplate | Use name, languageCode, and components; the catalog must show that name/language as approved. |
image, video, document, or audio by link or uploaded ID | messages.sendMedia | V1 outbound media uses an HTTPS mediaUrl; it does not accept a raw Meta upload ID. |
type: location | messages.sendLocation | Use latitude, longitude, and optional name/address with the same geographic meaning. |
type: contacts | messages.sendContactCard | Use 1 to 10 contacts and camel-cased SDK fields such as formattedName. |
status: read with a provider message ID | messages.markAsRead | Pass the same-environment public inbound msg_ ID; raw provider message IDs are not accepted. |
Adopt Yanola IDs and state
Persist the accepted msg_ or msg_test_ ID as the primary messaging reference. Retain requestId for support and transport debugging, evt_ IDs for event deduplication, del_ IDs for delivery attempts, conv_ IDs for conversation correlation, and med_ IDs for inbound media.
A provider message ID can appear inside retained Meta-compatible detail, but it is not the Yanola public control-plane ID and might not exist on synthetic or non-Meta-originated events.
| Need | Use |
|---|---|
Retrieve message state | client.messages.retrieve(msgId) |
List inbound and outbound history | client.messages.list(...) |
Deduplicate business events | yanola-event-id or normalized payload id |
Debug one delivery attempt | yanola-delivery-id and client.webhookDeliveries.retrieve(...) |
Correlate usage and cost | Message ID plus client.usage.list(...) |
Mark an inbound message read | client.messages.markAsRead(publicMessageId) |
Move the webhook trust boundary
Create the Test Mode and Live Mode endpoints in the Agent Project dashboard. Yanola does not call your route with Meta's GET hub.challenge verification flow, and your handler must not require hub.mode or hub.verify_token before accepting Yanola deliveries.
Replace X-Hub-Signature-256 verification with the endpoint's copy-once YANOLA_WEBHOOK_SECRET and yanola-signature. Yanola signs <timestamp>.<exact raw body>, includes a five-minute default tolerance in the SDK helper, and can emit multiple v1 digests during secret rotation.
| Direct Meta | Yanola |
|---|---|
GET hub.challenge plus a verification token | Dashboard endpoint configuration and a Test Mode delivery |
X-Hub-Signature-256 | yanola-signature |
Meta App Secret | YANOLA_WEBHOOK_SECRET |
Provider request/batch identity | yanola-event-id and yanola-delivery-id |
Provider retry behavior | Any 2xx succeeds; 10-second timeout; up to 12 attempts with jitter; failed-delivery replay |
Provider diagnostics | Agent Project event and delivery logs with redacted response previews |
Adapt an existing Meta-shaped handler
Select meta payload mode to keep the unwrapped object, entry, changes, value, messages, contacts, and statuses shape while you replace the transport boundary. Verify Yanola's signature over the raw body before handing parsed JSON to the existing parser.
- Read yanola-signature, yanola-event-id, yanola-delivery-id, and yanola-payload-format case-insensitively from the request headers.
- Verify the exact raw body before JSON.parse or any side effect.
- Validate the runtime JSON before treating it as MetaWebhookPayload.
- Deduplicate with yanola-event-id, durably persist the event, and then return 2xx.
- Process the familiar messages or statuses arrays asynchronously.
import {
verifyYanolaWebhookSignature,
type MetaWebhookPayload,
} from "@yanola/whatsapp"
async function acceptYanolaDelivery(input: {
rawBody: string
signature: string | undefined
eventId: string | undefined
}) {
if (
!verifyYanolaWebhookSignature({
secret: process.env.YANOLA_WEBHOOK_SECRET!,
signature: input.signature,
rawBody: input.rawBody,
})
) {
throw new Error("Invalid Yanola webhook signature")
}
if (!input.eventId) {
throw new Error("Missing yanola-event-id")
}
const payload = JSON.parse(input.rawBody) as MetaWebhookPayload
await webhookInbox.insertOnce({
id: input.eventId,
payload,
})
}Know the Meta-compatible caveats
Meta-compatible means compatible JSON structure. It is not a byte-for-byte replay of Meta's original HTTP request, and it does not preserve Meta's authentication, batching, identifiers, timing, or every provider-only field.
| Caveat | Required handler behavior |
|---|---|
One triggering Developer Event | Yanola narrows a retained Meta batch to the message or status that produced the event. Do not require unrelated entries, changes, messages, or statuses from the original batch. |
Synthesized fallback | When retained provider data is absent or the event is synthetic, Yanola builds the same structural shape from normalized data. Treat optional provider fields as absent and unknown fields as additive. |
Identifier semantics | entry.id, metadata.phone_number_id, message/status id, and conversation.id can be compatibility values or Yanola public IDs. Do not use them to call Graph or assume a numeric Meta resource. |
Yanola transport | Verify yanola-signature and use Yanola headers. X-Hub-Signature-256, hub.challenge, Meta request headers, and Meta retry identity do not apply. |
Test Mode | The body is synthetic because Test Mode makes no Meta call. Test parser shape, signature handling, deduplication, and business routing—not provider-only field fidelity. |
Conversation event | whatsapp.conversation.started can render as a familiar inbound message shape. Subscribe only to the event types you need, or use normalized mode when the event type must be explicit in the body. |
At-least-once delivery | Deduplicate by yanola-event-id and do not rely on delivery order. A replay has a new delivery attempt identity and yanola-replayed: true. |
If your business logic depends on exact provider-only fields, isolate that dependency and test the retained and synthesized cases. Prefer the stable yanola envelope for new logic.
Migrate inbound media and read receipts
An existing Meta parser can recognize an inbound media-shaped message, but application code should move from Meta media downloads to the Yanola event and media resources. Retrieve the normalized event identified by yanola-event-id to obtain its public message and med_ references.
client.media.retrieve returns a short-lived signed Yanola proxy URL. Download it before expiresAt and never add a Meta Authorization header. To mark the inbound message read, pass its public msg_ ID to client.messages.markAsRead; a retained wamid is not accepted by that route.
import { WhatsAppClient } from "@yanola/whatsapp"
const apiKey = process.env.YANOLA_API_KEY
if (!apiKey) {
throw new Error("Missing YANOLA_API_KEY")
}
const yanola = new WhatsAppClient({ apiKey })
const event = await yanola.events.retrieve("evt_...")
const publicMessageId = event.data.data.message?.id
const publicMediaId = "med_..." // From the normalized inbound payload
const media = await yanola.media.retrieve(publicMediaId)
const response = await fetch(media.downloadUrl)
const bytes = await response.arrayBuffer()
console.log(bytes.byteLength, media.expiresAt)
if (typeof publicMessageId === "string") {
await yanola.messages.markAsRead(publicMessageId)
}The signed download URL is short-lived and scoped. Keep it out of logs and persistent public records, and fetch a new URL from the med_ ID after expiry.
Test the cutover without double sends
- Create a narrowly scoped Test Mode key and a separate Test Mode webhook endpoint in meta payload mode.
- Run representative text, template, media, location, contact-card, and mark-read requests through @yanola/whatsapp and assert msg_test_ IDs, logs, synthetic events, and signatures.
- Exercise duplicate delivery and replay handling with yanola-event-id before enabling a Live endpoint.
- Create the Live Mode key and endpoint separately; never reuse Test Mode secrets or assume test records cross environments.
- Choose one send path per business operation during rollout. Do not call Meta directly and Yanola for the same recipient action.
- Canary a low-volume Live Sender, monitor message state, webhook deliveries, rate-limit headers, and usage records, then expand traffic deliberately.
It validates the Yanola contract without a provider call or Yanola Balance usage. Complete at least one controlled Live Mode canary for credentials, template approval, Customer Service Window behavior, and real delivery status.
Complete the production cutover
- Confirm every production number appears as an active, API-eligible Sender under the intended Agent Project.
- Confirm every required template name, language, and component layout through client.templates.list.
- Remove Graph URL construction and Meta access-token reads from application services, workers, and deployment secrets that no longer need them.
- Persist Yanola public IDs and update support, analytics, and reconciliation queries that previously keyed only on wamid.
- Switch webhook verification, deduplication, health checks, and alerts to the Yanola delivery contract.
- Fund Yanola Balance, verify subscription entitlements, and set throughput around documented rate-limit headers.
- Keep a rollback window that prevents duplicate sends, then disable the old direct callback/send path after Yanola logs and live statuses are healthy.
- Plan a follow-up move from meta payload mode to yanola mode so new code receives explicit event types, environment, and stable public resource projections.