SMS Sending API
The SMS sending API lets you send messages under your own sender name directly from your system. You can send a single message, such as an order notification or a code, or a batch of up to 1000 messages in one request. Pricing and billing are the same as for sending from the panel.
This guide describes API v2 (/api/v2/sms). The previous API v1 (/api/v1/sending/{key}) still works. See Migrating from API v1 for the differences.
Before you start
- The SMS Sending service is active on your account in the panel.
- You have at least one approved sender (status
active). Sender names (up to 12 characters) are registered in the panel under Senders. - You have funds: a prepaid balance or free postpaid limit.
API key
You will find the key in the panel: SMS Sending → API access. Send it in the header of every request:
Authorization: Bearer YOUR_KEY
The base URL is https://panel.dpay.pl/api/v2/sms. Request bodies are JSON objects (Content-Type: application/json).
Anyone who knows the key can send SMS at your cost. Use it only server-side. Never put it in a mobile app or in website code.
Endpoints
POST/api/v2/sms/messagesSend one message.Full contract in the API Reference
GET/api/v2/sms/messages/{id}Message status.Full contract in the API Reference
POST/api/v2/sms/batchesSend a batch of up to 1000 messages.Full contract in the API Reference
GET/api/v2/sms/batches/{id}Batch status and the result of every message.Full contract in the API Reference
GET/api/v2/sms/sendersYour account's senders and their status.Full contract in the API Reference
Single message
curl -X POST "https://panel.dpay.pl/api/v2/sms/messages" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-shipped" \
-d '{"msisdn":"600100200","sender":"SKLEP","message":"Twoje zamówienie zostało wysłane."}'
| Field | Required | Description |
|---|---|---|
msisdn | yes | Recipient number, see Recipient number. |
sender | yes | An approved sender name of your account. |
message | yes | Text, up to 900 bytes, see Text and segments. |
A 202 Accepted response means we accepted the message to the queue and reserved its cost. It does not mean delivery - the message is handed over to the operator in the background.
{
"batch_id": null,
"id": "9b2f4c1e-6f0a-4d7e-9a41-3c8f2d7b5e10",
"msisdn": "600100200",
"sender": "SKLEP",
"status": "queued",
"segments": 1,
"cost_net": "0.081300",
"created_at": "2026-10-14T12:00:00+02:00",
"finished_at": null
}
Keep the id - GET /api/v2/sms/messages/{id} tells you how the hand-over ended. The same URL is in the Location response header.
Bulk sending
A batch holds up to 1000 messages in one request. The batch-level sender is the default sender, and every message can set its own.
curl -X POST "https://panel.dpay.pl/api/v2/sms/batches" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-2026-10" \
-d '{
"name": "October campaign",
"sender": "SKLEP",
"messages": [
{"msisdn": "600100200", "message": "Wiadomość 1"},
{"msisdn": "+48600100201", "message": "Wiadomość 2 od innego nadawcy", "sender": "INNY"}
]
}'
A batch is accepted as a whole or not at all.
- If even one message is invalid, you get
422and nothing is accepted. Theerror.fieldsobject points at every invalid message, e.g.messages.1.msisdnis the number of the second message (counting from zero). - If your funds do not cover the whole batch, you get
402.
An accepted batch returns 202 with status pending and a price quote in estimate. Messages are created in the background, usually within a few seconds - that is when the cost is reserved.
{
"id": "3d6a0f5e-2b7c-4e19-8f3a-1c9d5e7b2a40",
"name": "October campaign",
"status": "pending",
"failure_reason": null,
"messages_count": 2,
"counts": {"queued": 0, "sending": 0, "sent": 0, "failed": 0, "unknown": 0},
"segments": 0,
"cost_net": null,
"created_at": "2026-10-14T12:00:00+02:00",
"updated_at": "2026-10-14T12:00:00+02:00",
"messages": [],
"estimate": {"segments": 2, "cost_net": "0.162600"}
}
Batches sent through the API also appear in the panel under Import, and their messages under History and reports.
Batch progress and results
curl "https://panel.dpay.pl/api/v2/sms/batches/3d6a0f5e-2b7c-4e19-8f3a-1c9d5e7b2a40" \
-H "Authorization: Bearer YOUR_KEY"
The response contains message counts per status and the result of every message in request order (index):
{
"id": "3d6a0f5e-2b7c-4e19-8f3a-1c9d5e7b2a40",
"status": "completed_with_errors",
"counts": {"queued": 0, "sending": 0, "sent": 1, "failed": 1, "unknown": 0},
"segments": 2,
"cost_net": "0.081300",
"messages": [
{"index": 0, "id": "5a1e9c2d-7b3f-4a8e-b6d4-0f2c9e1a7b35", "msisdn": "600100200", "sender": "SKLEP", "status": "sent", "segments": 1, "cost_net": "0.081300"},
{"index": 1, "id": "c7d2b8e4-1f6a-4c3d-9e5b-8a0f2d4c6e19", "msisdn": "600100201", "sender": "INNY", "status": "failed", "segments": 1, "cost_net": "0.000000"}
]
}
| Batch status | Meaning |
|---|---|
pending | Accepted, waiting to be processed. The messages list is still empty. |
processing | Messages created, some are still queued or being handed over. |
completed | All messages handed over to the operator (sent). |
completed_with_errors | Finished, but some messages are failed or unknown. |
rejected | Rejected while processing. Nothing was sent or charged - the reason is in failure_reason. |
A batch can still be rejected after acceptance if the account changes in the meantime:
failure_reason | What happened | What to do |
|---|---|---|
insufficient_funds | Another sending used up the funds between acceptance and processing. | Top up and send the batch again. |
sender_inactive | A sender used in the batch is no longer active. | Check your senders in the panel. |
account_unavailable | SMS sending is no longer available on the account. | Check the service status in the panel. |
invalid_batch, processing_failed | The batch could not be processed. | Send it again; if it repeats, contact support. |
You can send a rejected batch again, also with the same Idempotency-Key.
Message statuses
| Status | Meaning |
|---|---|
queued | Accepted, waiting in the queue. |
sending | Being handed over to the operator. |
sent | The operator accepted the message. This is not a delivery receipt from the recipient's phone. |
failed | The operator rejected the message. It is not charged (cost_net = 0.000000). |
unknown | The hand-over result could not be confirmed. The amount stays reserved until it is clarified - contact support if in doubt. |
Idempotency
Networks fail: a request may arrive while its response is lost. To make sure a retry does not send the SMS twice, add an Idempotency-Key header with your own unique key (1-128 printable ASCII characters, case-sensitive), e.g. your order number.
- A retry with the same key and the same body returns the original result instead of sending again. The response then has the
Idempotent-Replayed: trueheader. - The same key with a different body is a
409 idempotency_conflicterror. - A batch key covers the whole request body, including
name. - A request rejected by validation, missing funds or an inactive account does not use up the key. The key of a batch rejected while processing (
rejected) can be used again, too.
Recipient number
msisdn is a Polish number in one of the formats: 600100200, +48600100200, 0048600100200 or 48600100200. Spaces and hyphens are ignored. Responses always return the number as 9 digits.
Text and segments
The text can be up to 900 bytes in UTF-8. It is sent exactly as received - we do not strip spaces or line breaks.
A long message is split into segments, and the charge is the number of segments times your account's rate:
| Encoding | 1 segment | When split, per segment |
|---|---|---|
| GSM-7 (Latin letters without Polish characters, digits, basic symbols) | 160 characters | 153 characters |
| Unicode | 70 characters | 67 characters |
A single character outside the GSM-7 alphabet, e.g. ą, ę or ł, switches the whole message to Unicode. A 100-character message fits in one segment without Polish characters, but takes two with them.
The number of segments and the price of every message are returned in segments and cost_net (net amount in PLN). They are calculated by the same calculator as sending from the panel.
Errors
Every error has the same shape: a stable code to handle in your code and a message for humans.
{
"error": {
"code": "validation_failed",
"message": "Żądanie zawiera niepoprawne dane.",
"fields": {
"messages.1.msisdn": ["Numer odbiorcy to dziewięć cyfr, opcjonalnie z prefiksem +48."]
}
}
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json | The request body is not a JSON object. |
| 401 | unauthenticated | Missing or invalid key. |
| 402 | insufficient_funds | No balance or free limit (for a batch: for the whole batch). |
| 403 | account_unavailable | SMS sending is not active on the account. |
| 404 | not_found | No such message or batch on your account. |
| 409 | idempotency_conflict | Idempotency-Key already used for a different body. |
| 422 | validation_failed | Invalid fields - details in fields. Also covers an inactive sender. |
| 429 | rate_limited | Request limit exceeded. Retry after the time in the Retry-After header. |
Limits
- 120 requests per minute per API key. To send many messages, use batches instead of single requests in a loop.
- 1000 messages per batch and 900 bytes per message.
- A burst of failed authentication attempts from one address blocks further requests from that address for the rest of the minute (
429).
Migrating from API v1
API v1 still works, but new integrations should use v2.
| API v1 | API v2 | |
|---|---|---|
| Key | in the URL: /api/v1/sending/{key} | in the Authorization: Bearer header |
| Message fields | number, override, message | msisdn, sender, message |
| Bulk sending | /{key}/mass, a JSON list, message by message | /batches, a JSON object, the whole batch processed in the background |
| Errors | 200 with error|... for each list item | HTTP codes and error.code, invalid items pointed at in fields |
| Message status | none | GET /messages/{id}, GET /batches/{id} |
| Retries | no duplicate protection | Idempotency-Key |
The API key is the same - you do not need a new one.