✅ Meta App Review approved — GoSendAPI Cloud is live. Sign up free →
Changelog

Changelog

Major changes to the API, dashboard and infrastructure that affect integrators.

Versioning policy: while we’re in beta, we don’t ship breaking changes without 30 days advance notice via email + this changelog. Once GA, we’ll follow semver on the API path (/v1, /v2, etc.).

2026-08-13 — AI Agents: four LLM providers wired (Anthropic, OpenAI, Google, OpenRouter)

Second piece of the AI Agents runtime lands: the provider layer. The engine is now provider-agnostic — you declare your agent once and pick which LLM runs it.

Four built-in providers:

  • Anthropic — Claude family with explicit prompt-caching breakpoints.
  • OpenAI — GPT family with automatic caching for prompts over 1024 tokens.
  • Google — Gemini with optional cachedContent caching.
  • OpenRouter — a single billing account for dozens of open-source models (Llama, DeepSeek, Mistral, Qwen, and more).

Extensible registry: if none of the four fits your case, register your own provider (Cohere, a custom internal proxy, a hosted fine-tune) without touching the platform core. See the new LLM providers guide for the interface and how to bring your own.

Cost estimation: every model has a per-token rate card with four dimensions (input, output, cache read, cache write). Once the runtime lands in the next phase, AgentRun.costUsd is computed from real usage — not from estimates or averages.

What is still not usable: the runtime loop that actually invokes the models ships next (Phase 1.d). Until then, the registry is bootable but idle. Configuration is via env vars (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, OPENROUTER_API_KEY); a missing key marks the provider as unavailable instead of failing startup.

2026-08-13 — AI Agents module: data model + tool engine (Phase 1 begins)

The first pieces of the AI Agents module are live on the platform. This is the runtime that lets you point a language model at your own APIs — with a declarative, HTTP-based contract, not code.

What ships today:

  • Data model — 6 new entities: Agent, AgentVersion, AgentBinding, AgentThread, AgentRun, AgentEvent, plus an AgentTool catalog scoped per tenant. No public CRUD yet — schema is in place so the runtime has ground to stand on.
  • Multi-LLM from day one — 4 built-in providers: Anthropic, OpenAI, Google, and OpenRouter (for open-source models like Llama, DeepSeek, Mistral, Qwen with a single billing account). The LLMProvider is a registry — you can add your own provider without changing the core.
  • Tool declaration engine — the piece that lets an agent reach your ERP or booking system. Fully documented in the new AI Agents — Tools declaration guide.

Highlights of the tool engine:

  • Three namespaces: {{input.*}} (from the model, untrusted), {{binding.*}} (deployment config, trusted), {{context.*}} (runtime, trusted).
  • Hard SSRF floor: {{input.*}} cannot appear in request.url, request.auth.token, .key, or .password. Rejected at declaration time, re-checked at runtime.
  • Allowlist response extraction with pick — the model sees exactly the fields you declare, nothing else. Under Ley 25.326 and GDPR this is a compliance control, not a performance optimization.
  • Error classification distinguishes business errors (return to model) from infrastructure errors (escalate to a human). 401 and 403 are never sent back to the model as “business” — they’re broken auth, and the model would confidently mislead the user.

What is NOT ready yet: no LLM providers wired to the tool engine, no agent runtime loop, no queue, no public CRUD endpoints. Those ship in the following phases (F1.b through F1.j). Watch this changelog.

If you want to prepare an integration, the tool declaration format is stable — the endpoint that consumes it will land in a future release.

2026-08-13 — New occurred_at field on messages and webhooks

Every Message object exposed by the platform now carries an additional timestamp: occurred_at — when the message actually happened in WhatsApp — alongside the existing created_at — when the row was persisted in your account.

The distinction only matters for a specific case, but it matters a lot: coexistence history import. When a phone number reconnects (or joins the platform via Embedded Signup), Meta backfills every message sent and received while the number was disconnected. Those rows are persisted right now, but their events happened days, weeks or months ago. Sorting or filtering by created_at treats all of them as if they arrived today; sorting by occurred_at puts them in their real position on the timeline.

Where it appears:

  • GET /v1/messages — every message in the response now includes occurred_at as an ISO 8601 timestamp.
  • GET /v1/messages/{id} — same field on the single-message response.
  • Webhook envelopes for whatsapp.message.received, whatsapp.message.sent, whatsapp.message.delivered, whatsapp.message.read, whatsapp.message.failed and whatsapp.message.echoed — the inner message object now includes occurred_at (derived from Meta’s original epoch timestamp).

How the two timestamps relate:

  • created_at — when the row entered the platform’s database. Monotonic with id. Useful for “give me everything new since T” polling flows and for audit trails.
  • occurred_at — when the message actually happened in WhatsApp. Monotonic with the real chronological order of the conversation. Useful for reporting, timeline UIs, and any query where “the last message from customer X” means the chronological last, not the last one we persisted.

For non-coexistence flows, both fields are within seconds of each other and either works. For coexistence history, use occurred_at when you want the real timeline.

Compatibility: additive only. created_at keeps its exact semantics. No existing integration breaks.

What you need to do: nothing, unless your integration processes coexistence history and would benefit from real timestamps — in which case switch your ordering, filtering or timeline rendering to occurred_at.


2026-08-12 — Coexistence history: full inbox rendering for template messages

When a WABA reconnects to the platform (or completes Embedded Signup after operating on the WhatsApp Business app), Meta backfills messages sent and received while the number was disconnected via the field=history webhook. Until this release, three fields were missing on those historical rows:

  • coexistence_echo was NULL on outbound history, making them look identical to messages sent via the API. Every outbound history message is coexistence by definition (the number existed before joining the platform, so the outbound came from the phone’s WhatsApp Business app). It is now flagged as such.
  • rendered_body was NULL for template messages. The inbox falls back to the raw text body for plain messages, but templates need their definition to interpolate variables — without it, the bubble rendered empty. The handler now pre-fetches the WABA templates for the batch and interpolates inline (best-effort: if the template was later removed from Meta, the row keeps NULL and the fallback renderer takes over).
  • message_type='errors' had no signal for the UI. Meta sometimes flags historical rows as errors when the content is no longer reconstructable (message deleted, media removed, expired). We now populate error_title (defaulting to history_import_error), error_code and error_message from the first error entry, so the inbox can render “unavailable: less than reason greater than” instead of an empty bubble. Status stays historical — no billing or count impact.

Backfill: two scripts are shipped for retroactive updates on rows persisted before the fix. The SQL script (api/scripts/backfill/history-flags.sql) handles coexistence_echo and error_title — idempotent, no payload decrypt needed. The TypeScript script (api/scripts/backfill/backfill-history-rendered-body.ts) handles rendered_body for template rows and defaults to dry-run.

What you need to do: nothing. Newly imported history from this point on is fully rendered. Existing history rows are covered by the backfill scripts and will be reprocessed as part of the rollout.


2026-08-11 — Message retention aligned with the published privacy policy

Effective immediately, we no longer auto-delete message content or metadata. This aligns the actual behavior of the platform with what our Privacy Policy §7 already says:

Message content and metadata — kept as long as the Customer’s account is active or as configured by the Customer.

What changed: an internal nightly job was deleting Message rows older than 90 days. That 90-day figure applies only to operational logs, not to message content. The job is now disabled by default and controlled by an internal environment variable that ships turned off.

What you need to do: nothing. Your historical messages are safe. If you had imported coexistence history that appeared empty in the inbox after the day of import, this was the cause; that behavior is now fixed going forward.

Coming next (separate work, not part of this change): per-Customer configurable retention (the “or as configured by the Customer” part of the policy). Today retention is uniform — kept while the account is active. When per-Customer retention ships, we’ll announce it here.

Sensitive contacts or conversations can still be deleted on request through the Service or by writing to privacy@gosendapi.com.


2026-08-06 — New webhook event whatsapp.contact.phone_received + request_contact_info interactive support

When a user shares their phone in response to a request_contact_info interactive message (Meta’s mechanism to get a phone from a BSUID-only user), we now:

  1. Backfill the phone on the contact and conversation in one atomic transaction — only rows with phone IS NULL are updated (idempotent).
  2. Emit whatsapp.contact.phone_received to your webhook subscribers, so you can sync the phone to your own CRM:
{
  "event": "whatsapp.contact.phone_received",
  "waba_id": "...",
  "contact": {
    "business_scoped_user_id": "US.xxx",
    "phone": { "previous": null, "current": "5492223677754" }
  }
}

Sending the interactive itself already works — the send API accepts the shape interactive: {type: "request_contact_info", body: {text}, action: {name: "request_contact_info"}} and forwards it to Meta as-is.

The new event is available in the webhook subscription picker under Contacts.


2026-08-06 — Set your WhatsApp business username (@handle)

