Send SMS With a Python API Example: A Complete requests Walkthrough
A hands-on send SMS with Python API example using the requests library: the exact endpoint and parameters, a reusable function, reading the JSON status, real responses captured from a live server, and safe secret storage.
This is a practical send SMS with Python API example that you can copy, run, and adapt today. By the end you will have a small, reusable Python function built on the requests library that posts to the SharkSMS send endpoint, reads the JSON status the server returns, and handles the errors you will actually hit in production. Every request and response below was executed against a running SharkSMS server, so the output you see is real, not idealised. That includes the honest part: on a fresh install with no active plan and no paired phone, the API tells you exactly what is missing, and this guide shows that response and how to move past it.
If you want the endpoint reference open in another tab while you read, the SharkSMS SMS API page lists the authentication, parameters, and permissions in one place. If the underlying idea of a web-service SMS API is new to you, our explainer on what a web-service SMS API is sets the context this tutorial builds on.
What you need before the first call
An SMS API turns a piece of text and a phone number into a real message by handing it to something that can actually send: in SharkSMS that is a paired Android phone acting as a gateway, or a credit-based third-party gateway. Your Python code never sends the SMS itself. It describes the message, proves who is asking with an API key, and posts that to an endpoint. For a send to complete end to end you need three things in place:
- An API key created in the dashboard under Tools, in the API keys section. The key must carry the
sms_sendpermission scope. - An active subscription whose plan includes the SMS service. Without it, the send endpoint refuses the request even when your key is valid.
- A paired Android gateway device (or a configured credit gateway) to physically send the message. Our Android SMS gateway page covers pairing a phone.
We will call the endpoint before all three are satisfied on purpose, because seeing the real error responses is the fastest way to understand what each prerequisite does.
The endpoint and its parameters
The send endpoint lives at /api/send/sms on your SharkSMS installation. It expects an HTTP POST with form-encoded fields. These are the fields the SMS send path reads:
| Parameter | Required | Purpose |
|---|---|---|
secret | Yes | Your API key. Authenticates the request. |
mode | Yes | devices to send through your own paired phone, or credits to route through a gateway. |
phone | Yes | Recipient in full international E.164 format, for example +14155552671. Invalid numbers are rejected before anything is queued. |
message | Yes | The text body. It must satisfy the configured minimum and maximum length. |
device | In devices mode | The id of the paired device that should send. |
gateway | In credits mode | The gateway id (or global device) to route through. |
sim | Optional | 1 or 2 to pick a SIM slot on a dual-SIM phone. Defaults to 1. |
priority | Optional | Integer that pushes a message ahead in the queue. |
The response is always JSON with three fields: a numeric status, a human-readable message, and a data field. Worth knowing up front: the app returns HTTP 200 on the transport layer for its own responses and puts the real outcome in that status field, so your Python code must read the body, not just the HTTP status line.
Your first request with requests
The requests library is the standard way to make HTTP calls in Python. Install it with pip install requests, then a minimal send looks like this:
import requests
BASE_URL = "https://your-sharksms-site.com"
API_SECRET = "YOUR_API_SECRET"
response = requests.post(
f"{BASE_URL}/api/send/sms",
data={
"secret": API_SECRET,
"mode": "devices",
"phone": "+14155552671",
"message": "Your code is 4472. It expires in 10 minutes.",
"device": "1",
},
timeout=15,
)
print(response.status_code)
print(response.json())
Passing your fields through the data= argument tells requests to form-encode them, which is exactly what this endpoint expects. The timeout is not optional in practice: without it a stalled network call can hang your program indefinitely. The official requests documentation covers these arguments in detail.

A reusable send function
Inline code is fine for a first test, but you will want a single function you can call from anywhere in your application. This version reads the secret from the environment (more on that below), returns the parsed JSON, and lets transport failures surface as exceptions so the caller can decide what to do:
import os
import requests
BASE_URL = os.environ.get("SHARKSMS_BASE", "https://your-sharksms-site.com")
API_SECRET = os.environ.get("SHARKSMS_SECRET")
def send_sms(phone, message, device, sim=1, timeout=15):
"""Send one SMS through a paired Android gateway device.
Returns the parsed JSON dict from the SharkSMS API.
Raises requests.RequestException on a transport-level failure.
"""
url = f"{BASE_URL}/api/send/sms"
payload = {
"secret": API_SECRET,
"mode": "devices",
"phone": phone,
"message": message,
"device": device,
"sim": sim,
}
response = requests.post(url, data=payload, timeout=timeout)
response.raise_for_status()
return response.json()
The call itself is now a single readable line anywhere in your codebase:
result = send_sms(
phone="+14155552671",
message="Your code is 4472. It expires in 10 minutes.",
device="1",
)
Reading the JSON status and handling errors
Because the meaningful outcome is inside the response body, the pattern is: parse the JSON, branch on the status field, and treat 200 as accepted. Here is the caller with real error handling around it:
import json
import requests
try:
result = send_sms(
phone="+14155552671",
message="Your code is 4472. It expires in 10 minutes.",
device="1",
)
except requests.RequestException as exc:
print(f"Network error talking to the API: {exc}")
raise SystemExit(1)
status = result.get("status")
if status == 200:
print("Queued:", result.get("data"))
else:
print(f"API returned status {status}: {result.get('message')}")
print(json.dumps(result, indent=2))
The status codes this endpoint returns map cleanly onto how you should react:
| status | Meaning | What to do |
|---|---|---|
200 | Message accepted and queued for sending. | Store the returned id and move on. |
400 | Missing or invalid parameters, or a bad phone number. | Fix the request; do not retry unchanged. |
401 | Invalid API secret. | Check the key; regenerate if leaked. |
403 | Key lacks the scope, or the plan lacks the SMS service. | Grant sms_send and subscribe to a plan with SMS. |
404 | The named device does not exist. | Pair a phone and use its device id. |
500 | Server-side configuration or gateway error. | Retry later; check the server. |
What this actually returns: an honest run
Here is where an example earns its keep. I ran the reusable function above against a live SharkSMS server, with a real, valid API key, pointed at a genuine mobile number. The account had no plan with the SMS service enabled yet, and the server answered:
API returned status 403: Subscription has no permission to use SMS services!
{
"status": 403,
"message": "Subscription has no permission to use SMS services!",
"data": false
}
That is the truth of a fresh setup, and it is useful. A 403 here is not a bug in your code. The request was well formed, the key authenticated, and the server rejected it at the permission gate because the account is not subscribed to a plan that includes SMS. The next gate is the device: once an SMS-enabled plan is active but no phone is paired, the very same call returns {"status": 404, "message": "Device doesn't exist!"}. In other words, a valid "queued" success on a brand-new instance is not something you can fake into existence. You reach it by completing the two prerequisites the errors are pointing at.
This is why the error-handling branch above matters more than the happy path. During setup you will spend most of your time reading 403 and 404 messages, and each one tells you precisely which prerequisite is still missing.

