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.
Work through this page once, properly, before real customers reach your agent. It has two halves:
- 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: 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.
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.
- [ ] 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.
- [ ] The plan covers what you'll use. Compare your expected conversations and voice minutes with the plan's allowance on Billing. Usage past the allowance is paid from prepaid credits, and on Free the allowance is a hard cap. See Plans & 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.
- [ ] 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.
- [ ] The right people get notified. Each person turns on the email notifications they rely on. Security and billing emails always go out. See 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.
- [ ] Knowledge is attached and answers real questions. Ask the retrieval tester the questions customers actually ask, in their words. A source in error or with no chunks contributes nothing. See Knowledge bases.
- [ ] 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.
- [ ] You have published a version. A version gives you something to roll back to. See Publish a version.
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 if a change goes wrong.
Your channels #
Do the rows for the channels you use.
| Channel | Done when | Guide |
|---|---|---|
| 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 · Reply modes | |
| WhatsApp templates | Every template you'll send is approved, not pending or rejected. | Templates |
| Phone, in India | Business verification is approved under Settings → Compliance and the number is live with an agent on it. | Get a number |
| Phone, outside India | Our team has set up your number and it is added to your workspace. | 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 |
| 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 · Allow your domains |
| Calendar | Booking rules are saved and the calendar is linked to the agent that books. | Calendar 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.
- [ ] Responders will hear the queue. Each person turns on sound or desktop notifications on their own device. See 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.
- [ ] Anyone who calls customers from tickets has verified their phone. See 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 and Scheduling.
- [ ] A small test campaign to your own team went out and showed up in the campaign's results. See Create a campaign and Follow a campaign.
For an API integration #
Everything here is done from Developers, which owners and admins can open. See The Developers page.
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.
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 or the custom-UI client; 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:
curl https://services.amomic.in/api/v1/ping \-H "Authorization: Bearer $AMOMIC_API_KEY"
{"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.
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 has the full contract.
`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:
{"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 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 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.
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. 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.
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.
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.
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 |
| 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 #
Plans & limits
What is enforced: plan caps, allowances, rate limits and field bounds.
Troubleshooting
Symptom-first index: every API error, what it means and how to fix it.
Security & data
How keys are stored, what the request log keeps, and how to report a vulnerability.
The Developers page
Keys, webhooks and logs in the console.