# ama4devs > Local mock servers of the APIs a product depends on, published as Docker images on GHCR, for local development, automated tests and CI without real accounts, credentials or costs. Today: the WhatsApp Cloud API (Meta Graph API v23.0, WhatsApp Business Platform), the Asaas API v3 (Brazilian payment gateway: Pix, bank slip, credit card, installments, subscriptions, webhooks, with a virtual clock), an SMS gateway mock (native, Twilio-compatible, simple GET/POST, message lifecycle, magic numbers, OTP extraction, webhooks and phone inbox panel), plus a generator of valid fake Brazilian data (people, companies, CPF, CNPJ, CNH, PIS, RG, addresses with real CEP ranges, credit cards, bank accounts, Pix keys, charges) that also populates the Asaas mock in one call. Planned, not built yet: more payment providers and banks (Stripe, Rede/Itaú, Pagar.me, C6 Bank, Banco Inter). Em português: simuladores locais do WhatsApp, do Asaas e de SMS, e um gerador de dados fake. Use it when an application integrates with WhatsApp (sending messages and templates, receiving webhooks, media, voice calls) or with Asaas (charges, Pix, bank slip, card, installments, subscriptions, webhooks) and needs a faithful local API to point at. The Asaas mock's virtual clock (`POST /_admin/clock/advance`, needs `CLOCK_MODE=VIRTUAL_FLOWING` or `VIRTUAL_FROZEN`) skips days so overdue charges, D+32 card credit and subscription cycles happen in seconds. A subscription's saved card is charged on each due date with Asaas's retries (08:00, 14:00, 20:00 and three more the next day, a PAYMENT_CREDIT_CARD_CAPTURE_REFUSED webhook each, then OVERDUE); `PUT /_admin/credit-cards/:token {"approvalsLeft": 3}` makes a saved card pay three charges and decline the fourth. - WhatsApp mock image: `ghcr.io/instituto-ama/whatsapp-mock` (port 3010, API at `/v23.0/...`, panel at `/`) - Asaas mock image: `ghcr.io/instituto-ama/asaas-mock` (port 45445, API at `/v3`, panel at `/`) - Generator image: `ghcr.io/instituto-ama/generator-mock` (port 45450, API at `/v1/?count=&seed=`, UI at `/`; the same UI runs in the browser at https://instituto-ama.github.io/ama4devs/generator/app/). `POST /_mock/asaas/populate` creates customers and charges in the Asaas mock; `ASAAS_SEED_ON_BOOT` does it once on boot. The Asaas mock and the generator find each other by themselves (compose service name, localhost, the Docker host); the Asaas panel then shows Integrations › Generator to populate the selected account. `GENERATOR_ENABLED=0` keeps it out of the repository's compose; `GENERATOR_URL=` (empty) turns it off in the Asaas mock - SMS mock image: `ghcr.io/instituto-ama/sms-mock` (port 8899, native API at `/v1/messages`, Twilio-compatible at `/2010-04-01/...`, simple at `/sms/send`, panel at `/`). In-memory SMS gateway with message lifecycle (queued -> sent -> delivered), deterministic magic numbers (0001 failed, 0002 undelivered, 0003 rejected, 0004 stuck sent, 0005 filtered), GSM-7/UCS-2 segment counting, OTP extraction, status and inbound webhooks with HMAC signatures, long-poll `wait` for tests, and a live phone-inbox panel over WebSocket (`/ws`). Control routes under `/_mock/sms/*` - Both mocks together: `make setup`, or `docker compose --profile asaas --profile whatsapp up -d` in the repository root, with Mailpit for the payer e-mails. The SMS mock rides the `asaas` profile, so it comes up with the Asaas mock (`SMS_ENABLED=0` in `.env` keeps it down) - The Asaas mock finds Mailpit, the WhatsApp mock and the SMS mock by itself, however it runs (bun, `docker run`, any compose): by compose service name (`mailpit`, `whatsapp`, `whatsapp-mock`, `sms`, `sms-mock`), on `localhost` and on the Docker host, every 10 s, the closest first; no variable to set (`MAILPIT_URL`, `WHATSAPP_MOCK_URL`, `SMS_MOCK_URL` pin a place, empty turns a channel off). The WhatsApp mock runs alone. ## Documentation site - [Home](https://instituto-ama.github.io/ama4devs/) ([Portuguese](https://instituto-ama.github.io/ama4devs/portuguese/)) - WhatsApp mock: [overview and use cases](https://instituto-ama.github.io/ama4devs/whatsapp/), [quick start](https://instituto-ama.github.io/ama4devs/whatsapp/quick-start/), [API reference](https://instituto-ama.github.io/ama4devs/whatsapp/api/), [docs](https://instituto-ama.github.io/ama4devs/whatsapp/docs/) - Asaas mock: [overview and use cases](https://instituto-ama.github.io/ama4devs/asaas/), [quick start](https://instituto-ama.github.io/ama4devs/asaas/quick-start/), [API reference](https://instituto-ama.github.io/ama4devs/asaas/api/), [docs](https://instituto-ama.github.io/ama4devs/asaas/docs/) - Generator: [overview](https://instituto-ama.github.io/ama4devs/generator/), [quick start](https://instituto-ama.github.io/ama4devs/generator/quick-start/), [docs](https://instituto-ama.github.io/ama4devs/generator/docs/), [the app](https://instituto-ama.github.io/ama4devs/generator/app/) - SMS mock: [overview](https://instituto-ama.github.io/ama4devs/sms/), [quick start](https://instituto-ama.github.io/ama4devs/sms/quick-start/), [docs](https://instituto-ama.github.io/ama4devs/sms/docs/) - API data: [whatsapp-api.json](https://instituto-ama.github.io/ama4devs/data/whatsapp-api.json), [asaas-api.json](https://instituto-ama.github.io/ama4devs/data/asaas-api.json) (each operation with whether the mock has real behavior), and their `-details.json` (parameters, body fields, request examples, responses) ## WhatsApp Cloud API mock - [README](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/whatsapp/README.md): what it simulates, how to run it, the webhook and its signature - [Panel and API reference](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/whatsapp/docs/panel.md): the chat panel, templates, numbers and WABAs, voice calls, voice notes, the X-Mock-* test headers, the /settings routes - [Templates](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/whatsapp/docs/templates.md): message templates, seeds, examples of every component - [Meta's specification](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/whatsapp/spec/README.md): the vendored OpenAPI of the Cloud API and its errata ## Asaas API mock - [README](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/asaas/README.md): what it simulates, the API key, webhooks to localhost, the virtual clock, payer notifications ## Fake data generator - [README](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/generator/README.md): the kinds, the API, determinism by seed, the Asaas population ## SMS gateway mock - [README](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/sms/README.md): what it simulates, native and Twilio-compatible APIs, lifecycle, magic numbers, webhooks ## Repository - [Root README](https://github.com/Instituto-AMA/ama4devs/blob/master/README.md): quick start, profiles, environment - [Mocks contract](https://github.com/Instituto-AMA/ama4devs/blob/master/mocks/README.md): what every mock in this repository guarantees ## Generating speech with a custom pronunciation A text-to-speech model reads a word it does not know as another word — a brand name becomes "Llama", an abbreviation is said in full, an accent is dropped. The fix is the **pronunciation dictionary** of ElevenLabs: rules that force how a word is heard. This is the generic contract; the repository's own voice (`video/`) is one application of it. ### Passing text with a custom pronunciation **1. Create the dictionary from rules.** The rules are phonemes (IPA), which force the sound of the word: ```bash npx -y @elevenlabs/cli@latest pronunciation-dictionaries create_from_rules \ --json '{"name":"my-dictionary","rules":[ {"type":"phoneme","string_to_replace":"koru","phoneme":"ˈko.ɾu","alphabet":"ipa"}, {"type":"phoneme","string_to_replace":"cobrança","phoneme":"koˈbɾã.sɐ","alphabet":"ipa"} ]}' --format json ``` The answer carries `pronunciation_dictionary_id` and `version_id`. The fields of a rule are `string_to_replace`, `phoneme` and `alphabet` — **not** `from` and `to`, which is what the CLI's help shows. **2. Add rules to a dictionary you already have.** `rules set` replaces the whole set, `rules add` adds to it: ```bash npx -y @elevenlabs/cli@latest pronunciation-dictionaries rules set \ --pronunciation-dictionary-id \ --json '{"rules":[ {"type":"phoneme","string_to_replace":"koru","phoneme":"ˈko.ɾu","alphabet":"ipa"} ]}' --format json ``` **3. Pass the dictionary in the request.** The locator goes in the body of `POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}`: ```bash curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/$VOICE_ID?output_format=mp3_44100_128" \ -H "xi-api-key: $ELEVENLABS_API_KEY" \ -H "content-type: application/json" \ -d '{ "text": "É o koru que fala.", "model_id": "eleven_v4", "language_code": "pt", "pronunciation_dictionary_locators": [ {"pronunciation_dictionary_id": "", "version_id": ""} ] }' --output audio.mp3 ``` The version is optional: without it, the latest version of the dictionary is used. Up to three locators may be sent in one request. **4. Keep the version id.** A rule added to the dictionary does nothing until the request points at the version that has it. Store the `version_id` next to the dictionary id (in the project config) and update it when the rules change. ### The rules that matter - **A phoneme forces the sound; an alias rewrites the text.** An alias ("asaas" → "a-za-as") changes what the model reads; a phoneme changes how it sounds. For a word the model does not know, the phoneme is the one that works. - **A phoneme is a guess until it is heard.** Write the phrase, generate it and listen to the file. The IPA that looks right on paper can be heard as a completely different word — one character is the difference between "Herma" and the brand it was meant to be. An automatic transcription is not the judge either: it drops the accents ("cobranca" for "cobrança") and invents the words it does not know ("Llama", "Alma", "LLM"). Only the ear decides. - **When every phoneme fails, remove the rule.** A brand the model reads right on its own is better than a brand that a wrong phoneme breaks: write the text the way it must be heard and let the model read it. - **Never repeat a request to see if the answer changes.** The model reads a number either way ("32 dias" or "trinta e dois") and the choice changes between identical requests, so repeating proves nothing and costs characters. Fix the text or the rule instead. - **Write the text the way it should be heard.** If a word has to be spelled in the script for the model to say it right, write it that way in the script too. ### The permission The API key of a project usually cannot write dictionaries (it has no `pronunciation_dictionaries_write`). The rules go through the official CLI, which is authenticated by the browser: ```bash npx -y @elevenlabs/cli@latest onboard connect --env-path .env.local ``` The key lands in `.env.local` (gitignored). It is never printed and never copied into the chat.