New endpoint and portal UI to set a public WhatsApp @handle for your business. Once approved by Meta, users can find you by @yourhandle instead of the phone number. Requires the Meta usernames rollout (August 2026) to be active on your account.

API endpoint:

POST /v1/phone-numbers/{phoneNumberPublicId}/username
Content-Type: application/json

{
  "username": "myshop",
  "transferAction": "none"
}

Response on success: { "ok": true, "status": "approved" } (or "reserved" while Meta processes asynchronously). Format rules: 3-35 chars, letters/digits/period/underscore, at least one letter, no consecutive periods, no www prefix.

Errors from Meta are translated with a friendly message: 147001 (taken by another business), 147002 (account not eligible — insufficient messaging tier), 147005 (already in use on another of your phones — retry with "transferAction": "force_transfer").

Portal: added an inline “Business @handle” form in the phone number card (expanded panel). Owner/admin only.


2026-08-06 — BSUID rollout Fase 2c.2: contacts.phone is now nullable

Schema change: contacts.phone was NOT NULL — it is now nullable. With growing WhatsApp username adoption, contacts identified only by BSUID (no phone number) can now be stored.

The existing unique constraint (tenant_id, customer_id, phone) keeps working (MySQL treats NULL != NULL), and the (tenant_id, customer_id, bsuid) unique constraint from Fase 2c.1 ensures uniqueness of BSUID-only contacts.

Zero data loss — 0 rows had phone = NULL at migration time. The change is purely additive.

This closes the BSUID sprint started on 2026-08-05. All 5 phases are in production.


2026-08-06 — BSUID rollout Fase 5: message-level BSUID in outgoing webhooks

The webhook envelope now exposes BSUID at the message level (until today it was only at conversation level):

  • whatsapp.message.received — the message object now includes optional from_user_id and from_parent_user_id (Meta’s MetaIncomingMessage.from_user_id / from_parent_user_id).
  • whatsapp.message.sent / delivered / read / failed — the message object now includes optional recipient_user_id and recipient_parent_user_id.

Fields are omitted when Meta doesn’t send them. No breaking change — the additional keys are additive.

Why this matters: with message-level BSUID you can correlate individual messages with a stable identity across phone changes, without joining on the conversation. Useful when the same wamid needs to be tied to a per-user CRM record and the phone rotated between messages.


2026-08-06 — BSUID rollout Fase 2c.1: UNIQUE constraint on bsuid in conversations and contacts

Preventive schema change: the existing bsuid indexes on conversations(phone_number_id, contact_bsuid) and contacts(tenant_id, customer_id, bsuid) were promoted to UNIQUE. Before rolling this out we confirmed there were zero duplicate rows in production — the migration is purely additive.

Why this matters for you: as WhatsApp usernames adoption grows, more users will be identified only by BSUID (no phone). Without a unique constraint, two concurrent webhooks for the same BSUID-only user could race and create duplicate conversations or contacts. This closes that gap.

MySQL treats NULL != NULL in unique constraints, so contacts identified only by phone (bsuid = NULL) are unaffected. No application code changed.


2026-08-06 — Inbox row layout polish: @username sub-line, remove message count, hide phone/customer for single-phone tenants

Follow-up to the Fase 4 rollout of formatContactLabel. Three tweaks in the conversation list rows:

  • @username sub-line — when a contact has both a saved name and a WhatsApp username, both are visible now: the name on top and @username on a small muted line below. Previously the helper hid the @username if a name was set. Contacts identified by @username alone keep looking the same (single line label).
  • Phone / customer row hidden for single-phone tenants — the second line showing +54 9 XXXX · Customer Name was redundant for tenants operating a single phone number and used vertical space with no signal. It now shows only when the tenant has more than one phone.
  • Removed message count — the total message count per conversation (“N msgs · ”) was dropped from the footer. Only the last-activity timestamp remains.

The embed inbox got the same @username sub-line and count removal.


2026-08-06 — /app/logs shows the full raw Meta webhook envelope

Iteration on top of the previous entry. The message detail sheet in the logs stream now shows the full raw webhook envelope (object, entry[], changes[], value with metadata, contacts[], messages[]) instead of just the decrypted message object. This is what you actually want to inspect in a debug view: what Meta sent on the wire, in full.

If the raw envelope was purged by retention (typically 7-30 days), the sheet shows an explanatory field instead of the payload.


2026-08-06 — Debug UX polish: /app/logs payload decrypted, copy buttons work, envelope no longer overflows

Three UI fixes to the debug flow after using the new envelope viewer end-to-end:

  • /app/logs — payload now decrypted in the detail sheet. Meta message payloads are stored AES-256-GCM encrypted at rest. The logs drawer was showing the raw base64 blob instead of decrypting first. Now shows the same pretty-printed JSON as the messages drawer.
  • /app/logs — “Copy wamid” and “Copy path” buttons now work. They previously had no click handler. Now they copy to clipboard with a checkmark feedback. Retry buttons (never wired) were removed — use the messages drawer to retry a failed message.
  • /app/messages — envelope viewer no longer widens the drawer. The raw JSON viewer had missing overflow constraints; long lines would stretch the drawer past its max width. Now scrolls horizontally within its container.

2026-08-06 — Message detail drawer: view the raw Meta webhook envelope

The message detail drawer at /es/app/messages now includes a collapsible “Envelope Meta (raw)” section at the bottom. Shows the full Meta webhook envelope with contacts[], metadata and every field — data that isn’t in the message payload field (which only persists the message object).

Useful for debugging new Meta features (BSUID, username, contacts profile) that arrive in the envelope but aren’t stored in your message record.

Lazy-loaded on first click, so it doesn’t slow down the drawer. If the raw envelope was purged by retention (typically after 7-30 days), you’ll see a friendly “not available” message.


2026-08-06 — Portal shows WhatsApp usernames and “no phone” indicator

Complement to the BSUID rollout: the inbox and conversations views now show @username labels for contacts who adopted a WhatsApp username, and a discreet “no phone” chip when the contact is identified only by BSUID.

New — Label priority in the portal

Where the UI shows a contact’s name, the priority is now:

  1. Custom name you set (contactName override) — same as before, still wins
  2. WhatsApp @username — new
  3. Phone number — as before
  4. BSUID:xxx fallback — rare, only when no other identifier is available

Applies to inbox conversation list, headers, notifications, /app/conversations, and the embedded inbox.

New — “No phone” chip on the inbox header

When a conversation’s contact has no phone number available (typical for users who adopted a WhatsApp username and stopped sharing their phone), an amber chip “no phone” appears next to the contact label. Signals the operator that some flows (like phone-based lookup in external CRMs) may not apply.

Embedded inbox

The GET /v1/inbox-embed/conversations response now includes contactUsername and contactBsuid for each conversation. If you built a custom UI against this endpoint, you can now surface the same labels — no shape change is required, both fields are additive and nullable.


2026-08-05 — Send messages by BSUID (Meta identity rollout, part 2)

Complement to the BSUID capture support shipped earlier today. You can now target a WhatsApp contact by their BSUID even when you don’t have their phone number — useful for users who adopted a WhatsApp username and stopped sharing their phone.

New — recipient field on POST /v1/messages

The send API now accepts an optional recipient field with the contact’s BSUID:

POST /v1/messages
{
  "phone_number_id": "545152902020081",
  "recipient": "US.13491208655302741918",
  "type": "text",
  "text": { "body": "Hello" }
}

Rules:

  • Either to (phone) or recipient (BSUID) must be present. Sending neither returns 400.
  • Both are allowed. Per Meta’s rules, if you send both, phone takes precedence.
  • Format for recipient: US.xxx (portfolio BSUID) or US.ENT.xxx (enterprise). Country ISO code + up to 128 alphanumeric chars.

Failed webhook now includes BSUID

If a send fails and it was targeted by BSUID (no phone), the whatsapp.message.failed webhook now includes message.recipient_user_id in addition to (or instead of) message.recipient_id. Same shape as Meta’s own status webhooks.

Notes

  • Sending by BSUID only requires the contact already exists at Meta level. If you don’t know a valid BSUID for a contact, keep using to (phone) as before.
  • Media upload endpoints (POST /v1/messages/media, POST /v1/messages/upload-media) still require to — Meta’s media upload API doesn’t accept BSUID.

2026-08-05 — BSUID and username support (Meta 2026 identity rollout)

Meta introduced two new identifiers for WhatsApp users starting April 2026 (BSUID) and June 2026 (username). This release adds full support in our webhook payloads, plus a new event to track identity rotations.

New — BSUID + username fields in webhook payloads

Inbound whatsapp.message.received and status webhooks now include the following optional fields when Meta provides them:

  • conversation.business_scoped_user_id — the user’s BSUID scoped to your Business Portfolio. Format: country code + . + up to 128 alphanumeric characters (e.g., US.13491208655302741918).
  • conversation.parent_business_scoped_user_id — enterprise parent BSUID (format: US.ENT.xxx), only if your organization has parent BSUIDs enabled.
  • conversation.username — the user’s chosen @username on WhatsApp, if they’ve adopted the feature.

