Documentation
¶
Overview ¶
Package smstunnel sends texts (SMS) from your own Android phone, from your own number, with SMSTunnel (https://smstunnel.io).
The phone runs the SMSTunnel app from Google Play and is linked to your SMSTunnel account. Your program hands each text to your account with an API key, and the phone sends it from its own SIM, so recipients see a number they know and can reply to. Create the key in your dashboard: Apps → Add an app (https://dashboard.smstunnel.io/apps).
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
res, err := client.SendSMS(ctx, "+14155550123", "Your order #1042 has shipped.")
if err != nil {
log.Fatal(err)
}
fmt.Println(res.MessageID)
One call sends one text to one number: to text several people, call Client.SendSMS once per number. Sign-in and verification codes go through Client.SendSignInCode. Client.GetMessageStatus, Client.ListPhones, Client.GetUsage and Client.ListReceivedTexts read the rest; ReceivedTextsCursor follows received texts as they arrive.
Errors ¶
Every method returns an error when the call doesn't go through:
- *APIError when SMSTunnel answered with an error: it keeps the HTTP status, SMSTunnel's code (such as CodePermissionRequired), its words and the Retry-After wait. errors.Is matches it against ErrUnauthorized (401), ErrForbidden (403), ErrNotFound (404), ErrRateLimited (429) and ErrServer (5xx).
- *ConnectionError when no answer came back: SMSTunnel could not be reached, or didn't answer in time (ConnectionError.Timeout).
- An error that matches ErrInvalidInput when an argument is refused before anything is sent.
- An error that matches context.Canceled when the context was cancelled.
The client never retries by itself. After a timeout, a connection error or a 5xx answer a text may already be on its way: retry a send only with the same WithIdempotencyKey, so it can't go out twice.
A phone that is offline is not an error: the text waits on your account (SendResult.Queued) and goes out when the phone is back.
Index ¶
- Constants
- Variables
- type APIError
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) GetMessageStatus(ctx context.Context, messageID string) (*MessageStatus, error)
- func (c *Client) GetUsage(ctx context.Context) (*Usage, error)
- func (c *Client) GoString() string
- func (c *Client) ListPhones(ctx context.Context) ([]Phone, error)
- func (c *Client) ListReceivedTexts(ctx context.Context, q ReceivedTextsQuery) (*ReceivedTextsPage, error)
- func (c *Client) NewReceivedTextsCursor(opts ReceivedTextsCursorOptions) (*ReceivedTextsCursor, error)
- func (c *Client) SendSMS(ctx context.Context, to, message string, opts ...SendOption) (*SendResult, error)
- func (c *Client) SendSignInCode(ctx context.Context, to, message string, opts ...SendOption) (*SendResult, error)
- func (c *Client) String() string
- func (c *Client) UserAgent() string
- type ConnectionError
- type MessageStatus
- type Option
- type Phone
- type Quota
- type ReceivedText
- type ReceivedTextsCursor
- type ReceivedTextsCursorOptions
- type ReceivedTextsPage
- type ReceivedTextsQuery
- type Remaining
- type SendOption
- type SendResult
- type State
- type Usage
- type UsageCounts
- type UsageEnterprise
- type UsageLimits
- type UsageRemaining
Examples ¶
Constants ¶
const ( // CodePermissionRequired (403): the API key lacks a permission; APIError.Permission names it, e.g. "sms:inbox". CodePermissionRequired = "PERMISSION_REQUIRED" // CodeAPIKeyFromWebPage (401): an API key sent from a web page on another site. Call SMSTunnel from a server. CodeAPIKeyFromWebPage = "API_KEY_FROM_WEB_PAGE" // CodeDisabledBySupport: SMSTunnel support turned the key or the phone off. CodeDisabledBySupport = "DISABLED_BY_SUPPORT" // CodeIdempotencyKeyInvalid (400): the idempotency key is not 1 to 255 printable ASCII characters. CodeIdempotencyKeyInvalid = "IDEMPOTENCY_KEY_INVALID" // CodeIdempotencyKeyReused (422): the idempotency key was used in the last 24 hours for a different text. CodeIdempotencyKeyReused = "IDEMPOTENCY_KEY_REUSED" // CodeIdempotencyKeyInProgress (409): a call with this idempotency key is still running; retry after RetryAfter. CodeIdempotencyKeyInProgress = "IDEMPOTENCY_KEY_IN_PROGRESS" )
Values of APIError.Code a program can act on.
const DefaultBaseURL = "https://smstunnel.io"
DefaultBaseURL is the address of SMSTunnel's API.
const DefaultCursorOverlap = 2 * time.Minute
DefaultCursorOverlap is how far back each poll of a ReceivedTextsCursor looks again, by default.
const DefaultTimeout = 30 * time.Second
DefaultTimeout is how long a call may take, unless WithTimeout changes it.
const Version = "1.0.0"
Version is the version of this library. It is sent in the User-Agent header.
Variables ¶
var ( ErrUnauthorized = errors.New("smstunnel: unauthorized") // ErrForbidden (403): the key lacks a permission (Code is CodePermissionRequired), or the plan doesn't include // the feature. ErrForbidden = errors.New("smstunnel: forbidden") // ErrNotFound (404): no such message for this key. ErrNotFound = errors.New("smstunnel: not found") // ErrRateLimited (429): too many requests (RetryAfter says how long to wait), or the plan's limit for the day // or month is reached (no RetryAfter; the message says when texts can go out again). ErrRateLimited = errors.New("smstunnel: rate limited") // ErrServer (5xx): a problem on SMSTunnel's side. A send may or may not have been accepted. ErrServer = errors.New("smstunnel: server error") )
Errors that *APIError matches with errors.Is, by HTTP status.
var ErrInvalidInput = errors.New("smstunnel: invalid input")
ErrInvalidInput is matched (errors.Is) by the errors of arguments refused before anything is sent: an empty number, two numbers in one, a country code that isn't one, a bad option.
Functions ¶
This section is empty.
Types ¶
type APIError ¶
type APIError struct {
// StatusCode is the HTTP status.
StatusCode int
// Code is SMSTunnel's own code for errors a program can act on (see CodePermissionRequired and the others),
// or "".
Code string
// Message is what went wrong, in SMSTunnel's words.
Message string
// Permission is, with CodePermissionRequired, the permission the key lacks, such as "sms:inbox".
Permission string
// RetryAfter is how long to wait before trying again, from the Retry-After header; 0 when there was none.
RetryAfter time.Duration
// Header holds the answer's headers.
Header http.Header
// Body is the answer as SMSTunnel sent it (usually JSON).
Body []byte
}
APIError is returned when SMSTunnel answered with an error status.
Besides the statuses with a sentinel (see ErrUnauthorized and the others), expect 400 (a number SMSTunnel can't read, a sign-in code text without a code, a refused field), 409 and 422 (an idempotency key still in progress, or reused for another text).
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
_, err = client.SendSMS(context.Background(), "+14155550123", "Your table is ready.")
var apiErr *smstunnel.APIError
var connErr *smstunnel.ConnectionError
switch {
case err == nil:
fmt.Println("sent")
case errors.Is(err, smstunnel.ErrRateLimited) && errors.As(err, &apiErr) && apiErr.RetryAfter > 0:
fmt.Println("too many texts right now; try again in", apiErr.RetryAfter)
case errors.Is(err, smstunnel.ErrUnauthorized):
fmt.Println("check the API key in the dashboard: Apps")
case errors.As(err, &apiErr) && apiErr.Code == smstunnel.CodePermissionRequired:
fmt.Println("the key lacks", apiErr.Permission)
case errors.As(err, &connErr):
fmt.Println("no answer; retry only with the same idempotency key:", connErr.Timeout())
default:
fmt.Println(err)
}
}
Output:
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client calls the SMSTunnel API. It is safe for concurrent use: make one and reuse it.
func NewClient ¶
NewClient returns a client that sends with this API key, the key you create in your SMSTunnel dashboard (Apps → Add an app). Read it from an environment variable or a secret store, never from source code.
The error matches ErrInvalidInput when the key is empty or an option is refused.
func (*Client) GetMessageStatus ¶
GetMessageStatus looks up where a text is now (GET /api/v1/sms/status/{id}). It needs a key with sms:read.
Example ¶
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
status, err := client.GetMessageStatus(context.Background(), "6aea38f9b38ed3bed4f8569f")
if err != nil {
log.Fatal(err)
}
switch status.State {
case smstunnel.StateSent, smstunnel.StateDelivered:
fmt.Println("sent to", status.PhoneNumber)
case smstunnel.StateFailed:
fmt.Println("failed:", status.ErrorMessage)
case smstunnel.StateUnconfirmed:
fmt.Println("most likely sent; don't resend it")
default:
fmt.Println("waiting for the phone")
}
}
Output:
func (*Client) GetUsage ¶
GetUsage reads the plan, today's usage and what is left (GET /api/v1/account/usage).
Example ¶
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
usage, err := client.GetUsage(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s plan: %d texts today, %s left\n", usage.Plan, usage.Used.SMSClean, usage.Remaining.SMSClean)
}
Output:
func (*Client) ListPhones ¶
ListPhones lists the phones of the key's account and whether each is online (GET /api/v1/devices).
Example ¶
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
phones, err := client.ListPhones(context.Background())
if err != nil {
log.Fatal(err)
}
for _, phone := range phones {
fmt.Println(phone.ID, phone.Name, phone.Online)
}
}
Output:
func (*Client) ListReceivedTexts ¶
func (c *Client) ListReceivedTexts(ctx context.Context, q ReceivedTextsQuery) (*ReceivedTextsPage, error)
ListReceivedTexts lists texts your phones received, newest first (GET /api/v1/sms/received). It needs a key you allowed to read received texts ("Can also read received texts", the sms:inbox permission); other keys get a 403 *APIError with CodePermissionRequired. To follow new texts as they arrive, use Client.NewReceivedTextsCursor.
Example ¶
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
page, err := client.ListReceivedTexts(context.Background(), smstunnel.ReceivedTextsQuery{
Since: time.Now().Add(-24 * time.Hour),
Limit: 20,
})
if err != nil {
log.Fatal(err)
}
for _, text := range page.Texts {
fmt.Println(text.ReceivedAt.Format(time.Kitchen), text.Sender, text.Message)
}
}
Output:
func (*Client) NewReceivedTextsCursor ¶
func (c *Client) NewReceivedTextsCursor(opts ReceivedTextsCursorOptions) (*ReceivedTextsCursor, error)
NewReceivedTextsCursor returns a cursor over the texts your phones receive. See ReceivedTextsCursor.
func (*Client) SendSMS ¶
func (c *Client) SendSMS(ctx context.Context, to, message string, opts ...SendOption) (*SendResult, error)
SendSMS sends one text to one number (POST /api/v1/sms/send). To text several people, call it once per number.
to is the recipient in international format (+14155550123); a local number needs a country code. message is the text, up to 1600 characters.
Example ¶
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
res, err := client.SendSMS(context.Background(), "+14155550123", "Your order #1042 has shipped.",
smstunnel.WithIdempotencyKey("order-1042-shipped"))
if err != nil {
log.Fatal(err)
}
if res.Queued {
fmt.Println("Your phone is offline: the text goes out when it is back.")
}
fmt.Println("message", res.MessageID)
}
Output:
func (*Client) SendSignInCode ¶
func (c *Client) SendSignInCode(ctx context.Context, to, message string, opts ...SendOption) (*SendResult, error)
SendSignInCode sends a sign-in or verification code (POST /api/v1/sms/send-2fa). The text must contain the code as a number of its own, 4 to 10 digits ("Your code is 482913" or "123-456"), and be at most 320 characters; otherwise SMSTunnel answers 400. Codes have their own daily allowance. Use it for codes only.
Example ¶
package main
import (
"context"
"log"
"os"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
// The text must contain the code as a number of its own, 4 to 10 digits.
if _, err := client.SendSignInCode(context.Background(), "+14155550123", "Your sign-in code is 482913"); err != nil {
log.Fatal(err)
}
}
Output:
type ConnectionError ¶
type ConnectionError struct {
// Err is the error of the HTTP client.
Err error
}
ConnectionError is returned when no answer came back: SMSTunnel could not be reached (DNS, TLS, a refused or broken connection), or didn't answer in time. There is no HTTP status. For a send, the text may or may not have been accepted: retry it only with the same idempotency key.
func (*ConnectionError) Error ¶
func (e *ConnectionError) Error() string
func (*ConnectionError) Timeout ¶
func (e *ConnectionError) Timeout() bool
Timeout reports whether the call ran out of time (the client's timeout or the context's deadline).
func (*ConnectionError) Unwrap ¶
func (e *ConnectionError) Unwrap() error
type MessageStatus ¶
type MessageStatus struct {
// ID is the text's ID.
ID string `json:"id"`
// PhoneNumber is the number the text went to, in international format.
PhoneNumber string `json:"phoneNumber"`
// State is where the text is now.
State State `json:"state"`
// SentAt is when the phone sent it, or nil.
SentAt *time.Time `json:"sentAt,omitempty"`
// DeliveredAt is when delivery was confirmed, or nil.
DeliveredAt *time.Time `json:"deliveredAt,omitempty"`
// ErrorMessage says why it failed, or why it is unconfirmed.
ErrorMessage string `json:"errorMessage,omitempty"`
// Raw is SMSTunnel's whole answer, for fields this version doesn't map.
Raw json.RawMessage `json:"-"`
}
MessageStatus is where one text is now.
type Option ¶
An Option changes a setting of NewClient.
func WithBaseURL ¶
WithBaseURL points the client at another address than DefaultBaseURL, such as a test server ("http://127.0.0.1:3141"). A path is kept: "https://example.com/smstunnel" calls "https://example.com/smstunnel/api/v1/...".
func WithDefaultCountryCode ¶
WithDefaultCountryCode sets the country code ("+40", "40" or "0040") that completes numbers written without + or 00, for every text the client sends. A text's own WithCountryCode wins over it; when neither is set, SMSTunnel uses the phone's country. Numbers in international format (+14155550123) never need it.
func WithHTTPClient ¶
WithHTTPClient sends through your own http.Client, for its transport, proxy or TLS settings. The client keeps a copy of it: unless yours has its own CheckRedirect, the copy doesn't follow redirects, so the API key is never sent to another address. Its Timeout applies on top of WithTimeout.
Example ¶
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"),
smstunnel.WithHTTPClient(&http.Client{Transport: http.DefaultTransport}),
smstunnel.WithDefaultCountryCode("+40"),
smstunnel.WithTimeout(15*time.Second),
smstunnel.WithUserAgentSuffix("my-tool/2.1"),
)
if err != nil {
log.Fatal(err)
}
_ = client
}
Output:
func WithTimeout ¶
WithTimeout sets how long one call may take (DefaultTimeout otherwise). 0 turns the client's own timeout off; the context's deadline still applies.
func WithUserAgentSuffix ¶
WithUserAgentSuffix adds your program's name and version to the User-Agent header, after this library's: "smstunnel-go/1.0.0 (+https://smstunnel.io) my-tool/2.1".
type Phone ¶
type Phone struct {
// ID identifies the phone: pass it to WithDeviceID to send from it.
ID string `json:"id"`
// Name is the phone's name, as shown under Phones in the dashboard.
Name string `json:"name"`
// Online is whether the SMSTunnel app on the phone is connected now. Texts for an offline phone wait until it is back.
Online bool `json:"isOnline"`
// LastSeenAt is when the phone was last connected, or nil.
LastSeenAt *time.Time `json:"lastSeenAt,omitempty"`
}
Phone is one of your phones.
type Quota ¶
type Quota int64
Quota is a limit, or what is left of one. Unlimited means there is no limit. In JSON it is a number, or the string "unlimited".
const Unlimited Quota = -1
Unlimited is the Quota of something without a limit.
func (Quota) IsUnlimited ¶
IsUnlimited reports whether there is no limit.
func (Quota) MarshalJSON ¶
MarshalJSON writes the number, or "unlimited".
func (*Quota) UnmarshalJSON ¶
UnmarshalJSON reads a number (a negative one meaning no limit), "unlimited", or null (left as it is).
type ReceivedText ¶
type ReceivedText struct {
// ID is the text's ID.
ID string `json:"id"`
// Sender is the number that sent it.
Sender string `json:"sender"`
// Message is the words; "" when the phone synced the text encrypted (see EncryptedPayload).
Message string `json:"message"`
// Encrypted is whether the phone synced the text encrypted.
Encrypted bool `json:"isEncrypted"`
// EncryptedPayload is the encrypted text, for software that holds the key.
EncryptedPayload string `json:"encryptedPayload,omitempty"`
// ReceivedAt is when the phone received it (the phone's clock).
ReceivedAt time.Time `json:"receivedAt"`
// SIMSlot is the SIM slot it arrived on, when the phone says.
SIMSlot *int `json:"simSlot,omitempty"`
// SubscriptionID is Android's subscription ID of that SIM, when the phone says.
SubscriptionID *int `json:"subscriptionId,omitempty"`
// DeviceID is the ID of the phone that received it.
DeviceID string `json:"deviceId,omitempty"`
// DeviceName is the name of that phone.
DeviceName string `json:"deviceName,omitempty"`
}
ReceivedText is a text one of your phones received and synced to your account.
type ReceivedTextsCursor ¶
type ReceivedTextsCursor struct {
// contains filtered or unexported fields
}
ReceivedTextsCursor follows received texts as they arrive, for a watch loop: each call to Next returns the texts that reached SMSTunnel since the previous call, oldest first, each one once. It is not safe for concurrent use.
SMSTunnel filters received texts by the time it got them from the phone, so a text the phone syncs late is still found. The cursor asks again for the last Overlap of every poll and remembers the IDs it returned in that window.
It needs a key allowed to read received texts (sms:inbox).
Example ¶
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/narcisbodea/smstunnel-go"
)
func main() {
client, err := smstunnel.NewClient(os.Getenv("SMSTUNNEL_API_KEY"))
if err != nil {
log.Fatal(err)
}
cursor, err := client.NewReceivedTextsCursor(smstunnel.ReceivedTextsCursorOptions{Since: time.Now()})
if err != nil {
log.Fatal(err)
}
for {
texts, err := cursor.Next(context.Background())
if err != nil {
log.Print(err) // nothing is lost: the next call asks again
}
for _, text := range texts {
fmt.Println(text.Sender, text.Message)
}
time.Sleep(10 * time.Second)
}
}
Output:
func (*ReceivedTextsCursor) Next ¶
func (rc *ReceivedTextsCursor) Next(ctx context.Context) ([]ReceivedText, error)
Next returns the texts that arrived since the previous call (on the first call, since Options.Since), oldest first; none is an empty slice. After an error nothing is lost: the next call asks for the same texts again.
func (*ReceivedTextsCursor) Seen ¶
func (rc *ReceivedTextsCursor) Seen() []string
Seen lists, sorted, the IDs the cursor remembers, to pass as Options.Seen after a restart.
func (*ReceivedTextsCursor) Since ¶
func (rc *ReceivedTextsCursor) Since() time.Time
Since is the moment the next poll asks from. Save it with Seen to go on after a restart.
type ReceivedTextsCursorOptions ¶
type ReceivedTextsCursorOptions struct {
// Since is where to start: texts that reached SMSTunnel at or after this moment, by SMSTunnel's clock. Use
// time.Now() to follow only new texts; the zero time starts with every text the account has.
Since time.Time
// Phone keeps only the texts from this sender (part of the number is enough).
Phone string
// DeviceID keeps only the texts received by this phone.
DeviceID string
// PageSize is how many texts each request asks for, 1 to 100; 0 means 100.
PageSize int
// Overlap is how far back each poll looks again, so that no text is missed because of a clock difference
// between this computer and SMSTunnel. Texts seen in the overlap are not returned twice. 0 means
// DefaultCursorOverlap.
Overlap time.Duration
// Seen are the IDs of texts already handled, for example Seen() saved by an earlier run along with Since():
// they are not returned again.
Seen []string
}
ReceivedTextsCursorOptions sets up a ReceivedTextsCursor.
type ReceivedTextsPage ¶
type ReceivedTextsPage struct {
// Texts are the texts on this page, newest first.
Texts []ReceivedText `json:"messages"`
// Total is how many texts match, on every page.
Total int `json:"total"`
// Page is this page's number, starting at 1.
Page int `json:"page"`
// Limit is the number of texts per page.
Limit int `json:"limit"`
// HasMore is whether there is a next page.
HasMore bool `json:"hasMore"`
}
ReceivedTextsPage is one page of received texts, newest first.
type ReceivedTextsQuery ¶
type ReceivedTextsQuery struct {
// Limit is how many texts per page, 1 to 100; 0 means SMSTunnel's default (50).
Limit int
// Page is the page, starting at 1; 0 means the first.
Page int
// Since keeps only the texts that reached SMSTunnel at or after this moment (by SMSTunnel's clock).
Since time.Time
// Phone keeps only the texts from this sender (part of the number is enough).
Phone string
// DeviceID keeps only the texts received by this phone.
DeviceID string
}
ReceivedTextsQuery chooses which received texts to list. Every field is optional.
type Remaining ¶
type Remaining struct {
// Clean counts the texts (or, after SendSignInCode, the sign-in codes) that can still go out as written.
Clean Quota `json:"clean"`
// Total counts all that can still go out, including the ones with the Free plan's added line.
Total Quota `json:"total"`
}
Remaining is today's allowance left after a text.
type SendOption ¶
type SendOption func(*sendSettings)
A SendOption changes one text: the phone it goes out from, its country code, its idempotency key.
func WithCountryCode ¶
func WithCountryCode(code string) SendOption
WithCountryCode sets the country code ("+49", "49" or "0049") that completes this text's number when it is written without + or 00. It wins over WithDefaultCountryCode and over the phone's own country.
func WithDeviceID ¶
func WithDeviceID(id string) SendOption
WithDeviceID sends the text from this phone: its ID from the dashboard (Phones, the Copy button) or Phone.ID. Without it, SMSTunnel picks one of your phones.
func WithIdempotencyKey ¶
func WithIdempotencyKey(key string) SendOption
WithIdempotencyKey makes a retry safe: a second call with the same key within 24 hours gets the first call's answer back (SendResult.Replayed) instead of sending a second text. Use a value that names this one text, such as "order-1042-shipped" or a UUID you store with it (1 to 255 printable ASCII characters). Reusing a key for a different text fails with status 422 and CodeIdempotencyKeyReused.
type SendResult ¶
type SendResult struct {
// MessageID identifies the text: keep it to look up the status with GetMessageStatus.
MessageID string `json:"messageId"`
// Queued is true when no phone of yours was online: the text waits on your account and goes out as soon as a
// phone is back (within 3 days, otherwise it is marked failed). Nothing to resend.
Queued bool `json:"queued"`
// Branded is true when the Free plan added its short SMSTunnel line to the text.
Branded bool `json:"branded"`
// Remaining is what is left today after this text, when SMSTunnel says.
Remaining *Remaining `json:"remaining,omitempty"`
// Replayed is true when this is the stored answer to an earlier call with the same idempotency key: no second
// text was sent.
Replayed bool `json:"replayed"`
// Raw is SMSTunnel's whole answer, for fields this version doesn't map.
Raw json.RawMessage `json:"-"`
}
SendResult is SMSTunnel's answer to a text it accepted.
type State ¶
type State string
State is where a text is now.
const ( // StatePending: waiting for the phone, for example while it is offline. StatePending State = "pending" // StateSent: the phone sent it. StateSent State = "sent" // StateDelivered: the recipient's network confirmed delivery. StateDelivered State = "delivered" // StateFailed: it could not be sent; MessageStatus.ErrorMessage says why. StateFailed State = "failed" // StateUnconfirmed: the phone handed it to the SIM but never got the confirmation. It most likely went out and // is never resent automatically. Don't resend it yourself: the recipient could get it twice. StateUnconfirmed State = "unconfirmed" )
The states of a text. A state this version doesn't know is kept as SMSTunnel sent it.
type Usage ¶
type Usage struct {
// Plan is the plan, for example "free", "starter", "standard", "pro" or "enterprise".
Plan string `json:"plan"`
// AccountType is the kind of account, for example "standard".
AccountType string `json:"accountType"`
// Used is what was used today, and this month.
Used UsageCounts `json:"usage"`
// Limits are the plan's limits.
Limits UsageLimits `json:"limits"`
// Remaining is what is left.
Remaining UsageRemaining `json:"remaining"`
// Enterprise is the company account whose allowance this account shares, or nil.
Enterprise *UsageEnterprise `json:"enterprise,omitempty"`
// Raw is SMSTunnel's whole answer, for fields this version doesn't map.
Raw json.RawMessage `json:"-"`
}
Usage is the plan, today's usage and what is left. Daily counts restart at 00:00 UTC, the month's count on the 1st. A client of a company account shares the company's allowance and sees it here. The field names are the API's.
type UsageCounts ¶
type UsageCounts struct {
SMSClean int64 `json:"smsClean"` // texts sent today as written
SMSBranded int64 `json:"smsBranded"` // texts sent today with the Free plan's added line
TwoFAClean int64 `json:"twoFaClean"` // sign-in codes sent today
SMSCleanMonth int64 `json:"smsCleanMonth"` // texts sent this month as written
}
UsageCounts is what was used.
type UsageEnterprise ¶
UsageEnterprise is the company account a client account belongs to.
type UsageLimits ¶
type UsageLimits struct {
SMSClean Quota `json:"smsClean"` // texts as written, per day
TwoFAClean Quota `json:"twoFaClean"` // sign-in codes, per day
SMSCleanPerMonth Quota `json:"smsCleanPerMonth"` // texts as written, per month
SMSBrandedPerDay Quota `json:"smsBrandedPerDay"` // texts with the added line, per day (Free plan; 0 on paid plans)
}
UsageLimits are the plan's limits.
type UsageRemaining ¶
type UsageRemaining struct {
SMSClean Quota `json:"smsClean"` // texts that can still go out today as written
TwoFAClean Quota `json:"twoFaClean"` // sign-in codes that can still go out today
SMSCleanMonth Quota `json:"smsCleanMonth"` // texts as written left this month
SMSBranded Quota `json:"smsBranded"` // texts with the added line left today
}
UsageRemaining is what is left.
