✅ 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-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.

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 via a new nullable column retry_of_message_id (self-referential FK). 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 a wa_message_id exists

Until now, the retry endpoint (POST /v1/messages/:id/retry) rejected requests when the failed message already had a wa_message_id populated. That excluded the most common retryable case: error 131047 (message accepted by Meta with a wa_message_id, 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).
  • BullMQ jobId is now unique per attempt (retry-{messageId}-{timestamp}) with removeOnComplete/Fail: true — fixes an edge case where a completed or failed job stayed in Redis and blocked re-enqueueing when the same message needed another retry days later.

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, per-key rate limits, 131047 UX

Three shipped-in-one-day items focused on operational safety and integrator DX.

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.

Prevents an incident where a compromised client key floods Meta and gets your whole app suspended — you cut that customer’s flow in seconds instead of rotating credentials. See the Meta suspension mitigation plan in the AUP for context.

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

New — Rate limit per API key (apikey-burst + apikey-sustained tiers)

Until today, /v1/* was rate-limited by client IP only (30 req/s + 600 req/min). If a single API key was leaked or misused from a distributed source, one bad actor could exhaust the shared IP tier and impact honest traffic.

Now there’s a new ApiKeyThrottlerGuard that tracks per API key ID first (fallback: actor user ID, then IP). Tiers:

  • apikey-burst: 20 req/s short-window.
  • apikey-sustained: 600 req/min sliding-window.

Public endpoints (auth, webhook receivers) still fall back to the IP tier with the previous limits — no breaking change for unauthenticated paths.

API impact: potentially breaking for integrations exceeding 20 req/s from a single key. If you legitimately need higher throughput per key, contact us and we’ll raise the tier. The Retry-After header is returned as usual on 429.

Complementary: internal /admin-panel/rate-limit-monitor cron watches for keys hitting the 429 tier repeatedly and alerts. Useful signal for detecting leaked keys before they matter.

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.

New schema column Template.parameterFormat (VARCHAR(20), default "POSITIONAL"). Migration 20260705150000_template_parameter_format with rollback.sql.

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.

New column PhoneNumber.hiddenFromInbox (Boolean, default false). Migration 20260705120000_phone_number_hidden_from_inbox with rollback. Audit event phone.inbox_visibility_toggled.

API impact: none for existing endpoints. The field is exposed in GET /v1/phone-numbers/:id (nullable Boolean).

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. A background job evaluates every enabled webhook every 30 minutes. If all delivery attempts in the last 24h failed (with at least 3 attempts), the webhook is auto-disabled and 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. Existing POST /v1/webhooks and GET /v1/webhooks/:id responses now include auto_disabled_at and auto_disabled_reason (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

  • Companion rollback.sql per migration. Every new migration ships with a paired rollback file. Full runbook at docs/runbooks/rollback-migration.md (pre-flight checklist + how to update _prisma_migrations.rolled_back_at).
  • Docker HEALTHCHECK on both containers. API + portal images have healthchecks configured; the hosting layer auto-restarts unhealthy containers. Portal Prisma client warms up on first import to eliminate cold-start P3009 errors right 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 track the message via Meta’s biz_opaque_callback_data echo-back and reconcile 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 fewest open conversations is auto-assigned (assignment_source = 'team_dispatch').
  • 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 gains an optional team_id. The assignment_source field can now take team_dispatch as a new value. 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.
  • New assignment_source values you may see in audit: auto_archive_on_csw_close, auto_release_idle.
  • 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 have an assigned user. New columns on the conversation: assigned_user_id, assigned_at, assigned_by_user_id, assignment_source. Released when the assigned user is deleted (ON DELETE SET NULL) — 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 (stripeCustomerId, planTier, subscriptionStatus, taxId, billingEmail) — fields are nullable until Stripe 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 (phoneNumber, contactPhone) pair instead of session-based. Aligned with industry BSPs.
  • New sub-table ConversationSession tracks Customer Service Window (24h) and Free Entry Point (72h via Click-to-WhatsApp) independently. Message.sessionId references the session a message belongs to (NULL for messages outside any CSW, e.g. templates sent after window closed).
  • Migration is destructive — see deploy notes for production runbook.

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

  • messages.pricing_type captured alongside legacy billable to track PMP tier categories.
  • messages.recipient_country_code derived from E.164 with longest-prefix match across 200+ countries.
  • FEP detection via webhook v24+ (conversation.expiration_timestamp + origin.type=referral_conversion).
  • PricingTierHistory table + handler for VOLUME_BASED_PRICING_TIER_UPDATE (account_update field) to record automatic tier changes Meta applies based on volume.
  • 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: Customer, Waba, Webhook, Tenant, PhoneNumber, Template models added publicId String @unique @default(uuid()) columns
  • API: Responses now return UUIDs in id field instead of BigInt. Internal BigInt is 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, check Twitter @gosendapi (when launched) or email hello@gosendapi.com.

Confirmed next:

  • Sprint 3.4 — Logs page in portal (API + Meta + Webhook delivery logs in one view)
  • Templates Phase 2 — create templates directly from the portal UX
  • Embedded Signup live — fully functional once Meta approves App Review
  • Production-grade SDKs — official @gosendapi/sdk (npm), gosendapi/sdk (composer), gosendapi (pip)
  • OpenAPI versioning — once stable, lock /v1 and document upgrade paths

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