All three fields are nullable. Today they arrive populated on roughly 3% of Argentina messages and growing. When a user with a username has not shared their phone number in the last 30 days, the phone_number field may be omitted — always guard for its presence before using it.

New — whatsapp.contact.identity_changed event

Meta rotates a user’s BSUID when they change their phone number. Our new event notifies you so you can update your CRM with the correct identifier:

{
  "event": "whatsapp.contact.identity_changed",
  "delivery_id": "...",
  "occurred_at": "2026-08-05T...",
  "tenant_id": "...",
  "waba_id": "...",
  "contact": {
    "wa_id": "16505551234",
    "business_scoped_user_id": {
      "previous": "US.oldid",
      "current": "US.newid"
    },
    "parent_business_scoped_user_id": {
      "previous": "US.ENT.oldparentid",
      "current": "US.ENT.newparentid"
    }
  }
}

Subscribe to the new event in your webhook configuration to receive it. Idempotent on our side — retries of the same rotation are no-ops.

Coming soon

  • Send API will accept BSUID as the recipient field alongside phone number to (Meta already supports this since July 2026).
  • Portal UI will display @username labels when available.

2026-07-30 — Contact sync completeness + WABA lifecycle safety in multi-portfolio setups

Two-day sprint focused on the contact sync module (Phase 2 complete) and on making WABA delete/re-embed flows safe when multiple WABAs share the same Meta Business Portfolio.

New — Imported agenda visible in /app/contacts

When a customer chose contactSyncMode='all', the imported phone book was used only internally to enrich names in existing conversations — invisible in the UI. Now /app/contacts has a “Source” filter: With conversation (default) / Agenda only / All. Contacts imported from the customer’s phone book but without an active conversation appear as rows with an amber “Agenda only” badge.

Pagination is mixed — contacts (with conversation) come first, then agenda-only. A segmented counter shows the split: Showing 1–50 of 1615 · 15 with conversation + 1600 agenda only.

New — Sync mode badge on the phone card

The phone card in /app/phone-numbers now shows a chip with the active contact sync mode:

  • Sync: off (amber) — no name sync
  • Sync: conversations (muted) — names only for contacts who message you
  • Sync: full contact list (primary) — full phone book mirrored

Only visible on coexistence phones. Hover for the full explanation.

Reliability — Late history contacts now receive their name too

Race between smb_app_state_sync (agenda names) and history batches (contact creation) is now covered by a 24h Redis cache. If the agenda sync arrives before the history batch (typical), names are cached and applied when contacts get created later. Works in both conversations_only and all modes.

Empirically validated: conversations_only mode now recovers ~27% of history contact names (matching what all mode used to yield), without persisting the full phone book in DB.

Reliability — Deleting/disconnecting one WABA no longer breaks sibling tokens in the same Business Portfolio

When multiple WABAs in the same Meta Business Portfolio were authorized by the same Facebook user, deleting one used to revoke the OAuth grant globally, causing sibling WABAs to fail with code=190 Authentication Error. Confirmed empirically with a controlled test.

DELETE /me/permissions is now called only when the deleted WABA is the last one of the portfolio (“last one out turns off the lights”). While sibling WABAs remain active, the grant is preserved. As a defense-in-depth layer, sendMessage can fall back to a System User Token for a configurable set of business_ids (opt-in via META_SYSTEM_USER_BUSINESS_IDS env var).

Known limitation: to fully revoke the app grant when a sibling WABA remains, revoke manually from business.facebook.com → Settings → Apps and Websites.

Reliability — Honest history import banner

The “Importing history… X%” bar was misleading. Meta sends progress per phase (0=recent, 1=1-90d, 2=90-180d) and each phase ends at 100 even when more phases come — the previous logic closed the status when phase 0 hit 100, but Meta kept sending phase 1 and 2 for another 10-15 min.

The banner now shows just “Importing history…” with a spinner until Meta actually goes silent for >2 min (a new idle-detector cron runs every minute and closes the import based on silence). Rate limits from Meta app (code=4) and out-of-window errors (code=135000) no longer trigger retries that would only worsen the problem.

Improvement — Preventive warnings on delete/re-embed for SMB coexistence

Re-pairing an SMB coexistence phone that was already linked to gsacloud without first unlinking from the WhatsApp Business app on the phone generates an OAuth token with incomplete permissions — a limitation from Meta’s side. The workaround is documented in the delete modal and in the new phone flow: first unlink from Settings → Account → Business Platform in the WhatsApp Business app before re-pairing.

Hardening — Contacts always require a customer

contacts.customer_id is now NOT NULL at schema level (previously nullable). Delete of a customer cascades to its contacts (previously set-null). No behavior change for the current code path — this closes an old edge case where a legacy sync flow could create orphan contacts.

Improvement — Scope indicator on /app/contacts counter

When your tenant has multiple customers, /app/contacts now shows an active scope chip next to the title: “All customers (N)” or “[Customer name]”. The pagination counter also spells out the scope: of 3117 contacts (all customers) vs of 15 contacts in Consultorio A. Only visible on tenants with 2+ customers.

No API changes. Portal-only + one schema hardening migration (contacts.customer_id NOT NULL — orphan cleanup done automatically).

2026-07-13 — Inbox: status tabs with counters + prominent “Mark as resolved”

A UX refresh on the /app/inbox filter and the chat header action for closing conversations.

Status filter — dropdown → tabs with live counters

The status filter used to be a <select> inside a collapsible “Filters” panel that had to be expanded first, and it defaulted to “All”. It’s now 3 tabs always visible above the collapsible: Open (n) | Resolved (n) | All (n). Each tab shows a live counter for the current scope (respects customer / team / assigned / search / visibility filters).

The default landing tab is now Open — matching the natural workflow (agents want to see what needs attention first).

”Mark as resolved” button — icon-only → primary button

The chat header used to have a small <Archive> icon-only button, easy to miss. It’s now a primary green button with a check icon and the label “Mark as resolved” (visible on desktop, icon-only on mobile) — much more discoverable and aligned with common inbox conventions (Kapso, Front, Intercom).

Terminology alignment

The UI now says “Resolved / Mark as resolved” instead of “Archived / Archive conversation” — this matches the vocabulary of the multi-agent inbox space. Internally the schema stays as archived for backward compatibility (webhooks, API responses, external integrations unchanged).

Auto-reopen still works

If a contact writes again to a conversation you marked resolved, the webhook automatically reopens it — no manual step needed. This behavior has been in place since June 2026; the new tabs just make the state visible.

No API changes. No migration. Portal-only.

Inbox filter bar redesigned — progressive disclosure

The filter bar above the conversation list used to show three fixed dropdowns (Assignee, Team, Customer, Phone) inside a collapsible panel, taking ~108 vertical pixels even when nothing was filtered. Now:

  • “Mine only” toggle chip — always visible on the left, the fastest way to see just your conversations.
  • Active filters appear as chips — when a filter is applied (e.g. “Team: Sales”), a chip appears with the label + value + × to remove. Click the chip body to reopen the menu and change the value in one step.
  • ”+ Filter” button on the right — opens a menu with only the filters you haven’t applied yet (already-applied ones are not listed).

Default state (nothing filtered) shows just the “Mine only” toggle + ”+ Filter” button — ~28 pixels tall. That’s ~80px reclaimed for the conversation list on every load.

No API changes. Portal-only.

2026-07-12 — Retry V2: bulk button, retry chain, and lifted guards

Failed message retry is now materially more useful. Three changes ship together.

New — Bulk “Retry all failed” in the portal

The /app/messages list gets a “Retry failed (n)” button that requests a retry on every failed / failed_transient message in the current scope (respects tenant/customer/date filters), capped at 500 per batch. Confirm-inline (two clicks) — no dialog. Under the hood it does a single UPDATE messages SET retry_requested_at=NOW() and the background processor picks them up on the next cron tick.

Useful when a WABA outage or a template rejection causes a spike of failures and you don’t want to click “Retry” 200 times.

New — Retry chain visualized in the message drawer

Every message now tracks its parent retry. The drawer at /app/messages/:id shows:

  • If the message is a retry: a “Original message” link back to the parent (with status + timestamp).
  • If the message has retries: a list of children with badges (delivered / failed / pending) and creation time.

Makes it explicit which sends belong to the same attempt lineage — useful when reconciling with the customer’s downstream system.

Change — Retry no longer blocked when the original reached Meta

Until now, the retry endpoint (POST /v1/messages/:id/retry) rejected requests when the failed message had already been accepted by Meta. That excluded the most common retryable case: error 131047 (accepted by Meta, then delivered as failed via webhook because the 24h customer service window closed). You can now retry those messages — send a template first to reopen the window, then retry.

