Acquire a temporary phone number from Grizzly SMS and automatically watch it for the incoming SMS code — in a single run — with push notifications over ntfy and/or Discord.
Built for grabbing scarce numbers the moment they come in stock (e.g. Apple in
Turkey — SERVICE=wx, COUNTRY=62) and relaying the verification code straight
to your phone.
⚠️ Real purchases.getNumberis not a stock check — every acquired number reserves a real number and holds your Grizzly balance. A number that expires without an SMS is auto-refunded.MAX_ACQUISITIONS(default1) is a hard cap: extras won by a concurrency burst are cancelled and refunded automatically.
- One-shot flow — acquire → watch for the SMS code → notify, no manual steps.
- Hard single-number guarantee — burst-bought extras are cancelled (
setStatus=8) and refunded. - ntfy and/or Discord — pick either or both; you choose simply by setting the URL(s).
- Rate-limited workers — one shared limiter across threads, with a global backoff on HTTP /
429errors. - Fatal-response aware — stops on
BAD_KEY/NO_BALANCE/WRONG_MAX_PRICEand exits non-zero. - Provider filtering — target or exclude specific providers (
PROVIDER_IDS/EXCEPT_PROVIDER_IDS). - Live stock & price check — logs and notifies current availability/price for
SERVICE+COUNTRYviagetPricesV3, at startup and on an interval, independent of acquisition. - Auto-loads
.env— nosource .envneeded; zero third-party deps beyondrequests. - Watch-only mode — re-attach to numbers you already own:
python -m grizzly watch <id> ....
- Acquire — worker threads poll
getNumberat a shared rate limit. The first number returned (ACCESS_NUMBER:<id>:<phone>) is kept; any extra won in the same burst is cancelled and refunded, so you keep exactlyMAX_ACQUISITIONS. - Watch — the tool polls
getStatusfor the kept number until the code arrives (STATUS_OK:<code>), the number expires (STATUS_CANCEL), or the watch times out.
NO_NUMBERS → keep polling. HTTP / 429 → the whole pool backs off (honouring
Retry-After). Fatal response (BAD_KEY, NO_BALANCE, WRONG_MAX_PRICE:<min>) →
notify and stop.
Separately, and independently of the above, the bot checks live stock and price for
SERVICE+COUNTRY via Grizzly's
getPricesV3
action — once at startup (included in the startup notification) and again every
PRICE_CHECK_INTERVAL_SECONDS for the duration of the acquire phase. This costs no
balance and reserves no number. See Stock & price check below.
The SMS only arrives once you enter the acquired number into the target service. The watcher relays the code — it can't conjure one.
grizzly/
__main__.py # CLI entry point (full flow + `watch` subcommand)
config.py # env parsing + .env auto-loader
notify.py # ntfy / Discord backends + fan-out notifier
api.py # Grizzly client, rate limiter, response parsing, price parsing
bot.py # Acquirer (workers + hard cap), Watcher (SMS polling), StockWatcher (price check)
tests/ # stdlib unittest, no network
- Docker with Docker Compose
- A Grizzly SMS API key
- An ntfy topic URL and/or a Discord webhook URL
cp .env.example .env # then edit it (see Configuration)
docker compose up -d --build
docker compose logs -f --tail=100
docker compose downSet NTFY_URL and/or DISCORD_WEBHOOK_URL in .env (at least one is required).
By default, .env.example targets Apple with SERVICE=wx and Turkey with
COUNTRY=62. You can verify service and country codes in the Grizzly SMS
API documentation, the
Apple service page, and the
price/country table.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Full flow: acquire a number, then watch it for the SMS code
python -m grizzly
# Watch numbers you already own (no purchase)
python -m grizzly watch 541507557 541507572.env is auto-loaded from the current directory at startup — no source .env
needed. Real environment variables still take precedence, and
GRIZZLY_ENV_FILE=/path/to/env points at a different file.
| Variable | Required | Default | Description |
|---|---|---|---|
GRIZZLY_API_KEY |
yes | — | Your Grizzly SMS API key. |
SERVICE |
yes* | — | Service code (wx = Apple). |
COUNTRY |
yes* | — | Country code (62 = Turkey). |
MAX_PRICE |
yes* | — | Max bid; must be ≥ the platform minimum (else WRONG_MAX_PRICE). |
PROVIDER_IDS |
no | — | Comma-separated provider IDs to target; omitted when empty. |
EXCEPT_PROVIDER_IDS |
no | — | Comma-separated provider IDs to exclude; omitted when empty. |
NTFY_URL |
one of† | — | ntfy topic URL. |
DISCORD_WEBHOOK_URL |
one of† | — | Discord webhook URL. |
THREADS |
yes* | — | Number of worker threads. |
MAX_REQUESTS_PER_SECOND |
yes* | — | Global request rate shared by all workers. |
REQUEST_TIMEOUT_SECONDS |
yes* | 10 | HTTP timeout (defaults to 10s in watch mode). |
MAX_ACQUISITIONS |
no | 1 |
Numbers to keep; extras are cancelled + refunded. 0 = unlimited. |
STATUS_EVERY_REQUESTS |
no | 100 |
Progress-log cadence during acquisition. |
STATUS_POLL_SECONDS |
no | 5 |
Watch-phase poll interval. |
WATCH_TIMEOUT_SECONDS |
no | 1200 |
Watch deadline (number lifetime ≈ 20 min). |
PRICE_CHECK_INTERVAL_SECONDS |
no | 300 |
How often to re-check stock/price for SERVICE+COUNTRY during acquisition. Minimum 10. |
LOG_LEVEL |
no | INFO |
Python logging level. |
GRIZZLY_API_URL |
no | prod | Override the endpoint (debugging). |
† At least one of NTFY_URL / DISCORD_WEBHOOK_URL must be set. If both are set,
notifications go to both.
* Required only for the acquire flow (python -m grizzly). The watch
subcommand needs just GRIZZLY_API_KEY and a notifier (plus optional
STATUS_POLL_SECONDS / WATCH_TIMEOUT_SECONDS).
Service, country, and provider codes: see the Grizzly SMS API docs, the Apple service page, and the price/country table.
PROVIDER_IDS / EXCEPT_PROVIDER_IDS are optional and independent — target
specific providers, exclude specific providers, or leave both empty to accept
whatever Grizzly offers.
PROVIDER_IDS=311 # target one provider
PROVIDER_IDS=311,312 # target several
PROVIDER_IDS= # no targeting — accept any provider
EXCEPT_PROVIDER_IDS=12,25,159 # exclude specific providers insteadThe bot shows how many numbers are available and at what price for the configured
SERVICE+COUNTRY, using Grizzly's
getPricesV3
action. This is a separate, free lookup — it does not reserve a number or spend
balance, unlike getNumber.
It runs:
- Once at startup, logged and included in the startup notification.
- Every
PRICE_CHECK_INTERVAL_SECONDS(default 300s) in the background, logged only, for the duration of the acquire phase.
Example log line:
service=wx country=62 available=1307 price=2 providers=[311:820@2, 312:487@2.5/3]
available and price are the totals Grizzly reports for the service+country pair.
providers breaks the same numbers down per provider ID — useful together with
PROVIDER_IDS/EXCEPT_PROVIDER_IDS, since a provider can be in stock even when the
total looks low, or vice versa. A provider's price can list more than one value;
Grizzly returns a short list of price tiers per provider rather than a single number.
The startup notification also tries to show human-readable service/country names (via
getServicesList/getCountries) instead of just the codes. This is best-effort: if
Grizzly's response for either lookup doesn't parse, the bot falls back to the raw codes
and logs the reason at DEBUG — it never blocks the stock check itself.
If Grizzly ever changes what these codes mean or price data isn't available for your
account, getPricesV3 returns BAD_KEY, BAD_ACTION, or BAD_SERVICE; the bot logs
that exact reason rather than a generic failure.
Start with conservative values:
THREADS=5
MAX_REQUESTS_PER_SECOND=2Increase them slowly if your machine, network, and Grizzly SMS account can handle
it. If you receive HTTP errors, lower THREADS or MAX_REQUESTS_PER_SECOND.
| Event | Urgent |
|---|---|
| Bot started | no |
| Number acquired | yes |
| Extra number cancelled (refunded) | no |
| SMS code received | yes |
| Number expired | no |
| Watch timeout / interrupted | no |
| Fatal / stopped | yes |
No network, standard library only:
python -m unittest discover -s tests -t .- Repository: https://github.com/kurosaki-sol/GrizzlySmsBot
- Use with your own Grizzly account and API key. You are responsible for how you use temporary numbers and for complying with Grizzly SMS' terms of service.