Keep the secret out of your code
Notice that the reusable function read the key from os.environ rather than a hard-coded string. This is deliberate and it is the single most important habit in this whole example. An API key is a credential. If it appears in your source, it ends up in version control, in build logs, and in every copy of the repository. Instead, set it in the environment:
# macOS or Linux
export SHARKSMS_SECRET="your_real_api_key"
export SHARKSMS_BASE="https://your-sharksms-site.com"
# Windows PowerShell
$env:SHARKSMS_SECRET = "your_real_api_key"
$env:SHARKSMS_BASE = "https://your-sharksms-site.com"
Then read it once at startup and fail fast if it is missing, so a misconfiguration is loud rather than silent:
API_SECRET = os.environ.get("SHARKSMS_SECRET")
if not API_SECRET:
raise SystemExit("Set SHARKSMS_SECRET in the environment before sending.")
For local development a .env file loaded by a small library keeps the same shape without exporting variables by hand, and it should be listed in .gitignore. If a key ever leaks, delete it in the dashboard and issue a new one; because your code reads from the environment, rotating the key is a config change, not a code change.
No requests? Use the standard library
If you cannot install third-party packages, the same call works with nothing but Python's standard library using urllib. It is more verbose, but it ships with every Python install:
import json
import os
import urllib.parse
import urllib.request
import urllib.error
BASE_URL = os.environ.get("SHARKSMS_BASE", "https://your-sharksms-site.com")
API_SECRET = os.environ.get("SHARKSMS_SECRET")
payload = urllib.parse.urlencode({
"secret": API_SECRET,
"mode": "devices",
"phone": "+14155552671",
"message": "Your code is 4472. It expires in 10 minutes.",
"device": "1",
}).encode()
request = urllib.request.Request(f"{BASE_URL}/api/send/sms", data=payload)
try:
with urllib.request.urlopen(request, timeout=15) as resp:
result = json.loads(resp.read().decode())
except urllib.error.URLError as exc:
raise SystemExit(f"Network error: {exc}")
print(result.get("status"), result.get("message"))
The urllib.request module is documented on python.org, and if you want to understand the HTTP methods and status codes underneath all of this, the MDN HTTP reference is the clearest general source. Functionally this urllib version and the requests version send the identical POST and read the identical JSON; pick whichever fits your project's dependency policy.

Completing a real send
To turn that 403 into a queued message, complete the two prerequisites the errors point at. First, make sure your account is on a plan that includes the SMS service; that clears the subscription gate. Second, pair an Android phone as a gateway and use its device id in the device parameter; that clears the device gate. With both in place, the same function returns a 200 and a data payload containing the id of the queued message, which you store against the order or booking it relates to.
Keep in mind that a 200 means accepted for sending, not delivered. The physical send happens on a real handset over a real mobile network, so a large batch should be paced and the phone kept online and out of aggressive battery optimisation. Prefer the devices mode when you own the SIM and want the lowest cost per message. If you would rather compare the Python approach with a server-side language, our companion walkthrough on how to send SMS with PHP and an Android gateway uses the same endpoint from PHP. For the wider picture of where teams wire this in, the use cases page is a good tour.

Putting it together
A dependable send SMS with Python API example comes down to a short, repeatable shape: build a form-encoded POST to /api/send/sms, authenticate with a secret read from the environment, choose devices mode with a paired phone, and branch on the JSON status in the response body. Wrap that in one reusable function, add a try around the network call, and log the full JSON while you build. The errors you meet along the way are not obstacles to route around; they are the checklist. A 403 says subscribe to an SMS plan and scope the key, a 404 says pair a device, and a 400 says fix the number or the parameters. Clear those and the next response is the 200 you were after.
Ready to run this against your own account? Open the SharkSMS SMS API documentation, create a key with the sms_send scope, pair a phone, and post the example above to a number you control. The moment your plan and device are in place, the same Python function starts returning queued message ids.
Comments
No comments yet. Be the first.
Sign in to comment
Comments come from SharkSMS accounts, so you always know who you are reading. Creating one is free and takes a minute.