Send an SMS with the IllyVoIP API and track delivery

Send and reconcile SMS API batches using accepted message IDs, duplicate removals, rejected recipients and separate delivery and charge states.

Use POST /api/v1/sms/messages to submit an SMS, save the returned message identifiers, then use GET /api/v1/sms/messages/status or delivery webhooks to follow the outcome. A successful submission means the accepted messages were queued; it is not a delivery receipt.

This walkthrough is for a developer sending messages from a backend service. It covers one deliberate submission and its follow-up, rather than a bulk-send loop.

Submission is not delivery. Explanatory diagram showing the workflow, not an interface or completed transaction.
Submission is not delivery. Explanatory diagram showing the workflow, not an interface or completed transaction. Open the image for a larger view.

1. Prepare the account and request

You need account API access, sufficient balance, a permitted sender and the complete recipient number in international format. Use IllyVoIP or a sender approved and available for your account. A sender shown in another example does not give your account permission to use it.

The examples use a terminal with cURL and a server-side API key supplied through the ILLYVOIP_API_KEY environment variable. Keep the key out of browser code, screenshots and source control. Follow the API quickstart to reveal your key using the Portal’s verification prompts. Examples are templates, not records of a completed transaction; replace placeholders deliberately before running them.

The JSON-building examples also use jq. Set SMS_TO to a recipient you are authorized to message. Set SMS_FROM to your allowed sender. This request sends a real, potentially billable message; the API Playground also uses your account when you run a request.

2. Build one submission with a stable idempotency key

Create a unique key for this intended submission and retain it with the exact payload. Use the same key and same payload when reconciling or retrying that submission. Use a different key for a genuinely new message. Keys must meet the documented 8–128-character format.

# Set ILLYVOIP_API_KEY, SMS_FROM and SMS_TO securely first.
: "${SMS_FROM:?Set an allowed sender}"
: "${SMS_TO:?Set an authorized recipient in international format}"
SMS_REQUEST_KEY="$(python3 -c 'import uuid; print(uuid.uuid4())')"
jq -n --arg sender "$SMS_FROM" --arg recipient "$SMS_TO" \
  '{from:$sender,to:[$recipient],message:"Your requested update is ready."}' \
  > sms-request.json
printf '%s\n' "$SMS_REQUEST_KEY" > sms-request-key.txt
curl --silent --show-error --fail-with-body \
  'https://api.illyvoip.com/api/v1/sms/messages' \
  -H "X-Api-Key: ${ILLYVOIP_API_KEY:?Set ILLYVOIP_API_KEY}" \
  -H "Idempotency-Key: $SMS_REQUEST_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @sms-request.json > sms-response.json

Keep these request/response files private. The example creates a new key when run from the beginning, so do not rerun the whole block to retry an uncertain send. Read the saved key and reuse the saved payload for that same submission.

3. Read accepted and rejected recipients separately

Check the HTTP result and JSON status before treating the request as successful. On acceptance, store queued_message_ids, queued_count and the returned total_cost, along with your own request reference.

FieldHow to use it
Message IDsSave each accepted message identifier for status checks.
Rejected recipientsReview rejected recipients and their reasons; they were not silently accepted.
Removed duplicatesCheck which repeated destinations were removed.
Replay flagRecognize a repeated response for the same submission.

The corresponding response fields are queued_message_ids, invalid_numbers, duplicates_removed and replayed.

In a mixed batch, returned message IDs correspond to accepted unique recipients in their accepted order, not every input array position. Do not pair the first returned ID with the first original number without accounting for rejections and duplicate removal. A one-recipient request is easier to verify while building your integration.

4. Query an accepted message’s status

Copy an actual ID from the successful response into SMS_MESSAGE_ID. Do not use the idempotency key as the message ID.

: "${SMS_MESSAGE_ID:?Set a returned queued message ID}"
curl --silent --show-error --fail-with-body --get \
  'https://api.illyvoip.com/api/v1/sms/messages/status' \
  -H "X-Api-Key: ${ILLYVOIP_API_KEY:?Set ILLYVOIP_API_KEY}" \
  --data-urlencode "message_id=$SMS_MESSAGE_ID"