Guards that remain:

  • Server-side rate guard on the retry endpoint prevents accidental double-clicks within 5 minutes of the previous request (returns 429 too_many_requests).
  • Duplicate retry submissions for the same original message are deduplicated safely — you can’t accidentally spawn parallel retries.

API impact: additive. New retry_of_message_id field in GET /v1/messages/:id and GET /v1/messages responses (null for non-retry messages). No existing field changed. The behavior change on retry acceptance is more permissive, not more restrictive — existing successful retry calls keep working.

2026-07-07 — Kill switch per customer + friendlier 131047 in the portal

New — Kill switch per customer (403 CUSTOMER_SENDING_DISABLED)

Owner or admin of a tenant can now pause all outbound sending for a specific customer from /app/customers/{id} → Overview tab → “Pausar envíos” (with a required reason). While paused:

  • The API returns 403 with error code customer_sending_disabled on any send attempt tied to that customer, before touching Meta. Send stays in your queue if you’re using outbox pattern; no billable message hits WhatsApp.
  • The portal composer shows the same pause message inline instead of sending.
  • Reversible from the same banner. The reason and timestamps are stored on the customer for audit.

Useful when you need to stop a customer’s flow while investigating something on their side — you cut in seconds instead of rotating credentials.

API impact: additive. New error code customer_sending_disabled on send endpoints. No existing responses changed.

Fix — Friendly banner for error 131047 in the portal inbox

When Meta rejected a free-form message with error 131047 (outside 24h window), the portal inbox composer used to render the raw Meta error text — confusing for non-technical agents.

Now: when the API returns code: "131047" on a send, the composer shows an amber banner that reads “Fuera de ventana 24h — mandale template aprobado” with a “Send template” button that opens the template picker directly on the current conversation. Any other error still falls back to the generic red banner with a “Retry” button.

API impact: additive. POST /v1/messages (and equivalents) now include code alongside error_message in 400/4xx responses when Meta or our validator returned a specific code. Integrators using their own UI can pattern-match on code === "131047" for the same UX.

2026-07-05 — Named parameters in templates, hidden phones, embed cleanup

Three portal + API additions shipped together.

New — Named parameters in templates (parameter_format=NAMED)

Meta’s WhatsApp Templates API supports two variable formats: positional ({{1}}, {{2}}) and named ({{customer_name}}, {{order_id}}). Until now, gsacloud always emitted POSITIONAL. This release adds full support for NAMED end-to-end.

  • Create: /app/templates/new has a “Named parameters” toggle. When ON, the validator accepts {{lowercase_snake_case}} variables and rejects mixing with {{1}}. The POST /v1/wabas/:id/templates request adds parameter_format: "NAMED" and payload fields body_text_named_params: [{ param_name, example }] (and equivalent for header).
  • Sync: when we pull templates from Meta (either yours or a portfolio you have access to), we read parameter_format from Meta’s response and persist it — templates created directly in Meta’s UI with named params now sync correctly.
  • Send: POST /v1/messages template payloads accept body_named_params: { customer_name: "Andrea", order_id: "1234" } when the referenced template is NAMED. Each parameter emits parameter_name alongside value per Meta’s spec.
  • Inbox composer: the input labels in the template picker now show {{customer_name}} instead of {{1}} when the template is NAMED — much friendlier for the agent filling in variables.

API impact: additive. Template responses gain parameter_format field ("POSITIONAL" for pre-existing templates, "NAMED" for new ones). Send payloads accept the new body_named_params / header_named_param shapes only when the template is NAMED. Positional templates are unchanged — no migration needed on your end.

New — Hidden phones toggle in the inbox

Each phone card at /app/phone-numbers has a new toggle “Visible in Inbox” (default: on). When OFF for a given phone number:

  • Its conversations don’t appear in the inbox list at /app/inbox.
  • It’s not selectable in the phone filter dropdown.
  • The “New conversation” selector hides it.

Data is preserved — you can toggle back on any time and everything reappears. Useful for:

  • Older WABAs that a customer stopped using but you keep for historical retrieval.
  • Test numbers inside a productive project that shouldn’t clutter the agent view.

API impact: none for existing endpoints. GET /v1/phone-numbers/:id gains a hidden_from_inbox boolean field.

New — Inbox Embeds cleanup: filter active/all + “Purge revoked”

The /app/inbox-embeds list used to show revoked and active embeds mixed together with no quick way to clean up. Now:

  • Default view filters revoked_at IS NULL (only active). URL flag ?show=all restores the full list.
  • Header chips: “Active (N)” / “All (M)” for quick toggle.
  • New button “Purge revoked (X)” — deletes all revoked embeds in the current tenant (deleteMany, confirm inline). Audit event inbox_embed.purge_revoked.
  • Empty state text adapts to whether the filter is on and whether there are revoked embeds to purge.

API impact: none. Portal-only.

2026-07-03 — Migration safety net in CI + webhook consistency fix

Continuing yesterday’s operational hardening theme (rollback runbook + healthchecks). A small but meaningful addition to how we ship database migrations, plus a fix that makes the whatsapp.message.failed webhook payload consistent with what’s stored in the database.

Docs — Retry a specific failed message + Idempotency deep dive

Two new sections in the docs to reduce integrator questions:

  • error-handling: new “Retry a specific failed message” section documents the POST /v1/messages/:id/retry endpoint (already available) with concrete use cases: failed_transient from 5xx (retry succeeds after a few minutes), 132001 template not found (fix the template in Meta, then retry), 131047 outside 24h window (send a template first to re-open, then retry).
  • sending-messages: extended “Idempotency” section with a deep dive on what’s cached vs not (2xx cached 24h, 4xx/5xx cleared so retries execute cleanly), race behavior (409 request_in_progress on in-flight collision), the X-Idempotent-Replay: true header, and how to debug suspected stale cache hits.

Refinement — Webhook status badge in the list now reflects auto-disable

The webhook list at /app/webhooks was showing an ACTIVE badge (green) for webhooks that had been auto-disabled by the health check, whenever any edit to the webhook happened after the auto-disable (since standard updates don’t clear the auto_disabled_at flag). The badge now uses the priority auto_disabled (red) > disabled (grey) > active (green), matching what the Health dialog already showed.

New — Webhook Health V1: know when your webhook stops working

Until today, if your webhook endpoint went down, was misconfigured, or got deleted, you had no way to know except by noticing missed events. This release closes that gap end-to-end.

  • Health dashboard in the portal. Open any webhook’s row action menu → “Health” to see a dialog with success rate for the last 24h and 7d, the last 20 delivery attempts (with status, response code, latency, retry count), and a “Send test event” button that dispatches a webhook.test payload and shows the response inline.
  • Auto-disable to protect downstream systems. Webhooks that consistently fail over an extended period are auto-disabled. When this happens the account owner receives an email with instructions to fix the endpoint and reactivate. Prevents your queue from filling with retries against a dead endpoint.
  • Owner email alerts. Three new event types on the notification channel: webhook.disabled_auto (critical), webhook.degraded (10-50% success in 24h, warning), webhook.reactivated (info). Configurable per-tenant at /app/settings/notifications.
  • New API endpoint GET /v1/webhooks/health-by-phone?phone_number_id=X. Query by Meta phone number ID (string). Returns { status, successRate24h, deliveries24hTotal, lastDeliveryAt, ... } where status is one of healthy | degraded | failing | no_deliveries | disabled | auto_disabled | not_configured. Use this from your own dashboard/app to display webhook health next to your WhatsApp integration section.
  • Reactivation endpoint POST /v1/webhooks/:id/reactivate. Clears the auto-disabled state and re-enables delivery. Only applicable to webhooks that were auto-disabled (manual disable via PATCH enabled=false is unaffected).

Important — no event replay after auto-disable. Events that occur while a webhook is disabled are not queued for replay when it’s reactivated. Reconcile via GET /v1/messages or your own state store. A future release may add a short retention buffer.

API impact: additive. POST /v1/webhooks and GET /v1/webhooks/:id responses include the auto-disable state (nullable). No existing fields changed. Three new event types on webhook subscription — additive.

New — Healthcare recipes in the docs

A new guide at /guides/healthcare-recipes with four end-to-end patterns tailored to medical practices and clinics: appointment reminders with confirm/reschedule buttons, lab results delivery as PDF (upload-once vs send-by-URL), post-visit satisfaction surveys with 5-star rating, and cancellation waitlist broadcasts with race handling. Each recipe includes the exact template JSON to submit to Meta, the send request, and the webhook shape you’ll receive back.

Fix — whatsapp.message.failed and conversation.created events now delivered to phone-filtered webhooks

If you configured a webhook with a specific phone_number_id filter (i.e. “only send me events for this WhatsApp number”), you were not receiving whatsapp.message.failed or conversation.created events originated by your own sends against that number. The internal event router was matching only webhooks without a phone filter for those two event types. Inbound events (whatsapp.message.received, delivery statuses) were unaffected because their code path already passed the phone filter correctly.

