Integrating an SMS gateway with Python is straightforward until the first message goes missing, and then the design decisions made in the first hour determine whether it can be found. The code is short; the reconciliation design is what takes thought.
This guide covers choosing between the HTTP API and a protocol-level interface, sending and interpreting responses, the webhook-versus-polling decision for inbound traffic, idempotency, reconciliation against the device log, and the error handling that keeps a failed send from becoming a lost message.
To integrate an SMS gateway with Python, should you use the HTTP API or SMPP?
HTTP for integration speed, SMPP for throughput and control.
An HTTP API is simpler to build against and easier to secure with standard tooling, while a session-based protocol gives lower overhead per message and finer control over the submission window.
The trade shows up at different scales. A few thousand messages a day fits comfortably in an HTTP integration built with a well-tested client library, and the operational cost is close to nothing. At much higher volumes the per-message overhead and the connection handling become relevant, and the session-based interface earns its additional complexity. The short message framework that both ultimately use is defined in ETSI TS 123 040 and 3GPP TS 23.040, and reading the status semantics there is more useful than relying on what a client library chooses to surface.
Whichever you choose, keep the choice behind an interface in your own code. A small abstraction that exposes send, status and reconcile operations means the transport can be changed later without rewriting the parts of the application that decide what to send. That decision is worth an hour at the start and saves a week later.
| Interface | Best fit | Main cost |
|---|---|---|
| HTTP API | Moderate volume, fast integration | Per-message overhead, connection handling |
| Session protocol | High volume, precise control | Session lifecycle and window management |
| Both | Bulk on one path, control on the other | Two code paths to keep consistent |

Sending a message and handling the response
Treat the response as the beginning of the record.
The submission response tells you whether the message was accepted, and storing it against your own identifier is what makes everything afterwards traceable.
Record four things at submission: the client reference you generated, the identifier the platform returned, the outcome, and the timestamp. Serialising that record is a single call, and the standard library module documented at Python JSON support is sufficient for it. What matters is that the write happens before the next message is submitted, so a process crash cannot lose the association between the message and its identifier.
Handle the timeout case explicitly. A request that times out has an unknown outcome, and treating it as a failure is the most common way duplicates are created. The safe default is to record the message as unknown, look it up later by client reference, and only resend when the lookup confirms it never arrived. A synchronous HTTP client that blocks the whole worker on a slow response is also worth avoiding; the asynchronous concurrency primitives documented at Python asyncio let several sends be outstanding without blocking, which keeps throughput predictable under load.
Webhook or polling for inbound?
Webhooks for latency, polling for simplicity.
A webhook delivers inbound messages and receipts as they arrive, while polling retrieves them on a schedule at the cost of delay and repeated requests.
The decision is usually made by the network topology rather than by preference. A webhook requires the gateway to be able to reach your service, which means either a public endpoint or a tunnel, and it requires the endpoint to be available whenever the gateway sends. Polling inverts that dependency: your service initiates, and the gateway does not need to reach it at all.
Where both are available, a common arrangement is to use webhooks for receipts and polling as a reconciliation sweep. The webhook gives low latency in normal operation, and the periodic sweep catches anything that was missed while the endpoint was unavailable. The two mechanisms must deduplicate against each other, which brings the next requirement into play.
Idempotency and duplicate suppression
One identifier, stored once, checked before every action.
Every message needs an identifier that your system generates and keeps, because identifiers from other layers are not guaranteed to be returned unchanged or to be unique across restarts.
Suppression works on two sides. On the outbound side, the record of the client reference prevents a retry loop from submitting the same message twice. On the inbound side, the same reference or the platform identifier prevents a webhook and a polling sweep from both processing the same receipt. A unique constraint in the datastore is a more reliable mechanism than application logic, because it holds under concurrent workers; the standard library interface described at Python database access is adequate for a single-node integration and demonstrates the pattern clearly.
The failure this prevents is worse than it looks. A duplicate delivery to a recipient is visible to the customer, and it is one of the few integration defects that a business notices without being told. Paying for the constraint up front is cheap relative to the conversation it avoids.
Two habits make the integration easier to operate. Keep the configuration in one place, so the endpoint, credentials and retry policy are not scattered across modules that each grew an opinion about them. And emit a periodic summary of message states, because a leak between states — messages created but never sent, or sent but never resolved — shows up as a trend long before it shows up as a complaint from a customer.

