Rent and use a number
A rented number is a WhatsApp number MauKirim provisions and hosts for you. You can set its display name and profile photo, send messages, publish presence and receive events at your webhook. MauKirim keeps the WhatsApp session alive; you do not need to pair a QR code or run a client.
Choose and pay for a number in the dashboard or through MCP with a hosted checkout link. Once active, you can operate it over the API or through the corresponding MCP tools.
Renting is outside your package subscription. A package buys sending allowance; a rental buys a number, billed on its own term (monthly or annual) at the price the operator set for it, and it renews until you turn renewal off.
Checkout and API access
Section titled “Checkout and API access”| Step | Where | Why |
|---|---|---|
| Choose and rent a number | Dashboard under Services → Rent a Number, or MCP list_rental_offers, quote_rental and purchase_rental |
The account’s country and the configured payment provider decide which offers can be bought. An MCP purchase requires a device-scoped key, an idempotency key and explicit confirmation; it returns a hosted checkout URL. |
| Use the number | Dashboard, MCP or the public API | Send text, upload bounded base64 media then send by media id, set branding and presence, receive events and stop renewal. Multipart REST endpoints are available for binary uploads. |
There is no public REST purchase endpoint. Do not call the session-only /api/rentals route with a
bearer key; MCP’s purchase_rental tool uses the same checkout rules on behalf of the authenticated
account. The buyer completes payment on the provider’s hosted page.
Rented number setup, end to end
Section titled “Rented number setup, end to end”Steps 1–3 work in the dashboard or through MCP. Step 4 can be a dashboard or MCP action. Steps 5–9 show the public API path, with MCP tools available for text and settings. Steps 1–5 are required, steps 6–7 are optional, and steps 8–9 wire the event hop and prove it works.
1. Sign in to an account that may buy the offer
Section titled “1. Sign in to an account that may buy the offer”The storefront and MCP use the active account’s country to decide which numbers it may rent. A package is not required: rental sits outside the package lockout.
Services → Rent a Number and MCP list_rental_offers show numbers an operator has put on offer.
An offer appears only when:
- the device is allocated to rental and has an active plan,
- the plan’s
allowedCountriesis empty or contains your country, - some enabled payment rail in the plan’s region settles the plan’s currency,
- nobody else holds an open rental on it.
Each row shows the masked number, the plan, the price for the term you select, the plan’s included
sends and whether the session is ready (Session offline means the checks in step 6 are currently failing).
The storefront masks every number, because a listing is public. Once the rental is yours and usable —
active or past_due — My Numbers and GET /devices show the full +E.164 number; a reservation
still awaiting payment (pending_payment) and a released rental stay masked. Address a device by its id in
API calls either way.
2. Choose the number, the term and any promotion code
Section titled “2. Choose the number, the term and any promotion code”Pick a number and Monthly or Annual. The dashboard summary or MCP quote_rental shows the
total due today, with any promotion code checked server-side before anything is bought.
3. Pay
Section titled “3. Pay”In the dashboard, Continue to payment calls the session-only POST /api/rentals with an
Idempotency-Key. In MCP, purchase_rental requires the device scope, an idempotency key and a
confirmed number and term. Both use the same order and checkout rules. A bearer key cannot call
the dashboard route directly; there is no public REST purchase endpoint.
From the moment the order exists the device is held — the rental is pending_payment and a partial
unique index stops a second checkout taking the same number. Then one of two things happens:
- The code covers the whole price (total
0): the order settles locally with no provider call, and the rental activates immediately. - Otherwise the dashboard redirects to the payment provider’s hosted page. MCP returns that checkout URL for you to open. MauKirim never sees a card number. The provider’s notification settles the order; returning from checkout only reports what MauKirim already knows.
A reservation that is not paid within the order window (24 hours) expires and the device is released, so a
number can reappear in the catalogue. The dashboard shows these refusals as messages; the codes behind them
are rental_region_blocked (your country is not on the plan’s list), rental_unavailable (the number is no
longer for sale), rental_already_active (it is already reserved or rented) and currency_unsupported (no
enabled rail settles the plan’s currency).
4. Create an API key with the scopes the rest of the path needs
Section titled “4. Create an API key with the scopes the rest of the path needs”Under Developer → API keys. The API steps below use four scopes, and a key holding all of them can do everything on one number:
| Scope | What this page uses it for |
|---|---|
read |
GET /devices, GET /devices/{id} — confirm the rental and poll the session |
device |
PATCH /devices/{id}/profile, POST /devices/{id}/presence — brand the number, set presence |
send |
POST /devices/{id}/messages — send from the number |
webhook |
/devices/{id}/webhook, GET /webhook-deliveries — where events go and what happened to them |
Create a key in the signed-in dashboard; its value is shown once, and MauKirim keeps only a SHA-256 hash. An MCP assistant can also create one, but only within the scopes its own key already holds — see Authentication and the MCP server.
5. Confirm the rental is active
Section titled “5. Confirm the rental is active”The same rental appears under Services → My Numbers with its profile, webhook, delivery log,
invoices and renewal. Use MCP list_my_rentals or the API’s read key to inspect it:
curl https://app.maukirim.com/api/v1/devices -H "Authorization: Bearer mk_live_…"{ "ok": true, "devices": [{ "id": "gwd_…", "label": "Studio", "number": "+6281234567890", "plan": "Reserved monthly", "status": "active", "term": "monthly", "currency": "IDR", "priceMinor": 149000, "autoRenew": true, "currentPeriodEnd": "2026-10-28T00:00:00.000Z", "pushName": "Studio Support", "hasAvatar": true, "connection": { "state": "connected", "isConnected": true, "isLoggedIn": true, "missingSince": null } }]}The row appears as soon as the order exists, so status is the thing to read: pending_payment until
the first invoice is paid, then active. Only active and past_due make the per-device routes reachable;
every other state answers 404 rental_not_found on all of them — which is why this list, and not the
per-device routes, is where you check a rental.
number depends on status. While the rental is usable — active or past_due — number is the
full +E.164 number, +6281234567890 above, and pushName, hasAvatar and the connection object are
the real values. While it is not — pending_payment before the first invoice is paid, or a released
suspended/cancelled/expired rental — number is the storefront mask (+, first two digits,
••• •••, last four), pushName is null, hasAvatar is false, and every connection field is
null. A number you can use is one you may call back; a number you cannot is not yours to keep in a log.
Your customers’ numbers — the ones you send to — are always full E.164.
GET /devices/{id} reads one device instead and adds the rental terms (including includedMessages, the
rental plan’s send allowance), the webhook settings, lastSeenAt and the same connection object.
Only an active/past_due rental reaches it, so its device.number is always the full number.
It never returns the webhook signing secret.
6. Wait until the session is connected
Section titled “6. Wait until the session is connected”A rental being paid for is not the same as the number answering. Poll GET /devices (or read one device
with GET /devices/{id}) until connection.isConnected and connection.isLoggedIn are both true.
A row whose status is not active/past_due reports every connection field as null on purpose —
that is the same mask that hides its number, not a stale observation, and readiness only means something
once the rental is usable. For a usable rental, connection.state is the gateway’s own label for the
session — disconnected, connecting, connected or logged_in, and null before the first sync has
been observed. When the gateway’s status call did not answer, isConnected and isLoggedIn are null
rather than false: treat anything other than true as not ready. missingSince becomes non-null if the
gateway stops reporting the device. Until it is ready, a send, a branding write or a presence call is
refused:
| Code | Condition |
|---|---|
409 device_not_connected |
The WhatsApp session is not connected or not logged in |
409 rental_device_not_ready |
The last observation is too old to act on — or the device or its gateway row is gone |
409 gateway_disabled |
The gateway is switched off or has never been reached |
404 device_not_found |
The device is no longer in the gateway’s inventory |
The first two are worth a retry; the last two are an operator’s problem — contact support rather than retrying.
7. Brand the number and set its presence (optional)
Section titled “7. Brand the number and set its presence (optional)”Both are covered in Branding and Presence below. Set the display name and photo once so recipients see your business rather than a bare number, and use presence if you want the number marked online while a human is answering. Neither is required to send.
8. Point the number’s events at your endpoint
Section titled “8. Point the number’s events at your endpoint”MauKirim owns the device’s single gateway webhook, verifies what arrives, and forwards it to a destination you configure here:
curl -X POST https://app.maukirim.com/api/v1/devices/gwd_…/webhook \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -d '{"url": "https://api.your-app.com/hooks/maukirim", "events": ["message", "message.ack"]}'{ "ok": true, "webhook": { "id": "rwh_…", "deviceId": "gwd_…", "url": "https://…", "events": ["message", "message.ack"], "active": true }, "secret": "9f3c…64 hex characters…" }Store the secret now — it is returned only by the call that created the destination (or by a deliberate
rotation with PUT); an edit and a read never return it. Your endpoint verifies every delivery with it:
import { createHmac, timingSafeEqual } from "node:crypto"
// Over the raw bytes, before any JSON parsing: re-serializing changes them and the signature stops matching.export function verifyMauKirim(rawBody, signatureHeader, secret) { const expected = "sha256=" + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex") const a = Buffer.from(expected, "utf8") const b = Buffer.from(signatureHeader ?? "", "utf8") return a.length === b.length && timingSafeEqual(a, b) // constant time, length-checked first}Every delivery carries x-maukirim-event, x-maukirim-delivery and x-maukirim-signature: sha256=<hex HMAC-SHA256 of the raw request body>. Webhooks has the same function for PHP and for
debugging by hand, and the two ways this usually goes wrong.
GET /devices/{id}/webhook also reports gatewayWired: if it is false, MauKirim has not yet confirmed
that the gateway reports to us, so nothing will arrive and the fault is not at your endpoint.
Left out, events means every event; unknown names are rejected and duplicates removed. Valid url
values are https only, without credentials, query or fragment, and must name a public host — not an IP
literal and not localhost, because the worker POSTs on your behalf. A handler running on your laptop
therefore needs a public tunnel in front of it.
9. Send a test message, then prove the hop
Section titled “9. Send a test message, then prove the hop”Send to a phone you control (scope send):
curl -X POST https://app.maukirim.com/api/v1/devices/gwd_…/messages \ -H "Authorization: Bearer mk_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: setup-check-1" \ -d '{"phone": "+6281234567890", "message": "Setup check from our new number."}'{ "ok": true, "batchId": "gwb_…", "created": true, "progress": { "total": 1, "counts": { "queued": 1 }, "terminal": false } }created: false means the Idempotency-Key replayed an identical request and the earlier batch was
returned — not a second message. There is no batch-status endpoint: the snapshot returned here is not a
subscription, and the outcome arrives as a webhook (message when a message is sent or received,
message.ack for the recipient’s receipt).
Then check both hops:
- Send a WhatsApp message to the rented number from your personal phone. A
messageevent should reach your endpoint, signed. - Read what was forwarded and what your endpoint answered:
curl "https://app.maukirim.com/api/v1/webhook-deliveries?deviceId=gwd_…&limit=25" \ -H "Authorization: Bearer mk_live_…"{ "ok": true, "deliveries": [{ "id": "whd_…", "deviceId": "gwd_…", "event": "message", "state": "delivered", "attempts": 1, "responseStatus": 200, "errorCode": null, "payload": { "…": "sanitized event body" }, "createdAt": "…", "completedAt": "…" }], "hasMore": false}state is queued, delivered, failed (one non-2xx answer) or exhausted (all 5 attempts used, or the
webhook is inactive). Any 2xx counts as success — a 3xx is a failure — and retries reuse the same
x-maukirim-delivery id, so deduplicate on it.
Troubleshooting the setup
Section titled “Troubleshooting the setup”| What you see | Read this first | What it means |
|---|---|---|
Every device route answers 404 rental_not_found |
GET /devices |
No active/past_due rental for that device on this key’s account. A device that belongs to somebody else answers identically on purpose. If the row says pending_payment, the payment has not settled; suspended, cancelled and expired are terminal. |
403 api_key_scope_denied |
The operations table below | The key does not carry the scope the route requires. Create one that does; scopes cannot be added to an existing key. |
401 api_key_invalid / api_key_revoked / api_key_expired |
Authentication | The key is wrong, revoked or past its expiry. Rotation is create-then-deploy-then-revoke. |
409 device_not_connected or 409 rental_device_not_ready |
GET /devices → connection |
The session is down or the last health observation is stale. Retry once it reports isConnected and isLoggedIn; if it stays down, contact support. |
409 gateway_disabled, 404 device_not_found |
— | The gateway is off/unreachable or the device left the gateway’s inventory. Not something your code can fix: contact support. |
410 media_unavailable |
— | No private media bucket is configured, so an avatar or attachment cannot be stored. |
400 invalid_input |
Errors | The rejected field is named in the message: phone not E.164, pushName over 64 characters, an avatar that is not JPEG/PNG or whose bytes do not match its type, a webhook URL that is not public https, an unknown event name. |
409 batch_idempotency_conflict |
Idempotency | The same Idempotency-Key was reused with a different body. Use a new key for a new message. |
| Nothing arrives at your webhook | GET /devices/{id}/webhook |
webhook: null means none is configured; active: false means it is paused; gatewayWired: false means the gateway is not confirmed wired — ask MauKirim; otherwise check the events list and your signature check. |
The delivery log says endpoint_unreachable, endpoint_rejected, webhook_inactive, webhook_secret_unreadable |
Webhooks | Your endpoint did not answer, answered non-2xx, the webhook is paused, or the stored secret could not be opened (tell support). |
A message shows accepted but the customer says it never arrived |
message.ack on your webhook |
accepted is MauKirim’s side of the hop (queued → sending → accepted/failed); the recipient’s delivery and read receipts arrive later as message.ack events, so the customer’s phone is the other half of the story. uncertain means the attempt may or may not have arrived and was not retried. |
| The rental page still says pending | /services/rent/return |
The provider’s notification has not settled the order yet. Refresh; a late activation is not a lost number, and the reservation holds until the order window elapses. |
The operations you can perform
Section titled “The operations you can perform”| What | Request | Scope |
|---|---|---|
| List your rented numbers | GET /devices |
read |
| Read one device, its rental and its webhook settings | GET /devices/{id} |
read |
| Set the WhatsApp display name or profile photo | PATCH /devices/{id}/profile |
device |
| Set your presence to online/offline | POST /devices/{id}/presence |
device |
| Show or clear typing in one chat | POST /devices/{id}/chat-presence |
device |
| Send text, an image or a file from the number | POST /devices/{id}/messages |
send |
| Configure where events are delivered | GET/POST/PUT/DELETE /devices/{id}/webhook |
webhook |
| Read what was delivered and what your endpoint replied | GET /webhook-deliveries |
webhook |
Sends from a rented number are covered by the rental’s own terms, not by the OTP/notification credit allowance — see Credits and pricing.
Sending accepts multipart/form-data as well as JSON: a file part (an image up to 5 MiB, or a document up
to 16 MiB) with phone and an optional caption, and the filename you send is the filename the recipient
reads. An image renders inline with the caption, a document arrives as a named file.
Branding
Section titled “Branding”# the display name customers seecurl -X PATCH https://app.maukirim.com/api/v1/devices/gwd_…/profile \ -H "Authorization: Bearer mk_live_…" -H "Content-Type: application/json" \ -d '{"pushName": "Studio Support"}'
# the profile photocurl -X PATCH https://app.maukirim.com/api/v1/devices/gwd_…/profile \ -H "Authorization: Bearer mk_live_…" \ -F "avatar=@logo.png;type=image/png"The display name is 1–64 characters; the photo is a JPEG or PNG up to 5 MiB whose bytes must match its
declared type. Both take effect on WhatsApp immediately, and the next sync reports the name back as
pushName. There is no “remove photo” operation — WhatsApp’s API for this only replaces, so changing the
photo is the only mutating action.
Presence
Section titled “Presence”curl -X POST https://app.maukirim.com/api/v1/devices/gwd_…/presence \ -H "Authorization: Bearer mk_live_…" -H "Content-Type: application/json" \ -d '{"type": "available"}'type is available or unavailable. This is WhatsApp’s “online” indicator for the number — nothing is
recorded on MauKirim’s side, so it is not a status endpoint. Like a send, it requires the session to be
reachable, and fails with the same 409/404 codes when it is not.
Incoming typing updates use the rented device’s chat_presence
webhook event. To show typing from your number in one
recipient chat, use the separate endpoint below.
Typing before a rented-number send
Section titled “Typing before a rented-number send”Start typing for the recipient (scope device):
curl -X POST https://app.maukirim.com/api/v1/devices/gwd_…/chat-presence \ -H "Authorization: Bearer mk_live_…" -H "Content-Type: application/json" \ -d '{"phone":"+6281234567890","action":"start"}'Then submit the message with POST /devices/{id}/messages and its Idempotency-Key (scope send).
The send response means queued, not dispatched. When your workflow no longer wants typing shown, send
stop with the same number:
curl -X POST https://app.maukirim.com/api/v1/devices/gwd_…/chat-presence \ -H "Authorization: Bearer mk_live_…" -H "Content-Type: application/json" \ -d '{"phone":"+6281234567890","action":"stop"}'Do not stop solely because the send request returned 200; the queue may not have dispatched the message
yet. The typing calls are immediate gateway actions, take no Idempotency-Key, and do not bill a message.
They require a healthy gateway and a connected, recently seen rented device. See
Device actions for the response and errors.
Renewal, suspension and what happens if you stop paying
Section titled “Renewal, suspension and what happens if you stop paying”Rental status |
What it means | What the API does |
|---|---|---|
pending_payment |
A reservation while the first invoice is unpaid | Nothing yet — the device is held, and released if the order window lapses |
active |
Paid for the current period | Everything works |
past_due |
The renewal invoice is unpaid but still inside the grace period | Everything still works — this is your window to fix billing |
suspended |
The grace period ended | Every device route answers 404 rental_not_found |
cancelled |
The rental ended | Same as suspended |
expired |
A reservation that was never paid | Same as suspended; the number is back in the catalogue |
MauKirim invoices a rental before the period starts (three days ahead) and gives you a three-day grace
window after the due date before anything stops, so a failed card does not take a number offline the same
hour. Renewal is invoicing, not a stored card: each period arrives as an invoice you pay through the same
checkout, and the invoice history is on My Numbers. Cancel renewal turns autoRenew off and keeps the
period you already paid for: the rental ends at currentPeriodEnd and moves to cancelled then.
Your number, your events
Section titled “Your number, your events”The events from WhatsApp — incoming messages, acknowledgements, reactions, presence, group changes — are
delivered to your endpoint, not kept inside MauKirim. Configure it with
POST /devices/{id}/webhook, verify the signature, and read Webhooks before you build
on it: retries, ordering and the x-maukirim-delivery id all matter.