Read sms_status, its label/message and available timestamps. The status response can also describe costs and refunds. Keep delivery state separate from billing: a refund is not a delivery confirmation, and a sent message is not necessarily delivered to the handset.

  • Queued: accepted for processing.
  • Sent: submitted onward; wait for the available delivery result.
  • Delivered: the service has a delivery result; this is not proof that a person read it.
  • Failed: inspect the returned explanation.
  • Unknown or unresolved: reconcile before sending a replacement.
  • Refund pending/refunded: inspect the billing fields separately from delivery.

5. Add delivery webhooks when your first request works

Use the existing webhook guide to configure the required SMS events, verify signatures against the raw request body, acknowledge promptly and deduplicate event IDs. Store the message reference so your application can correlate submission, status lookup and webhook updates.

A validation rejection is not a queued SMS and does not produce a delivery webhook. Your application must process the immediate API response as well as later notifications.

6. Handle content, encoding and uncertain responses

SMS length depends on encoding and destination rules. GSM-7 and Unicode use different segment limits; some characters use more than one unit. Review the returned encoding and segment information. If a destination rejects Unicode, correct the message deliberately instead of stripping characters without telling the user.

If the response requests sender approval or more information about the SMS use case, follow that process. Staff approval does not resend an old stopped message automatically. Correct validation failures before resubmitting; do not treat them as transient network errors.

After a timeout, retain the original key and request. Do not generate a new key just to get past an incomplete-submission response: that can represent a new send. Check the available history/status and contact Support with the submission reference if the outcome remains uncertain. Avoid automatic retries of paid POST requests until your integration handles the documented semantics.

Frequently asked questions

Can I use this example for any country?

The endpoint accepts international-format destinations, but route availability, sender rules, encoding and delivery vary. Check the actual destination and account capability. A valid number alone is not proof of deliverability.

Why was only part of my recipient list accepted?

Inspect rejected recipients and duplicate removal in the immediate response. Track only the accepted message IDs as queued messages.

Does HTTP success mean the SMS arrived?

No. Follow the message’s delivery status or webhook outcome.

Related guides and reference

For current endpoint parameters and examples, use the IllyVoIP developer resources and public API specification.

Instructions reviewed on 8 October 2026. Available controls and service options depend on your account. Use the current values shown in your Portal.

Reconcile a batch containing duplicates or rejected numbers

A successful batch response does not mean every submitted row became a message. Store the original request, its idempotency key and the complete response together before creating delivery-tracking jobs. The response’s queued IDs belong to the accepted unique recipients, not every element of your input array.

  1. Start with international numbers and retain their original row identifiers. IllyVoIP normalizes common number formatting and removes repeated normalized destinations. A local normalization check is useful, but the API’s admission result remains authoritative.
  2. Read invalid_numbers with each returned reason and duplicates_removed separately. A removed duplicate is not a second failed delivery: it did not create a second queued message.
  3. For every returned queued_message_ids entry, request its SMS status and use the returned to field to confirm the recipient. This is safer than assuming that every raw input row lines up with a queued ID.
  4. Persist the relationship between your batch, original recipient rows and actual message ID. If multiple input rows represented the same number, link those rows to the one accepted message rather than sending it again.
  5. Track each message independently. The batch response confirms queue admission; delivery and final charging can change afterward.

Worked mapping example

Consider four illustrative rows: recipient A, recipient A again with spaces, an invalid value, and recipient B. If A and B are accepted, the result has two queued IDs, one duplicate removal and one invalid entry. Confirm each ID’s to through the status endpoint before assigning it to A or B. The invalid input has no queued ID. These letters describe a local test fixture, not destinations to submit.

Check that the number of IDs matches queued_count when that field is present. Do not manufacture IDs to fill a difference. If status lookup is temporarily unavailable, leave the association pending and retry the lookup; do not resubmit the SMS batch just to obtain a cleaner mapping.

Retry the submission without creating another batch

If the response was lost, keep the same endpoint, account, environment, request body and original idempotency key. Follow the retry rules above. A replay returns the stored acceptance result; it is not a fresh evaluation of which recipients you now want to reach. Correcting an invalid number or changing the message is a new, deliberately reviewed submission. Do not automatically resend all original recipients under a new key.

Keep delivery and money in separate columns

Retain the message ID, destination, delivery state and returned financial state. Where present, charge_status, customer_charge_amount, refund_amount and financial_review_required explain settlement. A failed delivery can still carry a sending charge. A refund pending is not a completed refund, and a missing financial field on an older response is not zero cost.

Related guides

Reviewed 12 October 2026 against the current IllyVoIP controls, implementation and applicable references. Available options depend on account permissions and service state.