Delivery reconciliation from the device log
Compare your record with the device’s, on a schedule.
The gateway keeps its own record of submission and outcome, and comparing it with your application record on a schedule finds the messages that fell between the two.
Reconciliation is what turns an integration from hopeful into auditable. Run it as a periodic job that reads the device record for a window and looks for messages your application has no outcome for, and for outcomes your application has no message for. The first case identifies lost receipts; the second identifies duplicates or mis-attributed records. Neither can be found by looking at either system alone.
Two details make reconciliation practical at volume. First, bound the window: comparing a rolling hour against the device record is manageable, while comparing a month is a batch job with its own failure modes. Second, normalise before comparing, so that a recipient number stored in one system with an international prefix and in the other without it does not appear as a mismatch. Both problems are cheap to avoid and expensive to debug once the job is running unattended and producing thousands of false discrepancies.
Log the reconciliation result rather than only acting on it, since the trend is often more informative than the individual discrepancy. A rising count of unmatched records points at a cause upstream — a changed response format, a new submission path, a clock difference — long before it produces a visible failure. Guidance on what to keep in an operational log and for how long is set out in NIST SP 800-92, and Python’s own logging facilities, documented at Python logging, are enough to emit it consistently.
Error handling that does not lose messages
Write the intent before attempting the send.
A message should be recorded before the submission is attempted, so that a crash or a restart leaves evidence of work that may need to be repeated.
The order — write, attempt, update — is what makes the system recoverable. A process that submits first and records afterwards has a window in which a message exists only in flight, and everything in that window is lost when the process restarts. The cost of the order is an occasional duplicate attempt, which the identifier constraint suppresses; the alternative cost is silent loss.
Classify errors rather than treating them uniformly. Connection failures and timeouts are retryable with backoff. Explicit rejections of the message are usually not, and retrying them wastes capacity. Anything ambiguous belongs in a parked state with its evidence, to be resolved by a rule or a person rather than by a loop. That third category is the one most integrations lack, and it is where unexplained losses accumulate.
A minimal project structure
The structure below is deliberately small enough to hold in one repository and to explain in a design review. It separates the transport, the message lifecycle and the reconciliation so that a failure in one does not require rewriting the others, and it assumes a client-generated identifier and a durable store are present from the first commit. Add to it only when a specific requirement demands it.
- A transport module that hides whether the API or a session protocol is used.
- A store that holds the message, its client reference and its current state.
- A sender that writes first, submits second and updates third.
- A receiver that deduplicates inbound records against the same store.
- A reconciler that compares the store with the device log on a schedule.
- A reporter that emits counts per state, so anomalies appear as trends.
That structure fits in a few hundred lines and covers the failure modes that cause most production incidents in messaging integrations. Adding features later is easy; retrofitting reconciliation into a design that assumed success is not.
Two more habits pay for themselves quickly: keep the credentials out of the code and in the environment, and make the store the source of truth for what was sent. Both are one-line decisions at the start and awkward migrations later.
Design the record before the retry logic. Send your interface choice, throughput target and reconciliation window to service@telarvo.com, or review the published configurations on the SK-SMS Gateway range and the SMS gateway solution page. Telarvo publishes the SIMBANK and SIMPOOL ranges on its product pages, and the configurations referenced above come from those listings.
FAQ
Is there a free way to send SMS from Python?
Free paths exist and generally trade reliability or consent for cost, which is rarely a good fit for business messaging. Where the deployment already includes gateway hardware, the marginal cost of a message is the tariff behind the SIM, and the integration effort is the same either way. Choose the path that produces evidence of delivery. For a design comparison, the deciding factor is usually the audit trail rather than the price.
Should the client library be allowed to retry automatically?
Only with an idempotency key, and preferably not at all. A library that retries transparently on timeout can resend a message whose first submission succeeded, producing a duplicate the application never sees. Keep retries in your own code where the identifier and the record are visible. Review what the library does on timeout before adopting it, because the default behaviour is rarely documented clearly.
What happens if the process restarts mid-campaign?
If the design writes before submitting, the restart leaves a set of messages in a known state and the sender can resume from the store. If it submits first and records afterwards, every message in flight at the moment of the restart is lost with no evidence that it existed. The ordering is the whole difference. A store that survives a restart is what makes the reconciliation possible later.
How often should reconciliation run?
Frequently enough to catch a format change before it becomes a campaign-wide loss, which in most deployments means hourly during active sending and once daily otherwise. Keep the results as a series rather than as a single figure, because the trend identifies the cause earlier than the count does. Alert on a change in shape rather than on a threshold alone.