Dev.to · 9 min read

One LINE Official Account, Multiple Tools: Webhook and Token Architecture

One LINE Official Account, Multiple Tools: Webhook and Token Architecture

A completed WhatsApp Embedded Signup dialog does not mean that a customer is ready to send and receive messages. The browser flow may have finished while the backend still has one of these problems: The result was attached to the wrong tenant The access token belongs to another app The wrong WhatsApp Business Account was selected The system user lacks the required access The phone number is not ready The app is not subscribed to WABA webhooks The first webhook cannot be routed to the customer A SaaS application should display Connected only after every backend gate has passed. Model onboarding as a state machine Avoid representing onboarding with one Boolean field such as: connected: true Use explicit states instead: type WhatsAppOnboardingStatus = | "started" | "browser_finished" | "token_validated" | "waba_resolved" | "access_verified" | "phone_ready" | "app_subscribed" | "webhook_pending" | "active" | "verification_required" | "failed"; A safe transition path is: started ↓ browser_finished ↓ token_validated ↓ waba_resolved ↓ access_verified ↓ phone_ready ↓ app_subscribed ↓ webhook_pending ↓ active Each transition should store evidence, not just a timestamp. Define the acceptance gates Meta's official Embedded Signup collection separates the browser flow from the Graph API work required afterward. Gate Evidence Failure risk Session correlation Tenant ID, state, configuration ID Assets attached to the wrong customer Token validation App ID, scopes, expiry metadata Token exists but cannot manage the WABA WABA resolution Exact WABA ID and business mapping First list result belongs to another customer System-user access System-user ID and required task Later API operations fail Phone readiness Phone-number ID and onboarding path WABA exists but messaging is unavailable App subscription App appears in subscribed_apps Meta receives messages but sends no webhook Delivery proof One correctly routed webhook Configuration passes without real delivery A tenant should remain unavailable until all required gates pass. 1. Correlate the browser result with a server session Create the onboarding session on your server before opening Embedded Signup. Store: type OnboardingSession = { id: string; tenantId: string; startedByUserId: string; state: string; configurationId: string; onboardingPath: "cloud_api" | "coexistence"; expiresAt: string; consumedAt?: string; }; The state value should be: Random Single-use Bound to one tenant Bound to the initiating user Short-lived Validated on the server When the browser reports completion: Look up the server session Verify the state Reject expired sessions Reject already-consumed sessions Verify that the current user can modify the tenant Mark the result as consumed atomically Do not trust a tenant ID submitted by the browser if it can be derived from the authenticated server session. If the Coexistence flow emits FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING, treat it as evidence that the dialog finished—not evidence that the backend is active. 2. Keep credentials out of the browser Credential exchange and Graph API verification belong on the server. Do not place access tokens in: Browser storage Query strings Client-side analytics Error-reporting breadcrumbs Support screenshots Application logs Database fields returned by public APIs Store the credential in a secret manager or encrypted server-side store. Persist only the audit metadata your application needs: type TokenAuditMetadata = { tenantId: string; appId: string; tokenType: string; grantedScopes: string[]; issuedAt?: string; expiresAt?: string; lastValidatedAt: string; }; A non-empty token is not proof that it belongs to the expected Meta app or has the permissions required for the selected WABA. 3. Resolve the intended WABA deterministically The official collection exposes this endpoint for retrieving client WABAs: GET /{business-id}/client_whatsapp_business_accounts A business can return multiple WABAs. Never select the first array element: // Unsafe const waba = response.data[0]; Match against information captured for the current onboarding session: function resolveWaba( availableWabas: Array, expectedWabaId: string, ) { const matches = availableWabas.filter( (candidate) => candidate.id === expectedWabaId, ); if (matches.length !== 1) { throw new Error("expected_waba_not_resolved"); } return matches[0]; } Store the relationship explicitly: type TenantWabaMapping = { tenantId: string; metaBusinessId: string; wabaId: string; sourceSessionId: string; verifiedAt: string; }; If no exact match exists, move the tenant to verification_required. Do not silently attach another WABA. 4. Verify system-user access Meta documents the following endpoint for checking assigned users: GET /{waba-id}/assigned_users?business={business-id} Verify: The expected system user is present It belongs to the expected business It has the task required by your integration Your backend credential can perform the required WABA operations Do not treat any returned system user as sufficient. A system user added for a different operational role may not have the access your message or template workflow requires. 5. Branch by onboarding path Standard Cloud API onboarding and WhatsApp Business App Coexistence are not the same phone-number path. Standard Cloud API Depending on the current onboarding contract, the backend may need to register the phone number and verify that its status permits messaging. Coexistence Coexistence uses a number already connected to the WhatsApp Business app. Do not automatically repeat the standard registration operation. Instead, validate: The number returned by the Coexistence flow Its current status Its WABA association The intended synchronization behavior The supported message-history boundary Which surface owns each business operation Keep the path in your tenant configuration: type WhatsAppConnection = { tenantId: string; wabaId: string; phoneNumberId: string; mode: "cloud_api" | "coexistence"; status: "verification_required" | "active" | "failed"; }; This prevents later jobs from applying Cloud API assumptions to a Coexistence number. 6. Subscribe the app to the WABA A valid WABA and phone number do not prove that webhook delivery is configured. Meta's official collection uses: POST /{waba-id}/subscribed_apps Verify the result afterward: GET /{waba-id}/subscribed_apps The expected Meta app must appear in the returned subscription list. Treat subscription as a separate gate: async function verifyAppSubscription( subscriptions: Array, expectedAppId: string, ) { const subscribed = subscriptions.some( (item) => item.whatsapp_business_api_data?.id === expectedAppId, ); if (!subscribed) { throw new Error("waba_app_subscription_missing"); } } Registration can succeed while subscription fails. Subscription can also exist while the selected phone number is not operational. Do not combine these conditions into one status. 7. Prove one real webhook delivery Configuration reads are necessary but not sufficient. Before marking the tenant active, require one controlled webhook to reach a production-equivalent receiver. The acceptance path should be: Controlled WhatsApp message ↓ Meta webhook delivery ↓ Signature verification ↓ WABA and phone-number lookup ↓ Tenant resolution ↓ Idempotency check ↓ Durable event storage ↓ Successful acknowledgement For a Meta webhook, verify the signature against the raw request body before parsing or routing it. An illustrative Node.js helper: import { createHmac, timingSafeEqual } from "node:crypto"; function verifyMetaSignature( rawBody: Buffer, signatureHeader: string, appSecret: string, ): boolean { const prefix = "sha256="; if (!signatureHeader.startsWith(prefix)) { return false; } const expected = createHmac("sha256", appSecret) .update(rawBody) .digest(); const actual = Buffer.from( signatureHeader.slice(prefix.length), "hex", ); return ( actual.length === expected.length && timingSafeEqual(actual, expected) ); } Never log the app secret, token, or complete sensitive webhook payload while debugging verification. 8. Route by provider assets, not browser state The webhook receiver must resolve the tenant from trusted provider identifiers. Relevant identifiers can include: WABA ID Phone-number ID Meta app ID Internal connection ID Do not route a production webhook using a tenant ID previously stored in browser state without verifying its asset mapping. A routing table can look like: CREATE TABLE whatsapp_connections ( id VARCHAR(64) PRIMARY KEY, tenant_id VARCHAR(64) NOT NULL, meta_app_id VARCHAR(64) NOT NULL, waba_id VARCHAR(64) NOT NULL, phone_number_id VARCHAR(64) NOT NULL, onboarding_mode VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL, UNIQUE (meta_app_id, waba_id, phone_number_id) ); If the mapping is missing or ambiguous: Do not guess Do not assign the event to the most recently onboarded tenant Store it in a restricted reconciliation queue Alert the integration owner Keep the tenant out of active 9. Make webhook processing idempotent Webhook delivery can be retried. Choose an idempotency key from stable provider fields appropriate to the event type. For inbound messages, the provider message ID is usually part of that identity. Store the event before running slow business logic: Verify signature ↓ Resolve tenant ↓ Insert event if absent ↓ Return success ↓ Process asynchronously A duplicate should not create: Two conversations Two automated replies Two billing records Two workflow executions Two customer notifications Operational readiness means that both the first delivery and a repeated delivery are safe. 10. Treat credit-line attachment as conditional The official Embedded Signup collection includes credit-line sharing for provider-paid arrangements. That does not make credit-line attachment a universal readiness gate. Apply it only when: Your business owns the billing relationship with Meta Your partner model requires credit sharing The customer WABA is expected to use that credit line For customer-paid or other supported billing arrangements, document the appropriate billing check separately. Do not block every tenant on a credit-line operation that does not belong to its commercial model. 11. Separate platform approval from operational readiness This checklist does not prove: Meta business eligibility App Review approval Advanced Access approval Display-name approval Number quality Template approval Messaging-limit tier Policy compliance Those are separate platform states. Maintain them independently: type WhatsAppReadiness = { embeddedSignup: "pending" | "completed"; appReview: "unknown" | "approved" | "rejected"; wabaAccess: "unknown" | "verified" | "failed"; phoneStatus: "unknown" | "ready" | "failed"; webhookSubscription: "unknown" | "verified" | "failed"; deliveryTest: "pending" | "passed" | "failed"; }; A successful test tenant also does not prove that every customer configuration will work. Keep the verification process repeatable for every onboarding session. Recommended failure states Return actionable states instead of a generic connection error. State Meaning Next action session_mismatch Browser result cannot be correlated Restart onboarding token_invalid Credential validation failed Repeat credential exchange waba_not_resolved Expected WABA was not found Review business selection system_user_missing Required access is absent Assign or repair system user phone_not_ready Phone path is incomplete Follow path-specific recovery subscription_missing App is not subscribed Subscribe and verify again webhook_not_received No delivery proof exists Inspect webhook and routing routing_ambiguous Asset mapping matches multiple tenants Stop and reconcile mappings These states make support, retry behavior, and audit logs much easier to reason about. Final post-onboarding checklist [ ] Server-side onboarding session exists [ ] State is valid, single-use, and tenant-bound [ ] Browser result has been consumed exactly once [ ] Credentials remain server-side [ ] Token app, permissions, and expiry were validated [ ] Exact WABA was resolved [ ] WABA was not selected by array position [ ] System-user access was verified [ ] Standard and Coexistence paths are separated [ ] Phone-number ID is stored [ ] Phone status is ready for the selected path [ ] Expected app appears in subscribed_apps [ ] Billing checks match the commercial model [ ] One real webhook reached the receiver [ ] Webhook signature was verified [ ] WABA and phone number resolved to one tenant [ ] Duplicate delivery is safe [ ] Failure states are recoverable [ ] Only then is the tenant marked active The browser dialog finishing is a user-interface milestone. Operational readiness requires a verified chain from the server-side onboarding session to the correct WABA, system user, phone number, app subscription, tenant mapping, and real webhook delivery. Official references Meta Embedded Signup collection Meta Embedded Signup v4 Onboarding WhatsApp Business app users Originally published on UnifyPort. This article was prepared with AI assistance for language and structure, then technically reviewed and verified by the author.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News