Fixed in three internal call sites. No API changes; integrations without phone_number_id filters (phone_number_id: null) were never affected.

Fix — errorTitle populated in whatsapp.message.failed events

When our API called Meta and Meta responded with a 4xx that didn’t include an error title (Meta ships type: "OAuthException" in those cases), the failed message record in our database was correctly falling back to the error type, but the outgoing webhook event whatsapp.message.failed was still emitting errorTitle: null. Integrations reading this field for error classification saw null while the same message showed a real value in GET /v1/messages. Both paths now use the same fallback (title → type → null).

New — GitLab CI blocks bad migrations before deploy

Every merge request or push that touches Prisma migrations or schema now triggers a CI job that spins up an empty MySQL 8 container and applies all migrations from scratch. If anything fails — SQL syntax error, invalid foreign key, duplicate index, schema drift — the pipeline goes red and the change can’t merge. Previously we discovered these failures only when EasyPanel ran the migration against real prod, which meant emergency rollbacks. Now we catch them ~5 minutes earlier, in a disposable container, before any prod database is touched.

API impact: none. Change is internal to our deploy process — integrations see no difference other than fewer prod incidents caused by migration issues.

2026-07-02 — WABA protection guards + inbox refinements

Focus of the day: protect customers against accidental WhatsApp Business Portfolio transfers during Embedded Signup, align our conversation-session model with Meta as the source of truth, and polish a handful of inbox details.

New — WABA lifecycle protection

Adding an existing WABA via Embedded Signup requires the customer to pick a portfolio in Meta’s account picker. If they pick the wrong one, Meta silently moves the WABA to that portfolio — a serious operational hazard. This release adds three layers of defense:

  • Portfolio pre-selection. When reconnecting an existing WABA, the expected portfolio is pre-filled in Meta’s picker. Customers can still change it (Meta requires this), but the default is right.
  • Advisory modal before reconnect. Clicking “Reconnect” from the WABA card now opens a warning modal with the expected portfolio ID and a clear explanation of what happens if a different one is selected.
  • Owner notifications on mismatch. If a mismatch does happen, we detect it, persist the new state so service isn’t interrupted, and email the account owner with context and next steps. Two new events on the health-alert channel: whatsapp.waba.portfolio_transfer_blocked (critical) and whatsapp.waba.target_mismatch (warning). Toggle per-tenant at /app/settings/notifications.

New — Meta-aligned Customer Service Window

The 24-hour CSW that ties messages to sessions now uses conversation.expiration_timestamp from Meta’s sent webhook as the source of truth, instead of computing openedAt + 24h locally. Closes minor drift that could affect session-based billing when Meta extends the window server-side (e.g. after a template send).

Inbox refinements

  • Contact display names now reflect edits from /app/contacts. Renaming a contact in Contacts updates the inbox header immediately (previously the inbox kept an early snapshot of the name from the first inbound).
  • Cleaner phone-number status badges. The old SANDBOX badge shown on any non-CONNECTED WABA was misleading (the Status field just below already carried that info). Now the badge only shows PRODUCTION when the WABA is connected; otherwise no badge.
  • /signin and /signup redirect to the app when already logged in. Opening either URL in a new tab now routes straight to the app if a session exists in the same browser.

Operational hardening

  • Reversible database migrations. Every schema change ships with a paired rollback path so we can revert safely if a deploy misbehaves.
  • Container healthchecks + warmup. Both API and portal images now report health to the hosting layer, which auto-restarts unhealthy instances. Post-deploy warmup eliminates cold-start database errors on the first requests after a deploy.

Delivery reliability

  • Handle Meta’s dual-webhook edge case. Meta occasionally sends two webhooks for the same message ID — first as type: "unsupported" (a fast preview), then with the real type (text, image, etc.). We now upgrade the record in place and emit the outgoing whatsapp.message.received event with the correct content. Integrations that already consumed the preview will still see the corrected message via subsequent events.

API impact: no breaking changes. Two new event types under whatsapp.waba.* — additive, subscribed via the webhook configuration UI.

2026-07-01 — Portal UX fix: wizard step numbering

  • Fix: /phone-numbers/new wizard step numbering. The connection-type wizard had duplicated i18n keys causing step 2 to display as “Step 3”, skipping “Step 2”. Now steps display correctly as 1 → 2.

2026-06-30 — Send reliability: idempotency + retry button + audit

Major robustness upgrade for POST /v1/messages. Send operations now have exactly-once guarantees when clients supply an idempotency key, and every failure gets persisted for audit + a new event so integrators can react.

  • Idempotency-Key header. Optional Idempotency-Key header on POST /v1/messages (format [a-zA-Z0-9_-]{8,255}). Cached responses for 24h. Retries with the same key return the cached response + header X-Idempotent-Replay: true, without calling Meta twice. Recommended: use a UUID/ULID per logical message.
  • Persisted failures. Meta 4xx errors (invalid template, missing parameter, etc.) now create a Message with status: "failed" and the full error detail. Before, the send would throw HTTP 4xx but not leave a record. Same for 5xx/timeout (status: "failed_transient"). Query these via GET /v1/messages?status=failed.
  • New whatsapp.message.failed event. Emitted from your outgoing webhook when a send fails (server-side or from Meta status webhook). Includes gosendapi.source ("send_error" when it comes from our send, null from Meta’s async report), gosendapi.transient (true for 5xx/timeout), plus the error code/message.
  • Race timeout reconciliation. If your client times out but Meta actually processed the send, we reconcile the message once the status webhook arrives. No more orphaned messages.
  • New status codes: 504 UPSTREAM_TIMEOUT when Meta takes >8s to respond (previously would hang until Node’s default 5-minute timeout); 409 request_in_progress when the same idempotency key is still processing.
  • New endpoint: retry a failed message. POST /v1/messages/:id/retry creates a new message with the same payload. Original stays intact for audit. Only works for messages in failed or failed_transient.
  • “Retry” button in message details. In the portal, for any message in failed or failed_transient, the detail drawer shows a retry action. Processed asynchronously (under 30s), a new message appears in the list once done. Original stays untouched.

API impact: all changes are additive; existing integrations that don’t send Idempotency-Key continue to work exactly as before. New event whatsapp.message.failed is opt-in via webhook subscription.

2026-06-24 — Teams + visibility scoping

Two complementary features to support multi-area teams (think accounting firms with Tax / Payroll / Audit, multi-area support).

  • Teams per customer. Group your agents by area or specialty. Each team has a name, optional description, color (for badges) and a list of members. Each member has a role: lead (auto-assigned by team dispatch), agent (receives assignments) or trainee (same as agent for V1). Configure at /app/customers/[id]?tab=teams (owner/admin only).
  • Conversation classification by team. From the inbox header, the assignment dropdown now has a “Team” section: pick a team to classify the conversation. If the conversation was unassigned, the team’s lead with the lightest load is auto-assigned.
  • New inbox sidebar filter: Team. Filter by All teams, No team (unclassified) or a specific team. Each conversation shows a small colored badge with the team name, so you can tell at a glance who handles what.
  • Visibility scoping (per-customer, configurable). New section under Assignment settings with three modes:
    • Open (default) — agents see all conversations of this customer.
    • Scoped — agents only see their assigned conversations, their teams’ conversations, and the unassigned pool. Don’t see other agents’ conversations outside their team.
    • Strict — agents only see conversations directly assigned to them. No pool, no team visibility.
    • Owner / admin / captain (in captain mode) always see everything regardless of mode.
  • API impact: Conversation responses gain an optional team_id. The assignment_source field can now take a new value indicating team-based dispatch. Existing integrations that ignore these are unaffected.
  • V1 limitation: visibility scoping applies only when the inbox is filtered to a single customer (the per-customer policy needs to be known). Agents browsing “all customers” fall back to the open behavior. Coming in V2: per-tenant default + multi-customer aggregation.

2026-06-23 — In-app alerts: sound + toast + title count

Three complementary alerts so agents notice activity even when the inbox tab is in the background and browser notifications aren’t enabled.

  • Sound on new message or assignment. A short ding (250ms) plays via the Web Audio API — no MP3 download needed. Toggle on/off with the speaker icon in the inbox header (preference persisted per browser, default on).
  • In-app toast. A small card slides in from the bottom-right when a new inbound arrives or a conversation is assigned to you. Auto-dismisses in 4 seconds, click to jump to the conversation, X to dismiss manually.
  • Tab title count. The browser tab title shows (N) Inbox · GoSendAPI when you have unread conversations — same pattern as Gmail, Slack, WhatsApp Web.
  • Self-action suppression preserved. None of these fire when you yourself take, assign, or release a conversation. Same 10s suppression window as the browser notification.

