# Going live

> The pre-launch checklist for Amomic-AI — workspace, agent, channels, your team and billing in the console, then keys, idempotency, webhooks, error handling, the 24-hour window and rate-limit headroom for an API integration.

Source: https://docs.amomic.in/operate/going-live

---

Work through this page once, properly, before real customers reach your agent. It has two halves:

- **[In the console](#in-the-console)**: the workspace, the agent, each channel, your team and billing. Owners and admins can do all of it; billing steps need the owner.
- **[For an API integration](#for-an-api-integration)**: keys, retries, webhooks and capacity, for when your own systems send messages, place calls or receive events. Skip it if you only use the console.

Each item says what to do, why it matters, and where to do it. Every item is cheaper now than at 2 a.m. with a queue backing up.

## In the console

**Launch progress** on Home is the short version: six items that tick themselves from what actually exists in your workspace. Get it to 6/6 first, then work through the rest of this half.

![Home with the Launch progress card at 6/6: create and customize your agent, connect a knowledge base, test your agent, install the web widget, connect another channel and invite your team, next to Channel health showing Voice, Web and WhatsApp live](https://docs.amomic.in/screens/home/overview.light.webp)

*Launch progress ticks from what exists, not from which buttons you clicked.*

### Your workspace

- [ ] **Country and time zone are right.** The country decides how phone numbers typed without a code are read and how you get a number; the time zone is the clock for campaign windows, live hours, call-backs and Duesly reminders. Check both under [Settings → Organization](/console/settings#organization).
- [ ] **Your team is invited, with the right roles.** Members see Inbox, Tickets and Live support; only owners and admins see Channels, Campaigns and Developers. Pending invites take a seat until they are accepted. See [Your team](/console/team).
- [ ] **The plan covers what you'll use.** Compare your expected conversations and voice minutes with the plan's allowance on [Billing](/console/billing#usage-this-cycle). Usage past the allowance is paid from prepaid credits, and on Free the allowance is a hard cap. See [Plans & limits](/operate/limits).
- [ ] **Prepaid credits are topped up,** if you are on a paid plan and expect to go past the allowance. When the allowance and the credits are both spent, new conversations and calls are refused until you top up. See [What happens when you run out](/operate/credits#what-happens-when-you-run-out).
- [ ] **Tax details are filled in** before your first payment, if you need a GSTIN on the invoice. An invoice can't be reissued later. See [Tax details](/operate/invoices-and-tax#tax-details).
- [ ] **The right people get notified.** Each person turns on the email notifications they rely on. Security and billing emails always go out. See [Notifications](/console/settings#notifications).

### Your agent

- [ ] **The purpose and instructions say what the agent is for, and what it must not do.** See [Write a purpose that works](/build/agents#write-a-purpose-that-works).
- [ ] **Knowledge is attached and answers real questions.** Ask the [retrieval tester](/build/knowledge-retrieval#the-retrieval-tester) the questions customers actually ask, in their words. A source in error or with no chunks contributes nothing. See [Knowledge bases](/build/knowledge).
- [ ] **You have tested it on the Test tab,** including the questions it should refuse and the ones that need a person. See [What to actually test](/build/testing#what-to-actually-test).
- [ ] **You have published a version.** A version gives you something to roll back to. See [Publish a version](/build/versions#publish-a-version).

::: warning Saved edits reach customers straight away
The live agent is the one you edit. Saving a change to its persona, instructions, tools or knowledge changes what customers get on the next message, whether or not you publish. Test a risky change in the Test tab **before** you save it, and use [Roll back](/build/versions#roll-back-to-an-earlier-version) if a change goes wrong.
:::

### Your channels

Do the rows for the channels you use.

| Channel | Done when | Guide |
|---|---|---|
| **WhatsApp** | The sender is **Ready** or **Connected** with four green readiness checks, **Verify a conversation** reached your phone, and the reply mode matches how your team works. | [WhatsApp go-live checklist](/channels/whatsapp#go-live-checklist) · [Reply modes](/channels/whatsapp-reply-modes) |
| **WhatsApp templates** | Every template you'll send is **approved**, not pending or rejected. | [Templates](/channels/whatsapp-templates) |
| **Phone, in India** | Business verification is approved under [Settings → Compliance](/console/settings#compliance) and the number is live with an agent on it. | [Get a number](/channels/phone-numbers) |
| **Phone, outside India** | Our team has set up your number and it is added to your workspace. | [Ask our team for a number](/channels/phone-numbers#outside-india-ask-our-team-for-a-number) |
| **Every phone number** | You chose the opening line, engine, voice, recording and recording notice, then called the number and listened to a whole call. | [Manage a number](/channels/phone-workspace) |
| **Web chat** | The widget is on your site, your domains are allowed, and a chat from your live site reached the inbox. | [Install the widget](/channels/web-chat) · [Allow your domains](/channels/web-chat#allow-your-domains) |
| **Calendar** | Booking rules are saved and the calendar is linked to the agent that books. | [Calendar go-live checklist](/console/appointments#go-live-checklist) |

**Channel health** on Home shows every channel at a glance. A channel that is not live there is not answering customers.

### Your people

- [ ] **Live support has hours and responders.** Set the live hours and who can take a handoff, and decide whether each customer goes to one person at a time. Leave **Create a ticket when live help is unavailable** on, so a request nobody can take live becomes a ticket. See [Set it up](/console/human-handoff#set-it-up).
- [ ] **Responders will hear the queue.** Each person turns on sound or desktop notifications on their own device. See [Make the queue audible](/console/human-handoff#make-the-queue-audible).
- [ ] **Tickets are routed, and replies can reach quiet customers.** Set routing, the number replies leave from, and get the ticket update template approved so customers quiet for more than 24 hours still hear back. See [Ticket settings](/console/ticket-settings).
- [ ] **Anyone who calls customers from tickets has verified their phone.** See [Verify your phone number](/console/settings#verify-your-phone-number).

### Before your first campaign

- [ ] **Sending rules and quiet hours fit your customers,** and your audience has consent where it needs it. See [Sending rules](/campaigns/sending-rules) and [Scheduling](/campaigns/scheduling).
- [ ] **A small test campaign to your own team** went out and showed up in the campaign's results. See [Create a campaign](/campaigns/create) and [Follow a campaign](/campaigns/results).

## For an API integration

Everything here is done from **Developers**, which owners and admins can open. See [The Developers page](/console/developers).

### Keys and secrets

#### Keys live in a secret manager, never in source

Put every `amk_…` key in the secret store your platform already has: your cloud's secret manager, Vault, your orchestrator's secret objects, or your CI provider's encrypted variables. It reaches the process as an environment variable at runtime and appears in no file that someone can `git clone`.

The `amk` prefix exists so secret scanners can find a leaked key in a repository, which tells you how often it happens. A key in a config file survives every later attempt to be careful: it stays in git history after you delete it, in old forks, in container image layers, and in any CI log that printed the environment. A key in a secret manager is in one place and is rotated in one place.

The same applies to webhook signing secrets. A `whsec_…` secret is shown once, when you add the endpoint, and can't be shown again. If you lose it, add a new endpoint, move your server to its secret, then delete the old one.

::: danger Never put a key in front-end code
`/api/v1` sends no CORS headers, so a browser can't call it, on purpose. A key in a JavaScript bundle is visible to every visitor. For in-page use, choose the [hosted widget](/channels/web-chat) or the [custom-UI client](/channels/web-chat-custom-ui); both use origin-bound visitor sessions and no API key.
:::

#### One key per system, named after that system

Don't share one key across your order service, your warehouse sync and your support tooling. Create one for each and name it after what holds it: `Order service`, `Warehouse sync`, `Support console`. Not `Test key 2`, not `Backend`, not a person's name.

When a key starts returning `429` at 3 a.m., the name tells you which deployment to look at. When you revoke one, the name tells you what will break. And because the **120 requests per minute limit is per key**, separate keys mean a backfill can't starve your customer notifications. The **Key** column in **Developers → Logs** shows which key made each request.

#### Each key carries the least scope it needs

**Create key** starts with **Read conversations** and **Read agents** ticked. Tick only what the system holding the key actually uses, and untick the rest. A warehouse sync needs `conversations:read` and nothing else; if it leaks, the damage is a data read. Give the same key `messages:send` "in case" and the damage becomes messages sent to your customers from your number.

A key's scopes **can't be changed after it is created**. To widen or narrow one, create a new key with the right scopes, deploy it, and revoke the old one. Rolling doesn't help: the replacement copies the old scopes. One asymmetry to know: reading a ticket needs `tickets:write`, because there is no read-only ticket scope.

Check what you actually granted with a call that changes nothing:

```bash
curl https://services.amomic.in/api/v1/ping \
  -H "Authorization: Bearer $AMOMIC_API_KEY"
```

```json title="200 OK"
{
  "ok": true,
  "workspace_id": "ten_01JQRS4T8XZ0AB1CD2EF3GH4",
  "key_name": "Order service",
  "environment": "live",
  "scopes": ["agents:read", "conversations:read", "messages:send"]
}
```

Most "why is this a 403" questions are answered by that response. See [Scopes](/api/scopes).

#### The key and the base URL come from the same place

Keys you create in the console are live keys (`amk_live_…`) for `https://services.amomic.in`. A key that doesn't belong to the host it is sent to is refused with `401` and `details.reason` of `wrong_environment`. Load the base URL and the key from the same configuration source, so a promotion can't move one without the other.

#### Rehearse a roll before you need one

**Roll** issues a new key with the same name and scopes and keeps the old one working for **24 hours**, so you can deploy the new key without a gap. **Revoke** is immediate and permanent: requests with that key fail within seconds.

Roll a key once before launch and deploy the replacement. You want to find out that your service only reloads secrets on a full restart during a rehearsal, not during the grace window. Write down which to reach for: a suspected leak is a **revoke**, planned hygiene is a **roll**.

### Requests

#### Every POST that sends or creates carries an idempotency key from your own record ids

`POST /messages`, `POST /calls`, `POST /tickets`, `POST /campaigns` and `POST /apps/duesly/subscribers` honour the `Idempotency-Key` header. Send one on every call. Derive it from an id your own system already treats as unique, combined with what the call is for.

| Key | Verdict |
|---|---|
| `order-4821-shipped` | Correct. One shipment event, retried as often as needed. |
| `order-4821` | Wrong. The "delivered" message later reuses it with a different body and gets `409`. |
| `crypto.randomUUID()` | Wrong. Every retry is a new key, so every retry sends another message. |
| `order-4821-${Date.now()}` | Wrong. The timestamp changes per attempt, so it isn't a key at all. |

Within 24 hours, the same key with the same body replays the stored response instead of messaging the customer twice or placing a second call. The same key with a **different** body is `409` with `idempotency_key_reuse`. The match is on the **raw request bytes**: serialise your JSON once and resend that exact string.

A second request with the same key while the first is still running gets `409` with `idempotency_in_progress`. Wait two seconds and retry once. A first attempt that returns an error releases the key, so a retry with the same key then runs fresh. If a retry keeps getting `idempotency_in_progress`, the first attempt is still running or never finished. Treat its outcome as unknown: check whether the message, call or ticket exists before you send again. [Idempotency](/api/idempotency) has the full contract.

::: note `POST /contacts` doesn't use the header
It resolves by email, then phone, so repeating it with the same details reaches the same contact. If the email and the phone belong to two different contacts, you get `409` with `identity_conflict` instead of a guessed merge.
:::

#### Error handling branches on `error.code`, then on `error.details.reason`

Every non-2xx response uses one envelope:

```json
{
  "error": {
    "code": "failed_precondition",
    "message": "This workspace has no WhatsApp sender connected.",
    "details": { "reason": "channel_not_connected" },
    "request_id": "req_01JQRS4T8XZ0AB1CD2EF3GH4",
    "correlation_id": "cor_01JQRS4T8XZ0AB1CD2EF3GH4"
  }
}
```

Branch on `error.code` for the class of problem (there are nine) and on `error.details.reason` for the cause. Write a `default` branch on both, because new reasons can be added.

The difference matters most on `401`. Six reasons produce it, and they need four different responses: `missing_api_key` and `malformed_api_key` are code defects; `invalid_api_key` and `revoked_api_key` mean fetch the current key; `expired_api_key` means a roll's grace period ended before you deployed; `wrong_environment` means the key is fine and the host is wrong. Code that pages someone for every `401` pages the wrong person most of the time.

Then decide retryability once, centrally:

| Outcome | Retry? |
|---|---|
| Connection error, timeout, no response | Yes, with the same key and the same bytes |
| `429 rate_limited` | Yes, after the minute boundary. `call_concurrency` needs a call to end first; `daily_call_budget` needs the next day |
| `409 idempotency_in_progress` | Once, after two seconds. See above |
| `500 internal`, `502 provider_error` | Yes, with backoff and the same key. For a message or call, first confirm it didn't go out: a `5xx` can come after the send |
| Other `400`, `401`, `403`, `404`, `409`, `412` | No. Retrying can't change the answer |

Log `error.request_id` on every failure. The request log in the console doesn't show request ids, so your own logs are where you'll find it, and quoting it turns a support thread into a lookup. [Errors](/api/errors) lists every code and reason.

#### Send a correlation id you control

Send `X-Correlation-Id` on every request with a value from your own tracing. It is echoed back on the response and kept with our record of the request, so when you ask about one specific send, the id that finds it is one you already have. If you send nothing, one is generated, which helps us and not you.

### Webhooks

#### The endpoint verifies the real signature, against the raw bytes

Your handler computes `HMAC-SHA256` over the exact string `"{timestamp}.{body}"`, using your `whsec_…` secret as raw UTF-8 bytes, and compares it with the `v1` value in the `Amomic-Signature` header in constant time. It also rejects a timestamp more than **5 minutes** from now, which stops a captured delivery from being replayed later.

The common failure: reading the body with a JSON parser and re-serialising it before verifying. Every byte is part of what was signed. Use `express.raw` rather than `express.json`, or `await request.body()` rather than the parsed model, and parse only after verification passes. [Verifying signatures](/webhooks/signatures) has working code.

An unverified endpoint is a public URL anyone who guesses it can post fake ticket updates and delivery receipts to.

#### The endpoint returns 2xx fast, then does the work

Acknowledge, enqueue, process. A delivery that gets no answer within **10 seconds** counts as failed. A timeout, a `429` or a `5xx` is retried, up to **4 attempts** in all. Any other status, including redirects and other `4xx`, is not retried. After **15 events in a row** fail, the endpoint is switched off automatically. Every attempt is listed in **Deliveries**, with the full payload, and any of them can be replayed. See [Retries & failures](/webhooks/reliability).

#### Someone is alerted when the endpoint starts failing

An automatic switch-off is silent from your code's point of view: your handler simply stops being called. Watch for it from both ends.

In the console, **Developers → Webhooks** shows each endpoint as **Enabled** or **Disabled**, the status of its most recent finished delivery (for example **Last 200**), and the reason when it was switched off automatically. **Deliveries** lists the newest 50 attempts. The console doesn't show a running failure count, so check after any incident rather than waiting for the switch-off.

From your side, alert on the **absence** of events: a handler that has received nothing for an hour on a workspace that normally has traffic. That alert fires whether the cause is a disabled endpoint, a DNS change or an expired certificate.

Deduplicate on the envelope `id`. Delivery is at-least-once, and a replay sends the same `evt_…` again.

#### There is a reconciliation path that doesn't depend on webhooks

Events can be missed: an outage, a switch-off, a deploy that returns `500` for ten minutes. Anything you must not lose needs a second path. For conversations that is the [sync job](/recipes/sync-conversations). For calls it is `GET /calls/{call_id}`. For tickets it is `GET /tickets/{ticket_id}`. Use webhooks for freshness and polling for completeness.

### Channels from your code

#### Notification flows have a plan for the 24-hour window

WhatsApp allows free-form text only inside the **24-hour customer service window**, which opens when the customer messages you and closes 24 hours after their latest message. Business events, such as an order shipped or an appointment tomorrow, rarely fall inside it.

- **Notifications send an approved template, every time.** Don't try text and fall back to a template: you spend a round trip discovering a state you already know, and customers get differently worded messages.
- **Replies send text,** inside the window the customer opened.

Text sent with the window closed returns `409` with `session_window_closed`. A template that is pending or rejected returns `404 template_not_found`. See [The 24-hour window](/channels/whatsapp-window).

#### You have sent to a real phone and read it on a handset

Not the console preview. A real number you control, receiving the real message from the real key, read on a phone. A line that reads well with `{{1}} = "Asha"` can read badly with `{{1}} = "asha menon (guest checkout)"`. For voice, place a real call and listen to all of it: the opening line, the pace, how the agent handles being interrupted, and what it says when it doesn't know.

Normalise phone numbers before they reach the API. `to` must be digits with an optional leading `+`, 6 to 20 characters: `+919876543210` and `919876543210` pass, while `+91 98765 43210` and `+91-98765-43210` are rejected with `400`.

#### Outbound calls are asynchronous

`POST /calls` returns `201` when the number is **ringing**, not when someone answers. The outcome arrives later, from `GET /calls/{call_id}` or the `call.ended` and `call.outcome` webhooks. Code that treats the `201` as a finished call reports every unanswered phone as a success.

Budget for the ceiling of **10 live calls per workspace**, refused with `429` and `call_concurrency` regardless of which key placed them, and for the daily outbound safety limit, refused with `429` and `daily_call_budget`. Queue calls rather than firing them in a loop.

### Capacity and operations

#### You know your rate-limit headroom

The limit is **120 requests per minute per key**, counted in a fixed wall-clock minute. Before launch, work out:

- what one customer event costs in requests (an order update is one `POST /messages`; an escalation that reads context first may be three);
- your peak: a sale, a dispatch batch, a nightly reconciliation;
- what else shares the key. If anything does, give it its own key.

Successful responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; slow down before you are refused. A `429` has no `Retry-After`, so wait for the next minute boundary. Pace bulk work at about two requests a second. See [Rate limits](/api/rate-limits).

#### Configuration failures alert separately from message failures

`channel_not_connected`, `template_not_found`, `no_agent_deployed`, `no_phone_number`, `allowance_exhausted`, `wallet_empty` and `channel_dormant` are not message failures. Every request fails the same way until someone fixes something in the console or on Billing. Give them their own alert, with the fix in the alert text.

#### You have looked at the request log

Open **Developers → Logs** and look at your own traffic before launch. It lists the newest 50 requests made with a valid key: time, method and path, key name, status with its error code, and duration. You'll spot the call you thought you removed, the retry loop that fires four times, and the endpoint you hit twice per event.

Requests refused before the key is recognised (a missing, wrong or revoked key, a suspended workspace, or the rate limit) don't appear there. For those, your own logs and the `request_id` in the response are the record. See [Logs](/console/developers#logs).

#### You know who to contact, and what to send

Write it down where the on-call person will find it.

| Situation | Contact | Send |
|---|---|---|
| Something is broken or behaving unexpectedly | `support@amomic.in` | The `request_id` from the error, the time with its time zone, the key name and your **Organization ID** from [Settings](/console/settings#organization) |
| A suspected security issue or a leaked key | `support@amomic.in` | What you found and how to reproduce it. Revoke the key first, in the console |
| A key needs revoking right now | **Developers → API keys** → **Revoke** | Nothing. It takes effect within seconds |

### The API list, condensed

| # | Item | Done when |
|---|---|---|
| 1 | Keys in a secret manager | No `amk_` string exists in any repository or image |
| 2 | One key per system, named for it | Each key's name identifies a deployment you could point at |
| 3 | Least scope on every key | `GET /ping` returns only the scopes that system uses |
| 4 | Key and base URL from one source | They can't be promoted separately |
| 5 | Roll rehearsed | You rolled a key and deployed the new one without downtime |
| 6 | Idempotency keys everywhere | Every POST that sends or creates sends one derived from your record ids |
| 7 | Errors branch on code, then reason | `401` reasons take different actions; a `default` branch exists |
| 8 | Correlation ids sent | `X-Correlation-Id` carries your trace id |
| 9 | Signature verified on raw bytes | Your handler rejects a tampered body and a 6-minute-old timestamp |
| 10 | Handler answers 2xx well under 10 s | The work happens after the acknowledgement |
| 11 | Webhook silence alerts | You are paged on silence, not only on errors |
| 12 | Reconciliation path exists | Losing an hour of webhooks costs nothing permanent |
| 13 | Window plan per flow | Notifications use templates; replies use text |
| 14 | Real phone tested | You read the message on a handset and heard a call |
| 15 | Rate-limit headroom known | You can state your peak requests per minute per key |
| 16 | Config failures alert separately | `channel_not_connected` doesn't look like a bad message |
| 17 | Request log reviewed | You have seen your own traffic in **Developers → Logs** |
| 18 | Contacts written down | On-call knows where to send a `request_id` |

## Next

::: cards

::: card Plans & limits
[What is enforced](/operate/limits): plan caps, allowances, rate limits and field bounds.
:::

::: card Troubleshooting
[Symptom-first index](/operate/troubleshooting): every API error, what it means and how to fix it.
:::

::: card Security & data
[How keys are stored](/operate/security), what the request log keeps, and how to report a vulnerability.
:::

::: card The Developers page
[Keys, webhooks and logs](/console/developers) in the console.
:::

:::
