Receive SMS Codes in Go: MarioSMS API Integration
Go is a natural fit for OTP automation: a small HTTP client, a polling loop with a context deadline, and no dependencies. This guide builds a minimal MarioSMS client using only the standard library.
You’ll need a MarioSMS account, some balance, and your API key from the app profile. The endpoint reference is at app.mariosms.com/api-docs; all requests carry an X-API-Key header.
The client
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
const base = "https://app.mariosms.com/api/v1"
type Client struct {
key string
http *http.Client
}
type Activation struct {
ID string `json:"id"`
Phone string `json:"phone"`
Service string `json:"service"`
Country string `json:"country"`
Price float64 `json:"price"`
Status string `json:"status"`
SMSCode string `json:"smsCode"`
SMSText string `json:"smsText"`
}
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}
}
req, err := http.NewRequestWithContext(ctx, method, base+path, &buf)
if err != nil {
return err
}
req.Header.Set("X-API-Key", c.key)
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("api error: %s", resp.Status)
}
if out != nil {
return json.NewDecoder(resp.Body).Decode(out)
}
return nil
}
Rent a number and wait for the code
POST /activation rents the number; polling GET /activation/:id until status == "received" yields the code. The context deadline doubles as your overall timeout:
func (c *Client) Rent(ctx context.Context, country, service string) (*Activation, error) {
var a Activation
err := c.do(ctx, http.MethodPost, "/activation",
map[string]string{"country": country, "service": service}, &a)
return &a, err
}
func (c *Client) WaitForCode(ctx context.Context, id string) (string, error) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-ticker.C:
var a Activation
if err := c.do(ctx, http.MethodGet, "/activation/"+id, nil, &a); err != nil {
return "", err
}
if a.Status == "received" {
return a.SMSCode, nil
}
}
}
}
func main() {
c := &Client{key: "your_api_key_here", http: &http.Client{Timeout: 15 * time.Second}}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
a, err := c.Rent(ctx, "us", "tg") // Telegram, US number
if err != nil {
panic(err)
}
fmt.Println("number:", a.Phone)
code, err := c.WaitForCode(ctx, a.ID)
if err != nil {
// context deadline hit; the platform refunds automatically
// when no SMS lands in the activation window.
panic(err)
}
fmt.Println("OTP:", code)
}
Cancelling early
POST /activation/:id/cancel refunds the full price if the SMS hasn’t arrived yet:
err := c.do(ctx, http.MethodPost, "/activation/"+a.ID+"/cancel", nil, nil)
You never pay for codes that don’t arrive: the timeout refund is enforced server-side, so a crashed client doesn’t cost you anything.
Service and country codes come from GET /services and GET /countries; live prices from GET /prices. The usual note applies: automate what you’re allowed to automate (QA and testing yes, abuse no) per the acceptable use policy.