2026-06-23 — Unread indicator: see new activity across all conversations

  • New activity indicator in the inbox sidebar. Conversations with messages since you last viewed them are now shown with bold text + a small blue dot — same pattern as WhatsApp Web, Slack, Gmail. Works for every conversation in your scope, not just the ones assigned to you.
  • Why it matters. If a captain hands off a conversation verbally (“I’m passing this to María”) without formally reassigning, María now sees the activity in her inbox without having to check Others filter manually. Also catches cases where customers reply while you’re offline or another agent chimes in on your conversation.
  • How it works. Each agent has a private last_viewed_at per conversation. When you open a conversation, it’s marked as viewed immediately (with a 5-second poll refresh while you stay on it, so an incoming message doesn’t make your own current conversation appear unread). When the sidebar refreshes, conversations with last_activity_at > last_viewed_at are highlighted.
  • First-time login note. If you’ve never opened the inbox before, all existing conversations with any activity will appear as unread. Open them or wait for an upcoming “Mark all as read” action.

2026-06-23 — Auto-archive on 24h close + idle assignee release

Two opt-in safety nets to address the “release is manual only” limitation noted in the previous entry.

  • Auto-archive on 24h close. When the 24-hour customer service window expires without any new inbound, you can now have the conversation archived automatically (instead of just closing the window). Configurable per customer at /app/customers/[id]?tab=assignment (default off). Applies your continuity setting — if continuity is set to Case-based, the agent is also released.
  • Auto-release idle assignees. Optional per-customer setting (default off). If a conversation has been assigned to the same agent without any activity for N days (you choose between 1 and 365), the system automatically clears the assignee so the conversation goes back to the pool. Does not archive the conversation — only releases the agent. Useful when agents go on vacation and forget to archive.
  • Schema impact: none. Both toggles live inside assignment_policies.config (JSON). No migration required.
  • The audit log records both automations distinctly so you can tell an auto-archive from an auto-release.
  • Defaults are off — existing customers see no change in behavior until they opt in from Settings.

2026-06-23 — Conversation Assignments + sticky/case continuity toggle

  • Assign conversations to agents. Each conversation can now carry an assignee, with timestamps and provenance (who assigned it and why). If the assigned user is later removed from the project, the conversation is safely released — your data is never orphaned.
  • Two assignment policies per customer, configurable at /app/customers/[id]?tab=assignment (owner/admin only):
    • Open pool (default) — conversations stay unassigned until an agent takes them. The first agent to reply is automatically marked as the assignee (sticky first-touch). Best for small horizontal teams.
    • Captain dispatch — new inbound conversations are automatically routed to the captain with the fewest open conversations (ties broken by the order you set). Captains can reassign to other agents from the inbox.
  • Conversation continuity toggle controls what happens when an agent archives a conversation:
    • Continuous (sticky) — keeps the assignee. If the contact writes again, the conversation reopens with the same agent. Default for open_pool.
    • Case-based (fresh) — clears the assignee on archive. The next message is re-routed by your policy. Default for captain. Best for teams organized by area or specialty (e.g. an accounting firm with separate Tax, Payroll and Audit teams).
  • New inbox UI:
    • Assignment filter alongside the status filter: Mine, Unassigned, Others, All.
    • Header dropdown on the open conversation shows the assignee’s avatar + name (or Unassigned). From it you can Take, Release, or Assign to… (the last requires owner/admin).
    • New Archive button between the assignment dropdown and Delete. Applies the customer’s continuity policy automatically.
    • Browser notifications fire when a conversation is assigned to you by someone else or by captain dispatch. Self-actions (Take/Assign-to-me) are suppressed.
  • Webhook impact: every inbound messages event now triggers captain dispatch if the conversation is unassigned and the customer is configured for captain mode. Fail-soft: an assignment failure never blocks the inbound forwarding.
  • REST API: POST /v1/conversations/:id/archive now respects the customer’s releaseOnArchive config. Existing integrators see no change in payload shape — only the side effect on assignment.
  • Permissions: Take and Archive are open to any user with scope to the conversation. Release is restricted to the current assignee or owner/admin. Assigning to another user requires owner/admin. Changing the policy requires owner/admin.
  • Known limitation: assignment release is manual only — there’s no automatic release based on inactivity, idle agents, or CSW closure. If an agent doesn’t archive, the conversation stays assigned to them. Auto-release based on time/CSW is on the roadmap.

2026-06-22 — Inbox UX: contact notes + browser notifications + delivery icons

  • Inline contact notes in the inbox right-side panel. Add private notes about a contact while you’re in the conversation — saves automatically as you type (1.5s debounce). Same notes show on /app/contacts/[id]. Available on both the main portal inbox and the embedded inbox.
  • Browser notifications for new messages. Opt-in per browser (a banner appears the first time you open the inbox). When you have it on, you get a desktop notification for each new inbound message in a conversation you don’t already have open. Clicking the notification jumps to that conversation.
  • WhatsApp-style delivery icons on every outbound bubble: ✓ (sent), ✓✓ (delivered), ✓✓ in blue (read), ⚠ in red with a tooltip explaining the error (failed), 🕒 (pending). The error tooltip shows errorTitle, errorMessage and errorCode — no need to open the message detail to diagnose.
  • Localization (es): the inbox and the landing page now render fully in Spanish. Previously some labels (status filter, contact info panel, CSW tooltips) were still English on the /es locale.
  • /app/messages custom date range: replaced the inline From/To inputs with a popover that shows two months side-by-side, click-to-select start and end (Stripe-style). Preserves your other filters.

2026-06-21 — Account entity + per-phone billing + team management

  • New Account entity introduced as the economic owner of one or more Projects. An Account holds billing identity (plan tier, subscription status, tax and billing metadata) — fields are nullable until payment processing is wired. One Account currently maps 1:1 to its owner user; co-owners with shared billing access are tracked in our roadmap.
  • Plan and quota moved from Project to phone number. Aligned with how Meta charges (pricing per message goes to a specific phone_number_id, not to the WhatsApp Business Account). Each phone number now owns its plan, quotaOverride and quotaPeriodStart. Existing tenants were migrated automatically — every phone inherited the plan of the project it belonged to.
  • Project no longer carries billing fields. It is purely organizational from now on. Multiple phones under the same project may carry different plans in the future; the current portal UI shows the plan of the first phone as the project-wide representative until a richer per-phone UI lands.
  • Team management: remove members + leave project. Owners and admins can now remove accepted members from /app/settings/team (with confirm dialog; CustomerAgent assignments to the project’s customers are cleaned in the same transaction). Any non-creator member can leave a project on their own (requires typing the project name to confirm). The project creator remains immutable — see entry below.

2026-06-20 — Project creator immutability + contacts portal UI

  • Primary owner is now immutable. Each project records a creatorUserId set on creation that cannot be reassigned, removed from the team, or demoted from owner. Visible as a “Project creator” badge in /app/settings/team. Deleting your account is blocked if you are the creator of a project that still has other members — transfer or delete the project first.
  • Contacts portal UI (V1). New /app/contacts lists CRM-style contacts (one per tenant/customer/phone triple), with search, customer/WABA/tag filters and case-insensitive tag autocomplete. /app/contacts/[id] shows Profile / Conversations / WABAs tabs; manual display-name edits flag nameOverridden=true so subsequent automatic syncs (smb_app_state_sync) don’t overwrite them. Also added as a tab inside each customer.
  • Inbox ?conv=<id> auto-selects a conversation on load (with a fallback fetch if it’s not in the first page of conversations). “View full contact” link in the inbox contact panel.
  • Pagination fixes: per-page selection now persists across Next/Prev navigation (previously it would reset to default). First / Last buttons and a “Go to: N” jump input added to messages, conversations and all three logs tabs.
  • Embed inbox infinite scroll restored — long conversation lists in embedded iframes load progressively when scrolled (parity with the main portal). Date formatting now respects the user’s locale across the portal.

2026-06-19 — UX polish

  • Sidebar collapsible on desktop (persisted in localStorage). Tooltip-on-hover when collapsed.
  • Contact info panel in inbox (portal + embed): avatar, badges (CSW status, coexistence), customer, WhatsApp number, message counts (with attribution to humans vs API), dates.
  • Template preview live: in the inbox template picker and in /app/templates Send Test, variables are substituted in real-time as you fill the inputs.
  • ?customer= query param respected in /app/inbox (used by the embed “Open in portal” fallback).

2026-06-18 — Customer-scoped agents & multi-agent embed

  • CustomerAgent model: assign users as operational agents of a specific Customer within a Tenant. One agent can cover several Customers; admins can self-assign for testing. Each outbound message persists sentByUserId and shows the agent’s avatar on the bubble for traceability.
  • Magic link invite: dedicated flow at /{locale}/invite/{token} accepts users without prior account (bypasses signup waitlist when there’s a pending invitation). EN + ES.
  • PATCH /v1/inbox-embeds/:id to edit name, allowed_origins, theme, require_agent_login without recreating the embed.
  • require_agent_login flag (only scope=customer): the iframe demands a portal session + CustomerAgent membership. Without auth → “Open in portal” fallback that opens the main portal scoped to the customer.
  • Sidebar reduced for scoped agents: only Inbox + Account visible; admin sections blocked server-side. ProjectSwitcher shows Agent: N customers label.

