SMS (prepaid)
SMS on SendSeven is prepaid. Every SMS has two parts, and they are paid in two different ways:
| Part | What it is | How it is paid |
|---|---|---|
| Provider fee | The carrier network cost for the destination country, charged per segment | Drawn from the workspace's prepaid SMS balance when the SMS is sent |
| Your fee | The SendSeven message rate of your plan, charged once per message, however many segments it has | Counts against your plan's included message pool, then billed on your monthly invoice like any other message |
So an SMS needs credit on the SMS balance to go out at all, and it also counts as a SendSeven message on your plan. Inbound SMS (messages your customers send you) is free and never touches the balance.
SMS is available on paid accounts after SMS access has been requested and approved in the app (Settings > Connected Channels > SMS). Trial accounts can't send SMS.
The SMS balance
One balance per workspace
The SMS balance belongs to the workspace that owns the SMS channel. Every workspace with SMS has its own balance; workspaces on the same billing account do not share it. (This is different from the RCS wallet, which belongs to the billing account.)
All SMS channels (senders) of a workspace draw from the same balance.
There is no minimum balance
You can spend the balance down to €0.00. A send is blocked only when the balance is used up (see When the balance reaches €0). SendSeven does not keep a safety margin on top.
Top-ups
You add credit with a top-up in the app, on the SMS channel page (Settings > Connected Channels > SMS). Top-ups are not available through the public API.
| Rule | Value |
|---|---|
| First top-up (during SMS setup) | At least €20 net |
| Later manual top-ups | At least €5 net |
| Payment | Card only (SEPA Direct Debit is not used for SMS top-ups) |
| VAT | Added on top where it applies; each top-up gets its own paid invoice |
| Credit added | The net amount |
Credit usually appears within seconds. Sometimes part of it arrives a few minutes later. Until then, pending_transfer_cents on GET /sms/balance shows the amount still on its way. You are charged once, and the full amount always arrives.
The fee for a dedicated phone number (monthly or yearly) is charged separately to your card. It is never taken from the SMS balance.
Auto top-up
Auto top-up charges a saved card automatically when the balance falls below a threshold you choose.
| Setting | Rule |
|---|---|
| Amount per top-up | Between auto_topup_min_amount_cents and auto_topup_max_amount_cents net (currently €20 to €100), returned by GET /sms/balance |
| Threshold | Lower than the amount |
| Payment | A saved card; each automatic top-up is a normal top-up with its own invoice |
| Limits | At most three automatic attempts per day, a few minutes apart, and none while earlier credit is still on its way |
If the card is declined or needs a 3D Secure confirmation that can't be completed without you, nothing is charged, auto top-up is switched off, and the workspace owners and admins get an email and an in-app notification. Fix the card and switch auto top-up on again.
Changing auto top-up charges a card without anyone present, so it needs the billing:update permission (the Owner and Billing roles). Others see the settings read-only; auto_topup_can_edit on GET /sms/balance tells you which case applies to the caller.
Low-balance warnings
When the balance drops below the warning threshold (€2.00 by default), the workspace owners and admins get an email and an in-app notification, at most once a day. Set your own threshold with PUT /sms/low-balance-threshold.
What an SMS costs
Segments
An SMS is sent in segments. A long message is split into several segments, and the carrier joins them on the phone. The provider fee is charged per segment, so a two-segment message costs twice the provider fee of a one-segment message. Your fee is charged once per message, whatever its length.
cost of one SMS = provider fee × segments + your fee
For example, a three-segment SMS with a provider fee of €0.07 and a fee of €0.03 costs €0.21 from the SMS balance plus one message (€0.03) on your plan: €0.24 in total, not €0.30.
How many characters fit in a segment depends on the encoding, which SendSeven picks automatically from the text:
| Encoding | When | One segment | Each segment of a longer message |
|---|---|---|---|
| GSM-7 | Every character is in the GSM-7 alphabet (Latin letters, digits, common punctuation, some accented letters such as ä ö ü é) | up to 160 characters | 153 characters |
| Unicode (UCS-2) | At least one character is outside GSM-7, for example an emoji, ł, ş, Cyrillic or Chinese | up to 70 characters | 67 characters |
- These GSM-7 characters count as two:
€ [ ] { } \ ^ ~ |and the form feed. - One emoji or other non-GSM-7 character switches the whole message to Unicode, which more than halves the room per segment.
- An SMS campaign with the opt-out link enabled appends a short unsubscribe link to every message. It counts towards the length.
Destination country
The provider fee depends on the destination country (the recipient's number). Your fee is the per-message rate of your plan. Both are shown in the app before you send: on the SMS card in Settings > Connected Channels, in the segment counter while you write, and in the campaign cost estimate. For the SendSeven plan prices, see sendseven.com/pricing.
To get the numbers in code, use GET /sms/message-pricing for one recipient and text, or GET /sms/pricelist for every country.
The plan's message pool
Every outbound SMS that was sent or delivered counts as one SendSeven message on your plan. It uses your plan's included message pool first (the pool is shared by all workspaces on the billing account), and anything beyond the pool is billed at your plan's message price on the monthly invoice. Some plans have a separate SMS message price; the invoice then shows an SMS messages line instead of counting SMS under Messages.
The pool covers only your SendSeven fee. The provider fee is always paid from the SMS balance, also while the pool still has room.
Not billed: inbound SMS, and SMS that ended as failed (including SMS blocked for a used-up balance).
The month's usage across channels, including SMS, is described in the Billing overview; per-workspace numbers are in Multi-workspace usage.
When the balance reaches €0
Single sends (POST /messages)
The send request itself does not fail. POST /messages accepts the SMS (201) and sending happens a moment later. When the balance is used up at that point, the message ends as failed instead of being sent:
{
"id": "0b7c3e9a-4f1d-4a8e-9c2b-5d6e7f8a9b0c",
"platform": "sms",
"direction": "outbound",
"status": "failed",
"error_code": "insufficient_sms_balance",
"error_message": "[ERR:sms_insufficient_balance:0.00:0] SMS send blocked: balance 0.00 EUR is used up (floor 0 EUR). Top up your SMS credit to resume sending."
}
- Read the outcome from the message (
GET /api/v1/messages/{message_id}):statusisfailedanderror_codeisinsufficient_sms_balance. Useerror_codein code. The bracketed prefix oferror_messageis meant for display and may change. - The same result appears when the balance runs out between the check and the send and the carrier side refuses for missing credit.
- The message is not retried and not sent automatically after a top-up. Send it again once there is credit.
- A blocked SMS is never billed.
- The
message.failedwebhook fires for it, once. - The workspace owners and admins get a "sends blocked" email and in-app notification (at most once a day).
- Receiving SMS keeps working at €0.
Other SMS send failures you can see in error_code:
error_code | Meaning |
|---|---|
insufficient_sms_balance | The SMS balance is used up |
sms_not_provisioned | The SMS setup of the workspace is not finished |
sms_duplicate_send | The same text went to this recipient within the last 3 minutes |
sms_daily_limit_reached | The daily limit for this recipient is reached; try again tomorrow |
The message.failed webhook
A message that fails while it is being sent, a blocked SMS included, triggers message.failed, with the same payload as a failure the carrier reports later. The reason is in data.error, and data.message.meta.last_failed_error.errors[] holds the structured error. The code there is the same value as the message's error_code:
{
"type": "message.failed",
"data": {
"message": {
"id": "0b7c3e9a-4f1d-4a8e-9c2b-5d6e7f8a9b0c",
"platform": "sms",
"direction": "outbound",
"status": "failed",
"error_code": "insufficient_sms_balance",
"failed_at": "2026-10-23T09:15:02Z",
"meta": {
"last_failed_error": {
"captured_at": "2026-10-23T09:15:02Z",
"errors": [
{
"code": "insufficient_sms_balance",
"message": "[ERR:sms_insufficient_balance:0.00:0] SMS send blocked: balance 0.00 EUR is used up (floor 0 EUR). Top up your SMS credit to resume sending."
}
]
}
}
},
"error": "[ERR:sms_insufficient_balance:0.00:0] SMS send blocked: balance 0.00 EUR is used up (floor 0 EUR). Top up your SMS credit to resume sending."
}
}
The example is shortened; the full event also carries conversation, contact and contact_method like every message event (see Webhook events). Each failed message triggers message.failed once: a send that fails and then succeeds on an automatic retry triggers none, and a duplicate delivery inside SendSeven does not trigger a second one. last_failed_error is also stored on the message, so GET /messages/{message_id} shows it.
Contact verification codes (POST /verifications)
A verification code sent by SMS is checked right away. With no credit, POST /verifications answers 402:
{
"detail": {
"error": "...",
"code": "insufficient_sms_balance",
"error_code": "insufficient_sms_balance",
"message": "..."
}
}
See Contact Verification.
Campaigns
When an SMS campaign hits a used-up balance, SendSeven pauses the campaign instead of failing every remaining recipient:
- The recipient(s) whose send hit the empty balance end as
failedwitherror_codeinsufficient_sms_balance, and thecampaign.message.failedwebhook fires for them. (A campaign send that fails never becomes a conversation message, so campaigns report failures withcampaign.message.failed, notmessage.failed.) - The campaign moves to
statuspausedwiththrottle_reasoninsufficient_sms_balance. - Every recipient that was not sent yet stays pending. Nobody who already got the message is affected.
- The campaign creator gets an in-app notification, and the owners and admins get the low-balance alert.
The campaign does not continue on its own after a top-up. To finish it:
-
Top up the SMS balance.
-
Re-arm the failed recipients:
curl -X POST "https://api.sendseven.com/api/v1/campaigns/{campaign_id}/retry-failed" \
-H "Authorization: Bearer s7_api_your_token" \
-H "Content-Type: application/json" \
-d '{"error_codes": ["insufficient_sms_balance"]}'{"success": true, "id": "c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8", "requeued": 3, "paused": true}On a paused campaign this does not resume sending. It only puts the failed recipients back in line (
"paused": true). -
Resume the campaign with
POST /api/v1/campaigns/{campaign_id}/resume. The re-armed recipients and everyone still pending are sent.
Both calls need the campaigns:send scope. See Messaging Campaigns for retry-failed in detail (cool-down, other error codes).
Compare what the campaign takes from the SMS balance (provider fee × segments × recipients; your fee is not taken from the balance) with balance_eur before you send, or switch on auto top-up. A campaign paused for balance needs two calls to finish, and recipients wait until then.
Required scopes
| Action | Endpoint | Scope |
|---|---|---|
| Read the balance and its settings | GET /sms/balance | channels:read |
| Set the low-balance warning | PUT /sms/low-balance-threshold | channels:update |
| Estimate the price of one SMS | GET /sms/message-pricing | channels:read |
| Get the per-country price list | GET /sms/pricelist | channels:read |
Endpoints
All paths are relative to https://api.sendseven.com/api/v1. EUR amounts are returned as decimal strings.
Get the balance
GET /sms/balance
| Query parameter | Description |
|---|---|
force_refresh | true reads the balance live instead of the value cached for up to 5 minutes. Default false |
{
"balance_eur": "12.40",
"auto_topup_enabled": true,
"auto_topup_threshold_cents": 1000,
"auto_topup_amount_cents": 5000,
"last_checked_at": "2026-10-23T09:12:44Z",
"low_balance_warn_eur": "2.00",
"low_balance_warn_is_default": true,
"pending_transfer_cents": 0,
"auto_topup_available": true,
"auto_topup_has_payment_method": true,
"auto_topup_last_attempt": {
"status": "succeeded",
"amount_cents": 5000,
"created_at": "2026-10-20T07:03:10Z",
"failure_code": null
},
"auto_topup_min_amount_cents": 2000,
"auto_topup_max_amount_cents": 10000,
"auto_topup_can_edit": false
}
| Field | Description |
|---|---|
balance_eur | Current SMS balance of the workspace. "0.00" when SMS is not set up yet |
auto_topup_enabled, auto_topup_threshold_cents, auto_topup_amount_cents | Auto top-up settings (net, cents) |
low_balance_warn_eur | The warning threshold in effect; low_balance_warn_is_default is true when it is the default |
pending_transfer_cents | Paid top-up credit still on its way to the balance (net, cents) |
auto_topup_available | false while auto top-up can't be switched on |
auto_topup_has_payment_method | A card for auto top-up is saved |
auto_topup_last_attempt | The latest automatic top-up (status pending, succeeded or failed; failure_code for a declined card) or null |
auto_topup_can_edit | The caller may change auto top-up (billing:update) |
Set the low-balance warning
PUT /sms/low-balance-threshold
{"threshold_eur": 5.00}
threshold_eur is between 0 and 1000 EUR with at most 2 decimals. null goes back to the default. The response is the same object as GET /sms/balance.
Estimate the price of one SMS
GET /sms/message-pricing?phone=%2B4915112345678&text=Your%20order%20has%20shipped%20...
| Query parameter | Description |
|---|---|
phone | Recipient number in E.164. Either phone or country is required |
country | ISO 3166 alpha-2 code, for example DE |
text | Optional. With a text, the response adds encoding and segment count, and the totals cover all segments. Without a text, the totals are for a one-segment SMS |
append_opt_out_link | true prices the text as a campaign SMS with the opt-out link appended |
{
"country_iso": "DE",
"country_name": "Germany",
"prefix": "49",
"platform_fee_eur": "0.0650",
"sendseven_fee_eur": "0.0200",
"total_per_segment_eur": "0.0850",
"price_per_message_eur": "0.0850",
"encoding": "GSM-7",
"char_count": 210,
"segments": 2,
"carrier_fee_total_eur": "0.1300",
"sendseven_fee_total_eur": "0.0200",
"total_eur": "0.1500",
"is_supported": true,
"quote_required": false,
"source": "cache"
}
The amounts above are an example, not current prices.
| Field | Description |
|---|---|
platform_fee_eur | Provider fee per segment, taken from the SMS balance. The lowest across the country's networks, so a specific number can cost a little more |
sendseven_fee_eur | Your plan's message rate, charged once per message. It counts against the plan's message pool |
carrier_fee_total_eur | platform_fee_eur × segments: what this SMS takes from the SMS balance |
sendseven_fee_total_eur | Your fee for this SMS. The same as sendseven_fee_eur, whatever the segment count |
total_eur | carrier_fee_total_eur + sendseven_fee_total_eur: the estimated price of this SMS |
total_per_segment_eur, price_per_message_eur | Older fields, kept for compatibility. Both are platform_fee_eur + sendseven_fee_eur, the price of a one-segment SMS. Don't multiply them by segments; use total_eur |
is_supported | false for a country SendSeven does not send SMS to (quote_required is then true) |
Get the price list
GET /sms/pricelist
Returns countries[] (iso, name, prefix, platform_fee_eur, sendseven_fee_eur, total_per_segment_eur, is_supported), plus currency, supported_countries and last_updated. As above, platform_fee_eur is per segment, sendseven_fee_eur is per message, and total_per_segment_eur is the price of a one-segment SMS. A message with n segments costs platform_fee_eur × n + sendseven_fee_eur.
Next steps
- Billing overview: how your plan, the message pool and prepaid balances fit together
- Messaging Campaigns: retry failed recipients and resume
- Webhook events:
message.failed,campaign.message.failedand the other message events - Error Handling: error shapes and when to retry