MarioSMS

Receive SMS Codes in Go: MarioSMS API Integration

Updated · MarioSMS team

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.