2026-06-16 — Conversation model: contact-perpetual + CSW/FEP sessions

  • Conversation is now perpetual per (phone number, contact) pair instead of session-based. Aligned with industry BSPs.
  • Customer Service Window (24h) and Free Entry Point (72h via Click-to-WhatsApp) are tracked as separate sessions. Each message references the session it belongs to (or none, for templates sent outside any open window). Enables accurate session-based billing and reporting.
  • Migration to the new model runs during deploy — reach out if you have external integrations that read raw conversation state.

2026-06-14 — Billing pricing tracking aligned with PMP (post-Jul/2025)

  • pricing_type captured on each message alongside the legacy billable flag to track PMP tier categories.
  • Recipient country derived from E.164 with longest-prefix match across 200+ countries.
  • FEP detection via webhook v24+ conversation origin and expiration signals.
  • Pricing tier history recorded — automatic tier changes Meta applies based on volume are persisted for reporting.
  • Webhook idempotency by wamid + status to handle Meta’s occasional duplicate deliveries.

2026-06-12 — Templates: HEADER/FOOTER/BUTTONS render + send

  • Full Meta template support when sending from the inbox: HEADER (text with variables, image/video/document with media handle), FOOTER, BUTTON URL with dynamic {{1}} param, BUTTON COPY_CODE. UI dynamically renders the right inputs per template.
  • POST /v1/templates/upload-media helper to upload media for HEADER (returns Meta media handle valid for 30 days).

2026-06-08 — Templates: robust sync + emoji support

  • Sync response now reports per-template details (added/updated/skipped/error + reason).
  • UPDATE refreshes category + language on every sync (previously only body/status).
  • Detects templates removed in Meta and soft-deletes them locally.
  • WhatsApp content columns migrated to utf8mb4 to support emojis (templates with 👋 📅 etc. no longer break INSERTs with MySQL error 1366).

2026-06-05 — Cost estimation dashboard /app/billing

  • New page: app.gosendapi.com/<locale>/app/billing — split into two clearly separated sections:
    • ⚡ Server activity (badge “Not billed”): total events processed by gosendapi-cloud in the period (inbound + outbound + status updates), with breakdown by WABA showing customer + inbound/outbound counts per WABA. This is infrastructure load, not what Meta charges.
    • 💰 Meta cost estimation: KPIs for billable / free / estimated cost, daily volume chart, breakdown by pricing category (marketing/utility/authentication/service), top phone numbers and top customers by billable volume, configurable per-category rates (USD/ARS/MXN/BRL/EUR), CSV export.
  • Date range: configurable from/to picker with quick links “This month” / “Last month”. MoM (month-over-month) trend indicator on billable KPI.
  • Disclaimer prominent: this is an estimation derived from the pricing object Meta sends in status webhooks (delivered/read). Final billing is what Meta charges in WhatsApp Manager — this is for forecasting and per-customer cost allocation.
  • Why this matters: integrators can now project monthly Meta spend mid-period, detect spikes in marketing volume, and allocate cost across the customers they serve, without ever leaving the portal.

2026-06-05 — Failure reason persisted on failed messages

  • New columns: messages.error_code (INT), error_title, error_message, error_details capture data from Meta failed status webhook events that was previously discarded.
  • API: GET /v1/messages/:id now includes error fields when status='failed'.
  • Portal: message detail drawer (/app/messages → eye icon) shows a red “Failed” card with the Meta error code, a localized explanation, and a “Suggested fix” tip. 15 common error codes mapped to actionable hints in EN + ES (131026, 131030, 131047, 131049, 131051..56, 132000..132016).
  • Meta Logs tab: when a message has an error, it now displays inline as <code>: <title> below the status badge.
  • Effect on integrators: no more “failed” with zero context — the operator sees code + title + suggestion in one place, and can act on it without leaving the portal.

2026-06-05 — Health check modal — full WABA configuration check

  • New: Health check button on each phone number card opens a comprehensive modal that re-fetches WABA + phone state from Meta in real time and updates the DB.
  • Sections:
    • Overall status (Healthy / Warning / Issues) with derived state.
    • Access token validation + granted scopes.
    • Phone number access (verified name, display number, quality rating with color-coded indicator).
    • Webhook verification + subscription (App ID and subscribed fields).
    • Messaging health entities from Meta (/{phone}/health_status): each entity (PHONE_NUMBER, WABA, BUSINESS_PORTFOLIO, APPLICATION) shows can_send_message status (AVAILABLE / LIMITED / BLOCKED) with errors and possible solutions.
    • Send test message: ships the hello_world template (pre-approved by Meta) to any number to validate the full pipeline.
  • Severity classification: LIMITED errors are automatically tagged as Cosmetic (doesn’t affect messaging, e.g. SIP/voice calling), Limits volume (caps tier — business verification, display name approval), or Blocks messaging (must be resolved before sending). Tooltips explain each badge.
  • Side effect: persisting the fresh state in DB updates the phone card with the latest quality rating, throughput, name status, and connection type (coexistence vs dedicated). No more frozen unknown quality ratings on long-lived phones.

2026-06-05 — Meta Logs tab + portal-wide timezone fix

  • New tab in /app/logs: Meta Logs alongside Webhook Logs and API Logs. Shows outbound + inbound message events with: direction arrow, message type icon, contact name/phone, status, billing classification (billable / free with category), WhatsApp Config, resource ID (wamid), and time.
  • Filters: All / Inbound / Outbound · All / Sent / Delivered / Read / Failed · All billing / Billable / Free · phone search.
  • Timezone fix: all timestamps across /app/logs, audit log, and sessions now render in the browser’s local timezone via a <LocalTime> client component. Previously they were rendered server-side in UTC (the container’s timezone), confusing operators in non-UTC regions.
  • Effect on integrators: a single Meta events feed with billing context inline, plus accurate local times everywhere in the portal.

2026-06-05 — Phone health badges + improved detail panel

  • Inbox header: when viewing a conversation in /app/inbox, the phone number’s quality rating (GREEN/YELLOW/RED/unknown), Coexistence mode (Coex badge), and Official Business Account (OBA badge) are shown inline with tooltips.
  • Phone numbers detail panel: each row now includes copy-to-clipboard buttons next to Phone Number ID / WABA ID / Business ID, a wa.me/<phone> quick-link to test directly in WhatsApp, the Business Account Status with color-coded badge, and webhook subscription count with a Manage Webhooks → link to the filtered webhook list.
  • Connection type bug fixed: phones were showing as dedicated even when actually coexistence. The display now derives from isCoexistence (Meta’s source of truth), and new phones save the correct value at creation by including is_on_biz_app in the Meta phone fields query.

2026-06-05 — Send media + send template from portal inbox

  • New: composer in /app/inbox now has an attachment button (paperclip) for images and documents, and a template button (file icon) for sending pre-approved templates outside the 24-hour service window.
  • Image upload: JPEG, PNG, WEBP up to 25 MB. Uploaded to Meta /media, then sent with caption.
  • Document upload: PDF, DOC, DOCX, XLS, XLSX, TXT, CSV, ZIP up to 25 MB. Sent with filename + caption.
  • Template picker: modal lists APPROVED templates of the conversation’s WABA with body preview, variable inputs ordered {{1}} {{2}} …, and validation on count match before sending. Sends via Meta Graph API directly using the encrypted access token (no extra API call from your code).
  • Effect on integrators: the agent can fully respond to inbound conversations from the portal — text, image, document, and templates — without an external WhatsApp client.

2026-06-05 — Pricing fields persisted from Meta status webhooks

  • New: messages.pricing_billable, pricing_category, pricing_model, conversation_origin columns capture data from Meta’s status webhook payload that was previously discarded
  • API: GET /v1/messages and GET /v1/messages/:id now return:
    {
      "pricing": { "billable": true, "category": "marketing", "pricing_model": "CBP" } | null,
      "conversation": { "origin": { "type": "marketing" } } | null
    }
  • Effect on integrators: billing reports can now distinguish billable vs free messages and breakdown by category (marketing/utility/authentication/service) without re-fetching Meta. Messages persisted before this deploy stay with null (no backfill).

