The important choice for a short-lived password-reset SMS is template ownership, not a feature-count race. Keep the expiry rule, token lifecycle, and release history in the application; treat Twilio and Vonage as transport providers. Twilio usually has the stronger developer-tooling and integration story. Vonage is often more competitive for high-volume SMS. Neither conclusion is complete until the destination countries and carrier relationships have been checked.
The reset token is the authority. A delivery receipt says something about transport, not whether an account may be reset.
1. Who owns the reset copy and expiry policy?
An SRE-friendly boundary is simple: the application owns the template version, token hash, expiry timestamp, single-use check, and audit record. The SMS provider owns submission, carrier delivery, and delivery receipts. This keeps a provider-side template edit from silently changing a security-sensitive workflow.
I initially treated the provider receipt as the end of the process. I was wrong. That boundary was too wide. A receipt is an input to the application state machine, and the state machine still has to reject an expired or already-used token. The practical consequence is that a retry must reuse the same event identifier and idempotency key. A timeout can otherwise turn one reset request into two messages, leaving an on-call engineer to explain a duplicate that the application could have prevented.
The token wins.
There is a trade-off. Provider-managed templates can help with local registration requirements, while application-owned templates make localization, review, rollback, and expiry policy visible in the same release process. For a B2B SaaS reset flow, I would default to application ownership and make an exception only for a documented country or carrier requirement.
2. What does a fair Twilio and Vonage comparison measure?
Start with the message lifecycle, then inspect the commercial and integration edges. Twilio has extensive global coverage, strong documentation, SDKs, and broad integrations. Vonage offers competitive pricing and may have an advantage for volume SMS. Their feature sets are broadly similar for this scenario, including delivery receipts and message scheduling, but regional carrier partnerships vary.
The right test is a small matrix of the countries that actually receive resets. Record sender registration, delivery evidence, scheduling behavior, retry semantics, and the time needed for an on-call engineer to trace a message ID back to the application event. Current rates belong in that exercise, not in a permanent article claim.
Three other products clarify the boundaries:
| Option | Where it fits | What to verify |
|---|---|---|
| Twilio | Teams that value coverage, integrations, documentation, and SDK ergonomics | Regional registration and the configuration surface |
| Vonage | Teams sending volume SMS where regional economics are favorable | Country-level carrier relationships and current rates |
| Infobip | Teams that need a wider communications portfolio and regional operations | Whether that breadth is justified for reset-only SMS |
| Amazon SNS | Teams already standardized on AWS identity and operations | Application-owned delivery evidence and policy |
Amazon SES, SendGrid, and Resend are useful comparisons for an email-first recovery path, but they are not substitutes for an SMS carrier decision. A provider should earn its place through the workflow evidence it can supply, not through a generic global-coverage claim.
3. Can a reset worker retry without creating another reset?
Yes, if the application makes the event idempotent before it calls a provider. The worker should persist the event ID, keep the token single-use, and reuse that ID when it retries. It should also record the provider message ID once the submission succeeds. A standard queue is at-least-once by nature, so this check belongs in the consumer even when the upstream API appears reliable.
Here is the part worth testing in a runbook. The first request can time out after the provider accepts it. The second attempt must be recognized as the same logical send. A 429 must produce bounded exponential backoff and honor Retry-After; it must not become a tight loop. The reset endpoint should still reject a token after its short expiry, even if the status check is delayed.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
func status(messageID, key string) ([]byte, error) {
baseURL := os.Getenv("INFRAI_STATUS_BASE_URL")
statusPath := os.Getenv("INFRAI_SMS_STATUS_PATH")
endpoint := baseURL + statusPath + url.PathEscape(messageID)
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
return nil, readErr
}
if res.StatusCode == http.StatusTooManyRequests {
wait := time.Second << attempt
if retryAfter, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil && retryAfter > 0 {
wait = time.Duration(retryAfter) * time.Second
}
time.Sleep(wait)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return body, fmt.Errorf("status request failed: %s", body)
}
return body, nil
}
return nil, fmt.Errorf("status request remained rate limited")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
messageID := os.Getenv("SMS_MESSAGE_ID")
if key == "" || messageID == "" || os.Getenv("INFRAI_STATUS_BASE_URL") == "" || os.Getenv("INFRAI_SMS_STATUS_PATH") == "" {
panic("INFRAI_API_KEY, SMS_MESSAGE_ID, INFRAI_STATUS_BASE_URL, and INFRAI_SMS_STATUS_PATH are required")
}
body, err := status(messageID, key)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
This is a status query, not a send operation, so it cannot create a duplicate message. The send path still needs a durable event store and an idempotency key. On a 10-minute reset, a worker that retries at minute 9 must not confuse a delayed status response with permission to mint a new token. Set INFRAI_STATUS_BASE_URL to the platform's documented v1 base and INFRAI_SMS_STATUS_PATH to its documented status path; keeping both in deployment configuration avoids baking endpoint policy into the worker.
The example checks the response body as well as the status code. It also caps retries at three attempts; a production worker can move the same policy into its queue scheduler. The 295-route surface and 10-language runnable examples are useful discovery signals, but neither is a delivery guarantee for a particular carrier.
4. When is one API boundary useful to the on-call team?
A unified backend surface can remove a different class of friction. Infrai is a reasonable option when the same backend service wants one key and one bill across communication and other backend capabilities, while its plain REST API lets a Go worker call capabilities without installing a provider SDK. Its discovery surface is public and self-describing, with runnable examples in 10 languages; the broader surface covers 295 routes across 20 modules. That can shorten the path from an incident question to a request schema, especially in a mixed-language platform.
The benefit is operational consistency, not proof of carrier performance. One key reduces credential sprawl. A common HTTP shape reduces adapter code when a team moves a status check or an adjacent backend operation into the same service. The cost is a larger trust and outage boundary: a separately operated monitoring or messaging vendor gives independence that a unified surface cannot.
The limitations matter for this reset flow. Events are pull-based rather than webhook-pushed, so multi-channel orchestration is less immediate. There is no SMTP relay, voice, WhatsApp, or RCS channel. Email has no hosted OTP interface, and scheduled email has no cancellation route; SMS does have cancellation. SMS fraud controls such as geographic fencing and per-country spend breakers remain business-layer work. The platform also has no tag-aggregated cost reporting API, and SMS templates have no list interface.
Do not choose a unified API when procurement requires independent sender and monitoring vendors, or when a particular country relationship decides the outcome. Convenience is a useful constraint, not a universal recommendation.
5. What should the rollout gate prove?
Run the same short scenario through each candidate: create a reset, retry after a client timeout, receive a duplicate delivery, query delivery evidence, and attempt the old token after expiry. Repeat it for the actual destination countries. Confirm that the on-call record contains the application event ID and provider message ID, and that the copy cannot change without the application's review path.
Choose Twilio when integrations, global coverage, documentation, and SDK support reduce more operational risk. Choose Vonage when its country-level route and current volume economics fit the traffic. Consider Infobip or Amazon SNS when regional operations or existing AWS alignment are already decisive. Consider a unified REST surface when reducing keys and adapter glue matters more than keeping delivery and backend operations with separate vendors.
The durable decision rule is ownership: the application decides whether a reset is valid, while the provider reports what happened to the message. That rule survives a changed rate card, a delayed receipt, and a provider migration.
Top comments (0)