2026-06-04 — Inbox Embeds (embeddable inbox iframe)

  • New: Inbox Embed feature — generate iframe-embeddable URLs scoped per customer / phone / project. Token-based auth (ibx_*), hash-stored with timing-safe lookup. Supports light/dark/system themes.
  • New endpoints:
    • POST/GET/DELETE /v1/inbox-embeds (X-API-Key, admin scope)
    • GET /v1/inbox-embed/auth + /conversations + /conversations/:id/messages (Bearer token, scoped)
    • POST /v1/inbox-embed/conversations/:id/messages (send text via embed)
  • New page: app.gosendapi.com/[locale]/embed/inbox/[token] — standalone inbox UI with conversation list, drawer messages, composer
  • New admin UI: app.gosendapi.com/app/inbox-embeds — list/create/revoke embeds with a polished form (scope dropdown, allowed origins with wildcards, default appearance)
  • New portal page: app.gosendapi.com/app/inbox — session-auth inbox for tenant team (no token needed), filters by status/phone/customer, polling 5s/15s, send text via direct Meta Graph API (reusing portal-side encryption)

2026-06-04 — UUID rollout for public IDs

  • New: Every public entity (Customer, Waba, Webhook, Tenant, PhoneNumber, Template) gets a stable UUID.
  • API: Responses now return UUIDs in the id field instead of internal numeric IDs (which are never exposed).
  • Backwards compat: URL params (/v1/customers/:id, /v1/wabas/:id, etc.) accept either UUID or BigInt — existing integrations don’t break
  • Effect on integrators: cleaner external identifiers (e.g., 85028cb8-7ca2-4890-940f-a1c240ac01de vs 12345) and stable across environments. Useful for any integration that stores customer IDs externally
  • New: POST /v1/customers + GET /v1/customers/:id + PATCH/DELETE + GET /v1/customers/:id/wabas (X-API-Key, scoped to tenant of the apikey)
  • New: POST /v1/customers/:customerId/setup-links + GET /v1/setup-links/:id (X-API-Key)
  • Effect on integrators: third-party panels and white-label products can now CRUD customers and generate Embedded Signup URLs from their own UI without needing an admin key

2026-06-04 — Meta App Review approved 🎉 — Live mode

  • Milestone: All 4 requested permissions approved by Meta — whatsapp_business_messaging, whatsapp_business_management, manage_app_solution, public_profile
  • App mode: Switched to Live — external customers can now authenticate and connect their own WABAs via Embedded Signup, without the 5-recipient sandbox limit
  • Effect on existing tenants: no change — your existing connections keep working as before
  • Coming next: Embedded Signup flow available end-to-end from the dashboard

2026-05-27 — Conversations UI + cross-platform phone normalization

  • New: Portal /app/conversations — threaded view of message exchanges grouped by contact. List filterable by status (active/inactive/ended) + search; detail page with chat-style timeline (bubbles), conversation metadata cards, and manual close action
  • New: Send-test from portal — per-template “Send test” button in /app/templates with modal for phone + variable substitution. Useful for QA without curl
  • New: “View in Meta” link per template — opens the corresponding Meta WhatsApp Manager page for cross-validation
  • Infra: PhoneNormalizer utility unifies handling of country-specific WhatsApp quirks (AR “9” mobile flag, MX “1” mobile flag, BR ninth-digit rule). Outbound auto-retries with alternate format on 131030; conversation grouping matches all variants; storage uses canonical form. Net effect: outbound and inbound messages from the same contact group into the same conversation regardless of which format Meta used. See Sending messages → Country-specific quirks.
  • Improved: Message.payload is now AES-256-GCM encrypted at rest for messages persisted from the portal Send-test (was already encrypted from API send + inbound webhook — this closes the inconsistency)
  • Schema: portal Prisma schema sync — added Conversation model + relations (table was already in production DB)

2026-05-18 — Multi-environment API keys

  • New: Each project can have multiple API keys with test (sandbox) or live (production) environment
  • New: Sandbox keys (gsk_test_*) return synthetic responses — no real Meta call, no cost, no spam
  • New: Live keys (gsk_live_*) route to real WABA, real delivery
  • New: Endpoints: GET/POST /v1/api-keys, POST /v1/api-keys/:id/rotate, DELETE /v1/api-keys/:id
  • New: Portal /app/api-keys redesigned with multi-key UI (create dialog with label + env selector, rotate/revoke per key, “shown once” reveal pattern)
  • Schema: new api_keys table; Tenant.apiKeyHash deprecated (kept nullable for legacy fallback)
  • Migration: existing tenant keys backfilled as “Default sandbox” entries

2026-05-18 — Documentation site live

  • New: docs.gosendapi.com launched — Nextra-powered, self-hosted
  • New: Interactive API reference — auto-generated from production NestJS source via OpenAPI + Scalar
  • New: Core Concepts section (7 pages: Projects, Customers, WABAs, Phone numbers, Templates, Messages, Conversations)
  • New: Guides section (6 pages: Sending messages, Webhooks, Setup Links, Error handling, Rate limits, Migration)
  • New: SDKs page with copy-paste helper classes for Node.js, PHP, Python, Go

2026-05-17 — Auth + email infrastructure

  • New: Email/password sign-up at app.gosendapi.com/signup
  • New: Magic link sign-in (passwordless via email)
  • New: Forgot/reset password flow
  • New: Email verification required for password login
  • Infra: transactional email moved out of sandbox — production email delivery active for sign-up, magic links, password reset, and team invites
  • Infra: bcryptjs (cost 12) for password hashing
  • Schema: email_verification_tokens, password_reset_tokens, magic_link_tokens tables added to DB

2026-05-17 — Templates UX rework

  • Improved: Templates page now filters by phone number (preselects first connected phone)
  • New: “Sync with Meta” button — fetches latest templates from Meta Graph API and upserts to DB
  • Improved: Owner/admin role required to sync (viewer/agent see read-only)
  • Internal: lib/encryption.ts mirror of API’s EncryptionService to decrypt AccessTokens portal-side

2026-05-17 — Portal i18n complete

  • New: Full ES/EN bilingual support across the portal (~1500 strings translated)
  • New: Locale switcher in account menu
  • Improved: Settings/Account pages widened to use full screen real estate
  • Improved: Dark mode contrast — card background lighter, muted text more legible, borders visible
  • Improved: Inter font loaded via next/font/google (self-hosted) — better rendering than system fonts

2026-05-16 — Webhook CRUD

  • New: Webhooks page in portal (/app/webhooks) — create, edit, delete, rotate secret, toggle enabled
  • New: Phone-scoped vs project-scoped webhooks
  • New: Event catalog with 14 events (messages, conversations, phones, templates)
  • New: Delivery logs tab with HTTP status, latency, retry count

2026-05-16 — Multi-project support

  • New: Multiple projects per user (each is an isolated workspace with its own customers/WABAs/keys)
  • New: Project switcher in sidebar
  • New: “Create new project” flow from dashboard

2026-05-15 — Team invitations

  • New: Invite member feature in /app/settings/team
  • New: Email invitations with one-time token (7-day expiry)
  • New: Public /invite/[token] page for acceptance
  • New: Role-based access (owner/admin/agent/viewer)
  • New: Pending invitations list with copy-link / revoke actions

2026-05-15 — Edit/delete project

  • New: Project settings page (/app/settings/general) — edit name, default webhook URL
  • New: Delete project flow with typed-name confirmation (owner only)
  • Security: Cascade delete of customers, WABAs, phones, messages, webhooks, invitations

2026-05-15 — Meta App Review submitted

  • Milestone: Submitted Step 2 (Tech Provider verification) to Meta
  • Status: In review (typically 2-7 business days)
  • Effect: Once approved, Embedded Signup flow will become functional

2026-05-13 — Encrypted payloads + admin keys

  • Infra: AES-256-GCM encryption for sensitive fields (message payloads, access tokens)
  • New: Admin keys system with optional IP allowlist
  • New: Audit log for admin operations

2026-05-12 — Customer / User / TenantMember schema

  • Schema: customers, users, tenant_members tables added
  • New: User account model (separate from Tenant)
  • New: Multi-tenant access via TenantMember rows with roles

2026-04-XX — Initial release

  • Infra: NestJS API + Next.js portal scaffolding
  • Auth: Google + GitHub OAuth via Auth.js v5
  • DB: managed MySQL with automated backups
  • Deploy: containerized via Docker with multi-stage builds
  • Endpoints: Messages, WABAs, Templates, Conversations, Webhooks initial implementation

What’s coming

For visibility into the roadmap, email hello@gosendapi.com.

On the radar (order not guaranteed, driven by client demand):

  • Broadcasts / campaigns — send templated messages to a list from the portal or API, with per-recipient variable substitution, throttling and per-conversation metrics.
  • Native mobile app (iOS + Android) — the portal inbox as an installable app with real push notifications, for agents who want alerts on their phone without a browser tab open.
  • Access policy V1 — restrict portal login by IP allowlist + business hours + role-level exceptions, for BPO / enterprise setups.
  • Production-grade SDKs — official @gosendapi/sdk (npm), gosendapi/sdk (composer), gosendapi (pip).
  • Named-recipient sends — send by WhatsApp business unique ID (BSUID) instead of phone number, plus request_contact_info interactive messages.

For subscribing to changelog by email or RSS, [coming soon].