Help us shape the future of Go! Take the 2026 Go Developer Survey

calisero

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 17 Imported by: 0

README ΒΆ

Calisero Go SMS API Library

Go Reference CI Coverage Go Version Go Report Card Release Dependencies License

Official Go library for the Calisero transactional SMS API.

Send SMS messages, verify phone numbers with one-time codes, manage opt-outs for GDPR compliance, receive delivery reports and monitor your account, with an idiomatic, context-aware library that has no dependencies.

Features

  • πŸš€ Simple & Intuitive: calisero.NewClient(apiKey), then client.Messages.Create(ctx, ...)
  • πŸ”’ Type Safe: typed requests, resources and errors, statuses as typed constants
  • 🌐 Zero Dependencies: built on the standard library's net/http, nothing else to download
  • ⚑ Context-aware: every call takes a context.Context that cancels it, and a client is safe for concurrent use
  • πŸ“± Complete API Coverage: messages, verifications (OTP), opt-outs and accounts, checked against the API's OpenAPI document
  • πŸ” OTP / 2FA: start a verification and validate the code in two calls
  • βœ‚οΈ URL Shortening: shorten the links of a message body and track their clicks
  • πŸ“Š Limits & Tracing: daily sending limit, request rate limit and the trace ID of every answer and error
  • πŸ“¬ Delivery Webhooks: typed, validated parsing of delivery status callbacks, and a ready http.Handler
  • πŸ›‘οΈ Error Handling: an error type for every kind of failure, found with errors.As, with the API's message and the trace ID
  • πŸ• Romania Time Handled: pass a time.Time to schedule a message, it is converted to the time zone the API expects
  • πŸ§ͺ Thoroughly Tested: unit, integration (real HTTP) and contract (OpenAPI) tests, with the race detector, on Go 1.22 to 1.27, Linux, macOS and Windows; 100% of the statements covered
  • πŸ“– Rich Examples: 19 runnable examples covering every operation, and documentation examples on pkg.go.dev
  • 🐘 Sibling Libraries: calisero/calisero-php, @calisero/calisero-node and calisero/laravel-sms

Requirements

  • Go 1.22 or later β€” tested on 1.22, 1.23, 1.24, 1.25, 1.26 and 1.27
  • A Calisero API key

The library carries no dependency outside the Go standard library. It is meant for server-side code: never ship your API key in a client application.

Installation

go get github.com/calisero/calisero-go

The package is named calisero:

import calisero "github.com/calisero/calisero-go"

Getting Your API Key

To use this library, you'll need an API key from your Calisero account:

  1. Log in to your Calisero account at https://calisero.ro
  2. Navigate to the dashboard and go to the "API Keys" section
  3. Click "Add Key" to create a new API key
  4. Configure your key:
    • Give it a descriptive name (e.g., "Production App", "Development")
    • Set IP filters if needed for additional security
    • Choose a sandbox key to test without sending or paying for real messages
  5. Copy your API key and store it securely

Security Note: Never commit API keys to version control. Use environment variables or a secret manager.

Quick Start

Basic SMS Sending
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	// Create the SMS client
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	// Send a simple SMS
	response, err := client.Messages.Create(context.Background(), &calisero.CreateMessageRequest{
		Recipient: "+40742***350",
		Body:      "Hello from Calisero!",
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Message sent! ID: %s\n", response.Data.ID)
	fmt.Printf("Status: %s\n", response.Data.Status)
}
Environment Configuration

For production applications, store your API key securely:

# .env file, or your secret manager
CALISERO_API_KEY=your-api-key-here

calisero.NewClient() returns an error when the key is missing or empty, so a misconfigured environment fails at startup rather than on the first message. Create the client once and share it: a Client is safe for concurrent use by multiple goroutines.

🎯 Common Use Cases

OTP / 2FA Authentication

The Verifications API generates the code, sends it by SMS and checks it, so your application never stores it:

ctx := context.Background()

// 1. Send a code (6 characters, valid for 5 minutes by default)
_, err := client.Verifications.Create(ctx, &calisero.CreateVerificationRequest{Phone: "+40742***350", Brand: "YourApp"})
if err != nil {
	return err
}

// 2. Check the code the user typed
response, err := client.Verifications.Validate(ctx, &calisero.VerificationCheckRequest{Phone: "+40742***350", Code: userInput})

var validationError *calisero.ValidationError

switch {
case errors.As(err, &validationError):
	// A wrong or expired code, or too many attempts (5)
	fmt.Println(validationError.Message)
case err != nil:
	return err
default:
	fmt.Println("Verified at", response.Data.VerifiedAt)
}

To send a code you generate yourself, hide it in the API's answers and in the dashboard with VisibleBody:

_, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient:   userPhoneNumber,
	Body:        "Your verification code is: AC3-4F6. Valid for 5 minutes.",
	VisibleBody: "Your verification code is: ******. Valid for 5 minutes.",
	Sender:      "YourApp",
})
if err != nil {
	return err
}
Order Notifications
_, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient:   customerPhone,
	Body:        fmt.Sprintf("Order #%s confirmed! Estimated delivery: %s", orderNumber, deliveryDate),
	CallbackURL: "https://yourstore.com/webhooks/sms/dlr",
	Sender:      "YourStore",
})
if err != nil {
	return err
}
Marketing Campaigns with Opt-out Compliance

The API refuses to send to a number that opted out: the message fails validation (422) and is neither sent nor billed.

_, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient: phoneNumber,
	Body:      "Special offer! Get 20% off with code SAVE20. Reply STOP to opt out.",
	Sender:    "YourBrand",
})

var validationError *calisero.ValidationError

switch {
case errors.As(err, &validationError) && len(validationError.ValidationErrors["recipient"]) > 0:
	fmt.Printf("Not sent to %s: %s\n", phoneNumber, strings.Join(validationError.ValidationErrors["recipient"], " "))
case err != nil:
	return err
}

// When a customer asks to stop receiving messages
_, err = client.OptOuts.Create(ctx, &calisero.CreateOptOutRequest{Phone: phoneNumber, Reason: "Replied STOP"})
Advanced Message Creation
response, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient:   "+40742***350",
	Body:        "Your code is 123456. Manage alerts: https://yoursite.com/alerts",
	VisibleBody: "Your code is ******. Manage alerts: https://yoursite.com/alerts", // Shown in answers and the dashboard
	Validity:    24,                                                                // Validity period, in hours
	ScheduleAt:  time.Date(2026, 12, 25, 8, 0, 0, 0, time.UTC),                     // Sent at 10:00, Romania time
	CallbackURL: "https://yoursite.com/webhook",                                    // Delivery reports
	Sender:      "Calisero",                                                        // One of your approved sender IDs
	ShortenURLs: true,                                                              // Shorten the links of the body
})
if err != nil {
	return err
}

fmt.Println(response.Data.ScheduledAt)

ScheduleAt takes a time.Time in any time zone, converted to the YYYY-MM-DD HH:MM:SS Romania time the API reads, summer and winter time handled; the zero time sends the message at once. A time already past today sends it at once too; the API refuses an earlier day, and accepts up to 5 years ahead.

Authentication

Using an API Key
client, err := calisero.NewClient("your-api-key-here")
Custom Authentication Provider

Pass an AuthProvider with WithAuthProvider instead of the key: its Token is called before every request, with the request's context, e.g. to read the key from a secret manager and rotate it without a restart.

provider := calisero.AuthProviderFunc(func(ctx context.Context) (string, error) {
	return secretManager.Read(ctx, "calisero-api-key")
})

client, err := calisero.NewClient("", calisero.WithAuthProvider(provider))

An error the provider returns fails the call before anything is sent.

πŸ“š Examples

This library includes runnable examples for every operation in the examples/ directory:

πŸ“± Message Examples
🚫 Opt-out Examples (GDPR Compliance)
βœ… Verification Examples (OTP)
πŸ‘€ Account Examples
πŸ“¬ Webhook Examples
πŸ›‘οΈ Error Handling
πŸš€ Running Examples
git clone https://github.com/calisero/calisero-go.git
cd calisero-go
CALISERO_API_KEY=your-api-key-here CALISERO_TEST_PHONE=+40742123456 go run ./examples/messages/send_simple_sms

Note: Examples use masked phone numbers (+40742***350) unless CALISERO_TEST_PHONE is set. Use a sandbox API key to try them without sending real messages.

The documentation on pkg.go.dev shows short examples too, next to the functions they use.

API Reference

Every call takes a context.Context first, which cancels it or bounds it with a deadline. A single resource comes as a *calisero.Response[T] (Data, ResponseMeta), a list as a *calisero.Page[T] (Data, Links, Meta, ResponseMeta).

Messages
Send a Message
response, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient: "+40742***350",
	Body:      "Hello World!",
})
if err != nil {
	return err
}

message := response.Data
fmt.Println(message.ID)     // Message UUID
fmt.Println(message.Status) // scheduled, then sent, delivered or undelivered
fmt.Println(message.Parts)  // Number of SMS parts

// What the answer's headers report
fmt.Println(response.ResponseMeta.TraceID) // The request's trace ID
if remaining := response.ResponseMeta.DailyRemaining; remaining != nil {
	fmt.Println(*remaining) // Messages left today (nil: no daily limit)
}
Field Type Description
Recipient string Required. Mobile number in E.164 format, e.g. +40742123456
Body string Required. The text, at most 1600 characters
VisibleBody string Shown instead of Body in the API's answers and the dashboard
Validity int Validity period, in hours
ScheduleAt time.Time When to send it, converted to Romania time; the zero time sends it at once
CallbackURL string Where Calisero posts the delivery status updates
Sender string One of your approved sender IDs; the default sender when empty
ShortenURLs bool Shorten the http(s):// links of the body

The optional fields left at their zero value are not sent.

Shorten URLs

Set ShortenURLs to true and Calisero replaces every http:// and https:// link of the body with a short one before sending, so the SMS gets shorter (and may take fewer parts). Each shortened link comes back with its click statistics:

response, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient:   "+40742***350",
	Body:        "Your order has shipped! Track it: https://yourstore.com/orders/12345/tracking",
	ShortenURLs: true,
})
if err != nil {
	return err
}

for _, link := range response.Data.ShortenedURLs {
	fmt.Println(link.OriginalLink)  // https://yourstore.com/orders/12345/tracking
	fmt.Println(link.ShortenedLink) // https://calisero.ro/s/ghJKPV
}

// Later: how many times the links were opened
later, err := client.Messages.Get(ctx, response.Data.ID)
if err != nil {
	return err
}

for _, link := range later.Data.ShortenedURLs {
	fmt.Println(link.ClickCount, link.LastClick) // Number of clicks, last click or nil
}
Get Message Details
response, err := client.Messages.Get(ctx, "message-uuid-here")
if err != nil {
	return err
}

message := response.Data
fmt.Println(message.Recipient, message.Body, message.Status)
fmt.Println(message.SentAt, message.DeliveredAt) // nil until sent, until delivered
List Messages
page, err := client.Messages.List(ctx, nil) // Newest first, 50 a page
if err != nil {
	return err
}

for _, message := range page.Data {
	fmt.Printf("%s: %s\n", message.ID, message.Body)
}

fmt.Printf("Current page: %d\n", page.Meta.CurrentPage)

if page.HasNextPage() {
	next, err := client.Messages.List(ctx, &calisero.ListOptions{Page: page.Meta.CurrentPage + 1})
	if err != nil {
		return err
	}

	fmt.Printf("Next page: %d messages\n", len(next.Data))
}
Delete a Message

Only a message still scheduled can be cancelled:

err := client.Messages.Delete(ctx, "message-uuid-here")

var forbidden *calisero.ForbiddenError

switch {
case errors.As(err, &forbidden):
	fmt.Println("Cannot delete: the message is already queued, sent or delivered")
case err != nil:
	return err
default:
	fmt.Println("Message cancelled")
}
Verifications (OTP)
Create a Verification
response, err := client.Verifications.Create(ctx, &calisero.CreateVerificationRequest{
	Phone: "+40742***350",
	Brand: "Calisero", // Named in the default message; required unless a template is given
	// Template:  "Your Calisero code is {code}", // Or a message of your own, holding {code}
	// ExpiresIn: 10,                             // Minutes the code stays valid, 1 to 10 (5 by default)
})
if err != nil {
	return err
}

verification := response.Data
fmt.Println(verification.ID, verification.Status) // unverified
fmt.Println(verification.ExpiresAt)

// The code's SMS counts towards the account's daily sending limit
if remaining := response.ResponseMeta.DailyRemaining; remaining != nil {
	fmt.Println(*remaining, "messages left today")
}
Get Verification Details
response, err := client.Verifications.Get(ctx, "verification-uuid-here")
if err != nil {
	return err
}

verification := response.Data
fmt.Println(verification.Status)     // unverified or verified
fmt.Println(verification.VerifiedAt) // Timestamp or nil
fmt.Println(verification.Expired, verification.Attempts)
List Verifications
// Optionally only the verified or the unverified ones
page, err := client.Verifications.List(ctx, &calisero.VerificationListOptions{
	Page:   1,
	Status: calisero.VerificationStatusVerified,
})
if err != nil {
	return err
}

for _, verification := range page.Data {
	fmt.Printf("%s | %s | %s\n", verification.ID, verification.Phone, verification.Status)
}
Validate a Verification Code (OTP)

The code is checked against the latest verification of the phone number, in any case:

response, err := client.Verifications.Validate(ctx, &calisero.VerificationCheckRequest{
	Phone: "+40742***350",
	Code:  "ABC123",
})
if err != nil {
	return err
}

fmt.Println(response.Data.Status) // verified (checking a verified code again succeeds)

A wrong or expired code, or a sixth attempt, returns a *calisero.ValidationError whose ValidationErrors["code"] says which; a phone number without a verification a *calisero.NotFoundError.

Opt-outs
Create an Opt-out
response, err := client.OptOuts.Create(ctx, &calisero.CreateOptOutRequest{
	Phone:  "+40742***350",
	Reason: "User requested opt-out via website",
})
if err != nil {
	return err
}

fmt.Printf("Opt-out created with ID: %s\n", response.Data.ID)
List Opt-outs
page, err := client.OptOuts.List(ctx, nil)
if err != nil {
	return err
}

for _, optOut := range page.Data {
	fmt.Printf("%s: %s\n", optOut.Phone, cmp.Or(optOut.Reason, "no reason"))
}
Get, Update and Delete an Opt-out
response, err := client.OptOuts.Get(ctx, "optout-uuid-here")
if err != nil {
	return err
}

_, err = client.OptOuts.Update(ctx, "optout-uuid-here", &calisero.UpdateOptOutRequest{
	Phone:  response.Data.Phone,
	Reason: "Updated reason",
})
if err != nil {
	return err
}

// The number can receive messages again
err = client.OptOuts.Delete(ctx, "optout-uuid-here")
Accounts
response, err := client.Accounts.Get(ctx, "account-uuid-here")
if err != nil {
	return err
}

account := response.Data
fmt.Printf("Account: %s\n", account.Name)
fmt.Printf("Credit: %.2f\n", account.Credit)
fmt.Printf("Status: %s\n", account.Status) // active, pending or disabled
fmt.Printf("Sandbox: %t\n", account.Sandbox)

// Daily sending limit (nil when no daily limit applies)
if account.DailyLimit != nil && account.DailyRemaining != nil {
	fmt.Printf("Daily limit: %d, left today: %d\n", *account.DailyLimit, *account.DailyRemaining)
}

fmt.Printf("Sent today: %d\n", account.SentToday)
Delivery Status Webhooks

When a message has a CallbackURL, Calisero posts a JSON payload to it each time the message's status changes (sent, delivered or undelivered). Answer with any 2xx status within 2 seconds: only a failed connection or a slower answer is retried (at most 5 attempts in total), a non-2xx answer is not.

calisero.DeliveryWebhookHandler is a ready http.Handler: it reads and validates the payload, hands it to your OnMessage as a typed *calisero.DeliveryWebhookMessage, and answers.

mux := http.NewServeMux()
mux.Handle("POST /webhooks/calisero", &calisero.DeliveryWebhookHandler{
	// Optional: accept only callback URLs that carry ?token=<this secret>
	Token: os.Getenv("CALISERO_WEBHOOK_TOKEN"),

	OnMessage: func(r *http.Request, message *calisero.DeliveryWebhookMessage) error {
		// message.MessageID        The message's UUID
		// message.Status           calisero.DeliveryStatusSent, DeliveryStatusDelivered or DeliveryStatusUndelivered
		// message.DeliveredAt      Set for delivered
		// message.Price            Price charged for the message's parts
		// message.RemainingBalance Account balance after billing
		// message.DailyRemaining   Messages left today when it was sent (nil: no daily limit)

		// Keep it short (update a record, queue a job): the answer waits for it
		return orders.UpdateDelivery(r.Context(), message.MessageID, message.Status)
	},

	OnError: func(r *http.Request, err error) { log.Println(err) }, // A refused callback, or a failed OnMessage
})

Routers built on net/http mount it as is (chi: router.Method(http.MethodPost, "/webhooks/calisero", handler)); with gin, router.POST("/webhooks/calisero", gin.WrapH(handler)); with echo, e.POST("/webhooks/calisero", echo.WrapHandler(handler)).

Then send the URL with the secret as each message's callback URL: CallbackURL: "https://yourapp.com/webhooks/calisero?token=" + os.Getenv("CALISERO_WEBHOOK_TOKEN").

Answer When
200 {"received":true} OnMessage handled the update
400 / 413 The payload is not a delivery status update / is larger than MaxBodySize (64 KiB)
401 Token is set and the request URL lacks it (compared in constant time)
405 Not a POST
500 OnMessage returned an error

Calisero does not sign its callbacks, which is why the handler checks a secret token in the URL instead. Elsewhere, read the payload yourself with calisero.ParseDeliveryWebhook(body), or let your framework bind it: DeliveryWebhookMessage implements json.Unmarshaler with the same validation. Both return an *calisero.InvalidWebhookPayloadError for a malformed payload (and for the [] Calisero sends when the message was cancelled meanwhile).

Daily Sending Limit

Every account has its own daily sending limit: how many messages it can send in a day. A new account starts with a default limit and keeps it until Calisero raises it on request.

  • Every real message counts once, whatever its number of parts: the messages of Messages.Create() and the codes of Verifications.Create() alike. A scheduled message counts on the day it is created.
  • Test messages (a sandbox account or a sandbox API key) never count and are never refused.
  • The day ends at midnight, Romania time (Europe/Bucharest), whatever time zone you send from.

You can see the limit and what is left of it in three places:

// 1. On the account: DailyLimit and DailyRemaining (nil when no daily limit applies), and SentToday
account, err := client.Accounts.Get(ctx, "account-uuid-here")
if err != nil {
	return err
}

if limit, remaining := account.Data.DailyLimit, account.Data.DailyRemaining; limit != nil && remaining != nil {
	fmt.Println(*limit, *remaining, account.Data.SentToday) // e.g. 1000 873 127
}

// 2. After every message or verification created (X-Daily-Limit / X-Daily-Remaining)
response, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{Recipient: "+40742***350", Body: "Hello!"})
if err != nil {
	return err
}

if remaining := response.ResponseMeta.DailyRemaining; remaining != nil {
	fmt.Println(*remaining) // e.g. 872, after this message
}

// 3. In every delivery status callback: message.DailyLimit, message.DailyRemaining, message.SentToday

Once the limit is reached, new messages and verifications are refused with a *calisero.DailyLimitExceededError until midnight, Romania time. Nothing is sent and nothing is billed:

_, err := client.Messages.Create(ctx, request)

var dailyLimit *calisero.DailyLimitExceededError
if errors.As(err, &dailyLimit) {
	fmt.Println(dailyLimit.Message)        // What happened and when the limit resets
	fmt.Println(dailyLimit.DailyLimit)     // e.g. 1000
	fmt.Println(dailyLimit.DailyRemaining) // 0
	fmt.Println(dailyLimit.ResetsAt)       // e.g. 2026-10-01 00:00:00 +0300 +0300
	fmt.Println(dailyLimit.RetryAfter)     // How long until the reset, e.g. 6h0m0s
}

To raise the limit, contact Calisero with your account and the daily volume you expect.

Rate Limits & Trace ID

Request Rate Limit

Separately from the daily sending limit, each API user may make 240 requests a minute, on every endpoint. Above that the API answers 429 and the library returns a *calisero.RateLimitedError:

_, err := client.Messages.Create(ctx, request)

var dailyLimit *calisero.DailyLimitExceededError
var rateLimit *calisero.RateLimitedError

switch {
case errors.As(err, &dailyLimit):
	// The daily sending limit: wait until dailyLimit.ResetsAt
case errors.As(err, &rateLimit):
	// The request rate limit: wait a few seconds and retry
	time.Sleep(cmp.Or(rateLimit.RetryAfter, time.Second))
	fmt.Println(rateLimit.RateLimitLimit)     // 240
	fmt.Println(rateLimit.RateLimitRemaining) // 0
	fmt.Println(rateLimit.RateLimitReset)     // When requests are accepted again
}

A *calisero.DailyLimitExceededError is also a *calisero.RateLimitedError, so test for it first. After any successful call, ResponseMeta.RateLimitRemaining tells how many requests are left in the current minute.

Trace ID

Every API answer carries the request's trace ID. Log it: quoting it to support lets Calisero find the request at once, and the dashboard shows everything that happened to it under Developers β†’ Debug.

response, err := client.Messages.Create(ctx, request)

var apiError *calisero.APIError

switch {
case errors.As(err, &apiError):
	// On error
	log.Printf("Calisero request %s failed: %s", apiError.TraceID, apiError.Message)
case err == nil:
	// On success
	log.Println(response.ResponseMeta.TraceID)
}

Error Handling

Every error the API answers with is a *calisero.APIError, wrapped in the type that matches its status. Test for them with errors.As, the most specific first:

_, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{Recipient: "+40742***350", Body: "Test message"})

var (
	unauthorized    *calisero.UnauthorizedError
	validationError *calisero.ValidationError
	dailyLimit      *calisero.DailyLimitExceededError
	rateLimit       *calisero.RateLimitedError
	serverError     *calisero.ServerError
	transportError  *calisero.TransportError
	apiError        *calisero.APIError
)

switch {
case err == nil:
	fmt.Println("Message sent")
case errors.As(err, &unauthorized):
	fmt.Println("Invalid or revoked API key") // 401
case errors.As(err, &validationError):
	fmt.Println("Validation error:", validationError.Message) // 422
	for field, messages := range validationError.ValidationErrors {
		fmt.Printf("%s: %s\n", field, strings.Join(messages, ", "))
	}
case errors.As(err, &dailyLimit):
	fmt.Println("Daily limit reached. Sending resumes at:", dailyLimit.ResetsAt) // 429
case errors.As(err, &rateLimit):
	fmt.Println("Rate limited. Retry after:", rateLimit.RetryAfter) // 429
case errors.As(err, &serverError):
	fmt.Println("Server error:", serverError.Message) // 5xx
case errors.As(err, &transportError):
	fmt.Println("Network error:", transportError.Err) // No answer
case errors.As(err, &apiError):
	fmt.Printf("API error %d: %s (trace ID: %s)\n", apiError.StatusCode, apiError.Message, apiError.TraceID)
default:
	return err // An invalid argument, or the error of your auth provider
}

Every *calisero.APIError carries the API's own error message (Message), the HTTP status (StatusCode), the request's trace ID (TraceID; RequestID() returns the same value), the decoded error body (Details) and the error that caused it, if any (Err). Its Error() names the status and the trace ID: calisero: Resource not found! (status 404, trace ID 9b80eef1-49d4-4502-85a8-febb68cc11a7).

Error When
*UnauthorizedError 401: invalid, revoked or missing API key, or a request from an IP the key does not allow
*ForbiddenError 403: not allowed, e.g. deleting a message already sent, or another account's resource
*NotFoundError 404: the resource does not exist
*ValidationError 422: invalid request data; field errors in ValidationErrors
*DailyLimitExceededError 429: the account reached its daily sending limit
*RateLimitedError 429: more than 240 requests a minute
*ServerError 500, 502, 503, 504
*APIError Any other API error, or an answer the library cannot read; wrapped by all of the above
*TransportError The request got no answer: network error, timeout or cancelled context (the original error is its Err)

Arguments that cannot be sent at all (a nil request, an empty ID, a negative page) return a plain error before any request is made; so does an error of your auth provider.

Retries

The library never retries a request on its own. The API takes no idempotency key, so a message retried after a timeout or a server error may already have been sent: check with Messages.List() before sending it again. The API itself refuses (422, duplicate_sms) the same message to the same recipient sent again within 5 seconds.

Advanced Configuration

Client Options
client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"),
	calisero.WithTimeout(10*time.Second),                    // How long to wait for each answer (30 seconds by default)
	calisero.WithBaseURL("https://rest.calisero.ro/api/v1"), // The default
	calisero.WithAppInfo("MyShop", "2.1.0"),                 // Named in the User-Agent header
)

Every request goes to https://rest.calisero.ro/api/v1 with a 30-second timeout and the headers Authorization: Bearer <key>, Accept: application/json, Content-Type: application/json and the User-Agent below. The default HTTP client follows no redirect: the API answers with one only when something is wrong, and the API key stays on its host.

The API accepts TLS 1.2 and TLS 1.3 only, and refuses older versions from 1 November 2026. Go's HTTP client offers TLS 1.2 and 1.3 by default, so the library needs nothing for it; a client given with WithHTTPClient must not set a tls.Config whose MaxVersion is below TLS 1.2. Keep the base URL on https://: the API redirects http://, and the default client answers that redirect as an error rather than resending the request.

User-Agent

Every request names the library and its version, then the runtime, so Calisero can tell its requests apart and see which version sent them:

User-Agent: Calisero-SMS-Go/1.0.0 (Go 1.27.1; linux amd64)

A request whose User-Agent starts with Calisero-SMS-Go/ comes from this library. Give your application's name with WithAppInfo and it follows, so the requests of each of your applications can be told apart too:

client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"), calisero.WithAppInfo("MyShop", "2.1.0"))
if err != nil {
	log.Fatal(err)
}

fmt.Println(client.UserAgent())
// Calisero-SMS-Go/1.0.0 (Go 1.27.1; linux amd64) MyShop/2.1.0

Characters a header does not allow (spaces, diacritics…) become - in the name and the version.

Through a Proxy

The default HTTP client uses Go's default transport, which reads the HTTPS_PROXY, HTTP_PROXY and NO_PROXY environment variables: a proxy set there needs no code. Otherwise, pass an *http.Client of your own:

proxyURL, err := url.Parse("http://proxy.internal:3128")
if err != nil {
	log.Fatal(err)
}

client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"), calisero.WithHTTPClient(&http.Client{
	Timeout:   30 * time.Second,
	Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
	// Answer a redirect as an error, as the default client does
	CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}))

A client given with WithHTTPClient is used as configured, its timeout and its redirect policy included.

Cancelling a Request
// Give up after 5 seconds, or when the incoming request is cancelled
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()

page, err := client.Messages.List(ctx, nil)

A cancelled call returns a *calisero.TransportError: errors.Is(err, context.Canceled) or errors.Is(err, context.DeadlineExceeded) tells why.

Custom HTTP Client

WithHTTPClient takes any calisero.HTTPDoer: an *http.Client, or anything with its Do method. Wrap the transport to log the traffic, add headers or plug in instrumentation:

type loggingTransport struct {
	next http.RoundTripper
}

func (t loggingTransport) RoundTrip(request *http.Request) (*http.Response, error) {
	startedAt := time.Now()
	response, err := t.next.RoundTrip(request)

	if err == nil {
		log.Printf("%s %s β†’ %d in %s", request.Method, request.URL, response.StatusCode, time.Since(startedAt))
	}

	return response, err
}

client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"), calisero.WithHTTPClient(&http.Client{
	Timeout:       30 * time.Second,
	Transport:     loggingTransport{next: http.DefaultTransport},
	CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}))

With OpenTelemetry, the transport is otelhttp.NewTransport(http.DefaultTransport).

Concurrency

A Client keeps no state between requests: create it once, at startup, and share it between goroutines. Each call's answer, headers included, is in its own result.

Types

Requests, resources, pages and errors are all typed. Statuses are typed string constants (MessageStatus, VerificationStatus, AccountStatus, DeliveryStatus) that still hold the values a future API version may add.

What the API sends as null reads as the zero value: an empty string, or a nil pointer for the dates and the numbers where zero means something (DailyRemaining is 0 once the limit is reached, nil when no limit applies).

The resources carry the API's field names in JSON (created_at, daily_limit…) and read the API's JSON with json.Unmarshal the way the client does: a required field missing or malformed is an error naming it, and amounts are read whether they come as numbers or as strings.

Testing

go test ./...                   # Run the tests
go test -race ./...             # With the race detector, as CI runs them
go test -cover .                # With the coverage of the library
go vet ./...                    # Vet the library and the examples
golangci-lint run               # Lint (.golangci.yml)

TESTING.md describes the suite: unit tests, integration tests over real HTTP against a local server, contract tests against the API's OpenAPI document, and the examples of the documentation.

Testing Your Implementation

Point the client at an httptest.Server: your tests then run without the network and see every request.

func TestSendsTheOrderConfirmation(t *testing.T) {
	var received map[string]any

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = json.NewDecoder(r.Body).Decode(&received)

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		_, _ = io.WriteString(w, `{"data": {"id": "9e2574e8-3615-4090-9b5a-0fc812079da8", "recipient": "+40742***350",
			"body": "Order #1001 confirmed!", "parts": 1, "created_at": "2025-02-06T10:18:43.000000Z", "status": "scheduled"}}`)
	}))
	defer server.Close()

	client, err := calisero.NewClient("test-key", calisero.WithBaseURL(server.URL))
	if err != nil {
		t.Fatal(err)
	}

	if err := sendOrderConfirmation(context.Background(), client, order); err != nil { // Your code
		t.Fatal(err)
	}

	if received["body"] != "Order #1001 confirmed!" {
		t.Errorf("sent %v", received)
	}
}

To test error handling, answer with the error the API sends, e.g. w.WriteHeader(422) and {"message": "The recipient field is required.", "errors": {"recipient": ["The recipient field is required."]}}.

Contributing

Please see CONTRIBUTING.md for details on how to contribute to this project.

Security

If you discover any security-related issues, please email support@calisero.ro instead of using the issue tracker.

License

This library is open-sourced software licensed under the MIT license.

Support

Changelog

Please see CHANGELOG.md for more information on what has changed recently.

Documentation ΒΆ

Overview ΒΆ

Package calisero is the official Go library for the Calisero transactional SMS API (https://calisero.ro).

Send SMS messages, verify phone numbers with one-time codes, manage opt-outs for GDPR compliance, receive delivery reports and monitor your account. The library has no dependency outside the standard library.

Getting Started ΒΆ

Create a client with your API key, from the dashboard's API Keys section, then send a message:

client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
if err != nil {
	log.Fatal(err)
}

response, err := client.Messages.Create(ctx, &calisero.CreateMessageRequest{
	Recipient: "+40742123456",
	Body:      "Hello from Calisero!",
})
if err != nil {
	log.Fatal(err)
}

fmt.Println(response.Data.ID, response.Data.Status)

A Client is safe for concurrent use by multiple goroutines: create it once and share it. Every call takes a context, which cancels it.

Services ΒΆ

The client groups the API's operations by resource:

  • Client.Messages sends messages, now or later, lists them, reads their delivery status and cancels the scheduled ones.
  • Client.Verifications sends one-time codes (OTP, 2FA) and checks them.
  • Client.OptOuts manages the phone numbers that asked to receive no message.
  • Client.Accounts reads the account: credit, status and daily sending limit.

A single resource comes as a Response, a list as a Page. Both carry a ResponseMeta, read from the answer's headers: the request's trace ID, the request rate limit and, for a created message or verification, what is left of the account's daily sending limit.

Errors ΒΆ

Every error the API answers with is an *APIError, wrapped in the type that matches its status: *UnauthorizedError (401), *ForbiddenError (403), *NotFoundError (404), *ValidationError (422), *RateLimitedError (429), *DailyLimitExceededError (429, the daily sending limit), *ServerError (5xx). A request that got no answer returns a *TransportError. Test for them with errors.As:

var validationError *calisero.ValidationError
if errors.As(err, &validationError) {
	fmt.Println(validationError.ValidationErrors["recipient"])
}

The client never retries a request: the API takes no idempotency key, so a message retried after a timeout or a server error may already have been sent.

Delivery Status Webhooks ΒΆ

When a message has a callback URL, Calisero posts its delivery status updates to it. ParseDeliveryWebhook reads and validates a callback's body, and DeliveryWebhookHandler answers the callbacks for net/http:

http.Handle("POST /webhooks/calisero", &calisero.DeliveryWebhookHandler{
	Token: os.Getenv("CALISERO_WEBHOOK_TOKEN"),
	OnMessage: func(r *http.Request, message *calisero.DeliveryWebhookMessage) error {
		log.Println(message.MessageID, message.Status)
		return nil
	},
})

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

View Source
const DailyLimitExceededCode = "daily_limit_exceeded"

DailyLimitExceededCode is the code of the error body that tells a refusal of the account's daily sending limit from a refusal of the request rate limit.

View Source
const DefaultBaseURL = "https://rest.calisero.ro/api/v1"

DefaultBaseURL is the production API.

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout is how long the default HTTP client waits for each answer, headers and body included.

View Source
const DefaultWebhookMaxBodySize = 64 << 10

DefaultWebhookMaxBodySize is the largest callback body a DeliveryWebhookHandler accepts when its MaxBodySize is not set: 64 KiB.

View Source
const UserAgent = "Calisero-SMS-Go/" + Version

UserAgent is the product token that opens the User-Agent header of every request: the library and its version. A request whose User-Agent starts with "Calisero-SMS-Go/" comes from this library.

View Source
const Version = "1.0.1"

Version is the library's version, sent in the User-Agent header. A test keeps it in step with the latest version of CHANGELOG.md.

Variables ΒΆ

View Source
var ErrInvalidWebhookToken = errors.New("calisero: invalid webhook token: the request URL lacks the expected token")

ErrInvalidWebhookToken is reported to DeliveryWebhookHandler.OnError for a callback whose URL lacks the handler's token.

Functions ΒΆ

This section is empty.

Types ΒΆ

type APIError ΒΆ

type APIError struct {
	// Message is the API's own description of the error, e.g. "Resource not
	// found!"; "HTTP error 404" when the answer carried none.
	Message string

	// StatusCode is the HTTP status of the answer; 0 when the request got no answer.
	StatusCode int

	// TraceID is the request's trace ID, from the X-Trace-Id header or the error
	// body's trace_id: quote it to Calisero support, or look the request up in the
	// dashboard under Developers β†’ Debug. Empty when the answer carried none.
	TraceID string

	// Details is the decoded JSON error body; nil when the answer had none.
	Details map[string]any

	// Err is the error that caused this one: the network error of a
	// *TransportError, the decoding error of an answer that could not be read.
	Err error
}

APIError is an error the Calisero API answered with, or the error of a request that got no answer (a *TransportError). Every error a call returns once its request was sent is one, wrapped in the type that matches the answer's status: test for them with errors.As, the specific types first.

Example ΒΆ
package main

import (
	"context"
	"errors"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.Messages.Get(context.Background(), "9e2574e8-3615-4090-9b5a-0fc812079da8")

	var apiError *calisero.APIError
	if errors.As(err, &apiError) {
		// Quote the trace ID to support, or look it up under Developers β†’ Debug
		log.Printf("Calisero request %s failed with %d: %s", apiError.TraceID, apiError.StatusCode, apiError.Message)
	}
}

func (*APIError) Error ΒΆ

func (e *APIError) Error() string

Error describes the error with its status and trace ID, e.g. "calisero: Resource not found! (status 404, trace ID 9b80eef1-49d4-4502-85a8-febb68cc11a7)".

func (*APIError) RequestID ΒΆ

func (e *APIError) RequestID() string

RequestID is the request's trace ID, the same as TraceID.

func (*APIError) Unwrap ΒΆ

func (e *APIError) Unwrap() error

Unwrap returns the error that caused this one, if any.

type Account ΒΆ

type Account struct {
	ID string `json:"id"`

	Code string `json:"code"`

	Name string `json:"name"`

	Description string `json:"description"`

	FiscalCode string `json:"fiscal_code"`

	RegistryNumber string `json:"registry_number"`

	IBAN string `json:"iban"`

	City string `json:"city"`

	State string `json:"state"`

	Country string `json:"country"`

	Address string `json:"address"`

	PostalCode *int `json:"postal_code"`

	Email string `json:"email"`

	Phone string `json:"phone"`

	ContactPerson string `json:"contact_person"`

	// Credit is the account's credit balance.
	Credit float64 `json:"credit"`

	Status AccountStatus `json:"status"`

	// Sandbox reports whether this is a sandbox account: its messages are test
	// messages, marked delivered at once, never sent to a phone nor billed.
	Sandbox bool `json:"sandbox"`

	// DailyLimit is how many messages the account can send in a day, midnight to
	// midnight, Romania time; nil when no daily limit applies.
	DailyLimit *int `json:"daily_limit"`

	// DailyRemaining is how many messages the account can still send today; nil
	// when no daily limit applies.
	DailyRemaining *int `json:"daily_remaining"`

	// SentToday is the number of real messages the account created today, Romania
	// time: OTP codes included, test messages left out, one per message whatever
	// its parts.
	SentToday int `json:"sent_today"`

	CreatedAt time.Time `json:"created_at"`
}

Account is a Calisero account. Its optional details are empty, or nil, when the account has none.

func (*Account) UnmarshalJSON ΒΆ

func (a *Account) UnmarshalJSON(data []byte) error

UnmarshalJSON reads an account as the API sends it, the way the client does: a required field missing or malformed is an error naming it.

type AccountService ΒΆ

type AccountService struct {
	// contains filtered or unexported fields
}

AccountService reads the account: credit, status and daily sending limit. Reach it as Client.Accounts.

func (*AccountService) Get ΒΆ

func (s *AccountService) Get(ctx context.Context, accountID string) (*Response[Account], error)

Get retrieves an account by its ID, shown in the dashboard.

type AccountStatus ΒΆ

type AccountStatus string

AccountStatus is the status of an account; other values may come with future API versions.

const (
	AccountStatusActive   AccountStatus = "active"
	AccountStatusPending  AccountStatus = "pending"
	AccountStatusDisabled AccountStatus = "disabled"
)

The statuses of an account.

type AuthProvider ΒΆ

type AuthProvider interface {
	Token(ctx context.Context) (string, error)
}

AuthProvider provides the bearer token the API authenticates requests with: the API key.

Token is called before every request, with the request's context, so it may read the key from a secret manager and rotate it without a restart. An error it returns fails the request before anything is sent.

type AuthProviderFunc ΒΆ

type AuthProviderFunc func(ctx context.Context) (string, error)

AuthProviderFunc adapts a function to an AuthProvider.

func (AuthProviderFunc) Token ΒΆ

func (f AuthProviderFunc) Token(ctx context.Context) (string, error)

Token returns f(ctx).

type Client ΒΆ

type Client struct {
	// Messages sends and manages SMS messages.
	Messages *MessageService

	// OptOuts manages the phone numbers that opted out of receiving messages.
	OptOuts *OptOutService

	// Accounts reads the account: credit, status and daily sending limit.
	Accounts *AccountService

	// Verifications verifies phone numbers with one-time codes.
	Verifications *VerificationService
	// contains filtered or unexported fields
}

Client is the Calisero SMS API client. Create it with NewClient, once, and share it: a Client is safe for concurrent use by multiple goroutines.

It never retries a request: the API takes no idempotency key, so a message retried after a timeout or a server error could be sent twice.

func NewClient ΒΆ

func NewClient(apiKey string, options ...Option) (*Client, error)

NewClient creates a client that authenticates with apiKey, a key of the dashboard's API Keys section, or with the provider of WithAuthProvider.

It returns an error when the API key is empty and no provider is given, or when an option is invalid: a misconfigured environment fails at startup rather than on the first message.

Example ΒΆ
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	response, err := client.Messages.Create(context.Background(), &calisero.CreateMessageRequest{
		Recipient: "+40742123456",
		Body:      "Hello from Calisero!",
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(response.Data.ID, response.Data.Status)
}
Example (Options) ΒΆ
package main

import (
	"fmt"
	"log"
	"os"
	"time"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"),
		calisero.WithTimeout(10*time.Second),          // 30 seconds by default
		calisero.WithAppInfo("MyShop", "2.1.0"),       // Named in the User-Agent header
		calisero.WithBaseURL(calisero.DefaultBaseURL), // The default
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(client.UserAgent()) // Calisero-SMS-Go/1.0.0 (Go 1.27.1; linux amd64) MyShop/2.1.0
}

func (*Client) BaseURL ΒΆ

func (c *Client) BaseURL() string

BaseURL is the base URL every request path is appended to, without a trailing slash: DefaultBaseURL unless WithBaseURL gave another one.

func (*Client) UserAgent ΒΆ

func (c *Client) UserAgent() string

UserAgent is the User-Agent header of every request: the library and its version, the Go release and the platform, then your application, e.g. "Calisero-SMS-Go/1.0.0 (Go 1.27.1; linux amd64) MyShop/2.1.0".

type CreateMessageRequest ΒΆ

type CreateMessageRequest struct {
	// Recipient is the recipient's mobile number in international E.164 format,
	// e.g. +40742123456. Required.
	Recipient string

	// Body is the text sent, at most 1600 characters. Required.
	Body string

	// VisibleBody is what the API shows instead of Body in its answers, its lists
	// and the dashboard, e.g. the text with a code masked: "Your code is ******".
	VisibleBody string

	// Validity is the message's validity period, in hours.
	Validity int

	// ScheduleAt is when to send the message, converted to the Romania time the
	// API reads; the zero time sends it at once, as does a time already past
	// today. From today to 5 years ahead.
	ScheduleAt time.Time

	// CallbackURL is where Calisero posts the message's delivery status updates.
	CallbackURL string

	// Sender is one of the account's approved sender IDs, exactly as approved; the
	// default sender when empty.
	Sender string

	// ShortenURLs replaces the http:// and https:// links of the body with short
	// ones, whose clicks are counted. The body is sent as written otherwise.
	ShortenURLs bool
}

CreateMessageRequest is a message to send. The optional fields left at their zero value are not sent.

type CreateOptOutRequest ΒΆ

type CreateOptOutRequest struct {
	// Phone is the phone number in international E.164 format. Required.
	Phone string

	// Reason is why the number opted out, at most 600 characters.
	Reason string
}

CreateOptOutRequest is a phone number to opt out of receiving messages.

type CreateVerificationRequest ΒΆ

type CreateVerificationRequest struct {
	// Phone is the phone number to verify, in international E.164 format. Required.
	Phone string

	// Brand is the brand named in the default message, at most 120 characters:
	// required unless a Template is given.
	Brand string

	// Template is a message of your own, at most 600 characters, that must hold
	// {code}.
	Template string

	// ExpiresIn is how many minutes the code stays valid, 1 to 10: 5 when 0.
	ExpiresIn int
}

CreateVerificationRequest is a verification to start: Calisero generates a code and sends it by SMS. The optional fields left at their zero value are not sent.

type DailyLimitExceededError ΒΆ

type DailyLimitExceededError struct {
	RateLimitedError

	// DailyLimit is how many messages the account can send in a day.
	DailyLimit int

	// DailyRemaining is how many messages the account can still send today: 0 once
	// the limit is reached.
	DailyRemaining int

	// ResetsAt is the next midnight, Romania time, when the account can send
	// again, e.g. 2026-10-01T00:00:00+03:00; the zero time when the answer did not
	// say.
	ResetsAt time.Time
}

DailyLimitExceededError is returned when the account reached its daily sending limit (429 with the code daily_limit_exceeded): nothing was sent and nothing was billed.

The limit resets at midnight, Romania time (ResetsAt); test messages never count towards it. It wraps a *RateLimitedError, so code that tests for that type keeps catching this refusal too.

Example ΒΆ
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.Messages.Create(context.Background(), &calisero.CreateMessageRequest{Recipient: "+40742123456", Body: "Hello!"})

	// A *DailyLimitExceededError is also a *RateLimitedError: test for it first
	var dailyLimit *calisero.DailyLimitExceededError
	var rateLimit *calisero.RateLimitedError

	switch {
	case errors.As(err, &dailyLimit):
		fmt.Println("Nothing sent; the daily limit resets at", dailyLimit.ResetsAt)
	case errors.As(err, &rateLimit):
		fmt.Println("Too many requests; retry in", rateLimit.RetryAfter)
	case err != nil:
		log.Fatal(err)
	}
}

func (*DailyLimitExceededError) Unwrap ΒΆ

func (e *DailyLimitExceededError) Unwrap() error

Unwrap returns the embedded *RateLimitedError, so errors.As finds it.

type DeliveryStatus ΒΆ

type DeliveryStatus string

DeliveryStatus is a status a delivery status callback reports; other values may come with future API versions.

const (
	DeliveryStatusSent        DeliveryStatus = "sent"
	DeliveryStatusDelivered   DeliveryStatus = "delivered"
	DeliveryStatusUndelivered DeliveryStatus = "undelivered"
)

The statuses a delivery status callback reports.

type DeliveryWebhookHandler ΒΆ

type DeliveryWebhookHandler struct {
	// OnMessage is called with every delivery status update, before the answer,
	// which Calisero waits 2 seconds for: record the status or queue a job, then
	// return. An error it returns is answered with 500. Required.
	OnMessage func(r *http.Request, message *DeliveryWebhookMessage) error

	// Token is a secret the callback URL carries as its token query parameter,
	// e.g. https://yourapp.com/webhooks/calisero?token=…: a request without it is
	// answered with 401, the comparison taking constant time. Calisero does not
	// sign its callbacks, so this is how to accept only those of the URLs you gave
	// it. No token is checked when it is empty.
	Token string

	// OnError is called when a request is refused or OnMessage fails, e.g. to log it.
	OnError func(r *http.Request, err error)

	// MaxBodySize is the largest body accepted, in bytes: DefaultWebhookMaxBodySize
	// when it is not positive.
	MaxBodySize int64
}

DeliveryWebhookHandler answers the delivery status callbacks Calisero posts to a message's callback URL: an http.Handler for net/http and the routers built on it. It answers:

  • 200 {"received":true} once OnMessage handled the update;
  • 400 for a payload that is not a delivery status update, 413 for one larger than MaxBodySize;
  • 401 when Token is set and the request URL lacks it;
  • 405 for anything but POST;
  • 500 when OnMessage returns an error.

Calisero retries only a callback that got no answer within 2 seconds, at most 5 attempts in total, never one answered with an error.

Example ΒΆ
package main

import (
	"log"
	"net/http"
	"os"
	"time"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	http.Handle("POST /webhooks/calisero", &calisero.DeliveryWebhookHandler{
		// Accept only the callback URLs that carry ?token=<this secret>
		Token: os.Getenv("CALISERO_WEBHOOK_TOKEN"),

		// Keep it short (update a record, queue a job): the answer waits for it
		OnMessage: func(_ *http.Request, message *calisero.DeliveryWebhookMessage) error {
			log.Println(message.MessageID, message.Status, message.Price)

			return nil
		},

		OnError: func(_ *http.Request, err error) {
			log.Println(err)
		},
	})

	server := &http.Server{Addr: ":8080", ReadHeaderTimeout: 5 * time.Second}
	log.Fatal(server.ListenAndServe())
}

func (*DeliveryWebhookHandler) ServeHTTP ΒΆ

ServeHTTP answers a delivery status callback.

type DeliveryWebhookMessage ΒΆ

type DeliveryWebhookMessage struct {
	MessageID string `json:"messageId"`

	// Sender is the sender ID the message went out under; empty for the default sender.
	Sender string `json:"sender"`

	// Recipient is the recipient in E.164 format.
	Recipient string `json:"recipient"`

	// Status is the delivery status: sent, delivered or undelivered.
	Status DeliveryStatus `json:"status"`

	// ScheduleAt is when the message was scheduled to go out, if it was scheduled.
	ScheduleAt *time.Time `json:"scheduleAt"`

	// SentAt is when the message was handed to the network (statuses sent and
	// delivered).
	SentAt *time.Time `json:"sentAt"`

	// DeliveredAt is when the handset confirmed delivery (status delivered only).
	DeliveredAt *time.Time `json:"deliveredAt"`

	// Price is the price charged for the message's parts.
	Price float64 `json:"price"`

	// RemainingBalance is the account's balance after billing, if available.
	RemainingBalance *float64 `json:"remainingBalance"`

	// DailyLimit is the account's daily sending limit when the callback was sent;
	// nil when no daily limit applies.
	DailyLimit *int `json:"dailyLimit"`

	// DailyRemaining is how many messages the account could still send today when
	// the callback was sent; nil when no daily limit applies.
	DailyRemaining *int `json:"dailyRemaining"`

	// SentToday is the number of real messages the account created today, Romania
	// time, when the callback was sent: OTP codes included, test messages left
	// out, one per message whatever its parts. Nil in payloads that predate the
	// daily limit.
	SentToday *int `json:"sentToday"`
}

DeliveryWebhookMessage is the payload Calisero posts to a message's callback URL when its status changes: sent, delivered or undelivered.

It reaches your endpoint from outside your application, so it is validated: ParseDeliveryWebhook and UnmarshalJSON refuse a malformed payload with an *InvalidWebhookPayloadError.

func ParseDeliveryWebhook ΒΆ

func ParseDeliveryWebhook(body []byte) (*DeliveryWebhookMessage, error)

ParseDeliveryWebhook reads the raw body of a delivery status callback.

It returns an *InvalidWebhookPayloadError when the body is not a JSON object or a required field is missing or malformed, which includes the [] Calisero sends for a message cancelled in the meantime.

Example ΒΆ
package main

import (
	"fmt"
	"time"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	body := []byte(`{"messageId":"019961db-14a7-7348-963f-5a7a789a969f","recipient":"+40742***350","status":"delivered",
		"price":0.0378,"deliveredAt":"2025-09-19T12:02:53.000000Z","dailyLimit":1000,"dailyRemaining":588}`)

	message, err := calisero.ParseDeliveryWebhook(body)
	if err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println(message.MessageID, message.Status)
	fmt.Println(message.DeliveredAt.Format(time.RFC3339))
	fmt.Printf("%.4f\n", message.Price)
	fmt.Println(*message.DailyRemaining, "of", *message.DailyLimit, "messages left today")

	// What Calisero sends for a message cancelled in the meantime
	_, err = calisero.ParseDeliveryWebhook([]byte(`[]`))
	fmt.Println(err)

}
Output:
019961db-14a7-7348-963f-5a7a789a969f delivered
2025-09-19T12:02:53Z
0.0378
588 of 1000 messages left today
calisero: invalid delivery webhook payload: expected an object, got an array

func (*DeliveryWebhookMessage) UnmarshalJSON ΒΆ

func (m *DeliveryWebhookMessage) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a delivery status callback the way ParseDeliveryWebhook does, e.g. when a web framework binds the request's body: a malformed payload, the JSON null included, is an *InvalidWebhookPayloadError.

type ForbiddenError ΒΆ

type ForbiddenError struct {
	APIError
}

ForbiddenError is returned when access is forbidden (403), e.g. deleting a message already sent, or reading another account's resource.

func (*ForbiddenError) Unwrap ΒΆ

func (e *ForbiddenError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As finds it.

type HTTPDoer ΒΆ

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPDoer sends an HTTP request and returns the answer, whatever its status: *http.Client implements it.

It returns an error only when the request got no answer (a network error, a timeout, a cancelled context): the client turns that error into a *TransportError.

type InvalidWebhookPayloadError ΒΆ

type InvalidWebhookPayloadError struct {
	// Message says what is wrong with the payload, e.g. `invalid delivery webhook
	// payload: "messageId" must be a non-empty string, got nothing`.
	Message string

	// Err is the error that caused this one, e.g. the JSON syntax error.
	Err error
}

InvalidWebhookPayloadError is returned when a delivery status callback is not the payload Calisero sends: not JSON, not an object, too large, or a required field missing or malformed.

func (*InvalidWebhookPayloadError) Error ΒΆ

Error describes what is wrong with the payload.

func (*InvalidWebhookPayloadError) Unwrap ΒΆ

func (e *InvalidWebhookPayloadError) Unwrap() error

Unwrap returns the error that caused this one, if any.

type ListOptions ΒΆ

type ListOptions struct {
	// Page is the page to read, from 1, each holding 50 items; 0 reads the first one.
	Page int
}

ListOptions are the options of the calls that list resources.

type Message ΒΆ

type Message struct {
	ID string `json:"id"`

	// Recipient is the recipient's phone number.
	Recipient string `json:"recipient"`

	// Body is the text sent, or the VisibleBody it was sent with.
	Body string `json:"body"`

	// Parts is how many SMS parts the body takes.
	Parts int `json:"parts"`

	CreatedAt time.Time `json:"created_at"`

	// ScheduledAt is when the message is or was due to be sent.
	ScheduledAt *time.Time `json:"scheduled_at"`

	// SentAt is when the message was handed to the network; nil until it is.
	SentAt *time.Time `json:"sent_at"`

	// DeliveredAt is when the handset confirmed delivery; nil until it does.
	DeliveredAt *time.Time `json:"delivered_at"`

	// CallbackURL is where Calisero posts the message's delivery status updates;
	// empty when it has none.
	CallbackURL string `json:"callback_url"`

	Status MessageStatus `json:"status"`

	// Sender is the sender ID; empty for the default sender.
	Sender string `json:"sender"`

	// ShortenedURLs are the links of the body Calisero shortened (asked with
	// ShortenURLs), with their click statistics; empty when none were.
	ShortenedURLs []ShortenedLink `json:"shortened_urls"`
}

Message is an SMS message.

func (*Message) UnmarshalJSON ΒΆ

func (m *Message) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a message as the API sends it, the way the client does: a required field missing or malformed is an error naming it.

type MessageService ΒΆ

type MessageService struct {
	// contains filtered or unexported fields
}

MessageService sends and manages SMS messages: reach it as Client.Messages.

func (*MessageService) Create ΒΆ

Create sends a message, now or at its ScheduleAt.

Its ResponseMeta holds what is left of the account's daily sending limit after this message. Once the limit is reached it returns a *DailyLimitExceededError; a refused recipient (invalid, or opted out) a *ValidationError.

Example ΒΆ
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	response, err := client.Messages.Create(context.Background(), &calisero.CreateMessageRequest{
		Recipient:   "+40742123456",
		Body:        "Your code is 123456. Manage alerts: https://yoursite.com/alerts",
		VisibleBody: "Your code is ******. Manage alerts: https://yoursite.com/alerts", // Shown in answers and the dashboard
		Validity:    24,                                                                // Hours
		ScheduleAt:  time.Now().Add(time.Hour),                                         // Converted to Romania time
		CallbackURL: "https://yoursite.com/webhooks/calisero",                          // Delivery reports
		Sender:      "Calisero",                                                        // One of your approved sender IDs
		ShortenURLs: true,                                                              // Shorten the links of the body
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(response.Data.ID, response.Data.Status)
	fmt.Println(response.ResponseMeta.TraceID)

	// What is left of the account's daily sending limit (nil: no daily limit)
	if remaining := response.ResponseMeta.DailyRemaining; remaining != nil {
		fmt.Println(*remaining, "messages left today")
	}
}

func (*MessageService) Delete ΒΆ

func (s *MessageService) Delete(ctx context.Context, messageID string) error

Delete cancels a scheduled message. A message already queued, sent or delivered cannot be: Delete then returns a *ForbiddenError.

func (*MessageService) Get ΒΆ

func (s *MessageService) Get(ctx context.Context, messageID string) (*Response[Message], error)

Get retrieves a message by its ID.

func (*MessageService) List ΒΆ

func (s *MessageService) List(ctx context.Context, options *ListOptions) (*Page[Message], error)

List lists the messages, newest first, 50 a page; options may be nil.

Example ΒΆ
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	// Every page, newest first, 50 messages each
	for page := 1; ; page++ {
		messages, err := client.Messages.List(context.Background(), &calisero.ListOptions{Page: page})
		if err != nil {
			log.Fatal(err)
		}

		for _, message := range messages.Data {
			fmt.Println(message.ID, message.Status)
		}

		if !messages.HasNextPage() {
			break
		}
	}
}

type MessageStatus ΒΆ

type MessageStatus string

MessageStatus is the status of a message; other values may come with future API versions.

const (
	MessageStatusScheduled   MessageStatus = "scheduled"
	MessageStatusPending     MessageStatus = "pending"
	MessageStatusQueued      MessageStatus = "queued"
	MessageStatusSent        MessageStatus = "sent"
	MessageStatusDelivered   MessageStatus = "delivered"
	MessageStatusUndelivered MessageStatus = "undelivered"
	MessageStatusFailed      MessageStatus = "failed"
	MessageStatusCanceled    MessageStatus = "canceled"
)

The statuses of a message. Only a scheduled message can be cancelled.

type NotFoundError ΒΆ

type NotFoundError struct {
	APIError
}

NotFoundError is returned when a resource is not found (404).

func (*NotFoundError) Unwrap ΒΆ

func (e *NotFoundError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As finds it.

type OptOut ΒΆ

type OptOut struct {
	ID string `json:"id"`

	Phone string `json:"phone"`

	// Reason is why the number opted out; empty when none was given.
	Reason string `json:"reason"`

	CreatedAt time.Time `json:"created_at"`

	UpdatedAt time.Time `json:"updated_at"`
}

OptOut is a phone number that opted out of receiving messages.

func (*OptOut) UnmarshalJSON ΒΆ

func (o *OptOut) UnmarshalJSON(data []byte) error

UnmarshalJSON reads an opt-out as the API sends it, the way the client does: a required field missing or malformed is an error naming it.

type OptOutService ΒΆ

type OptOutService struct {
	// contains filtered or unexported fields
}

OptOutService manages the phone numbers that opted out of receiving messages: reach it as Client.OptOuts. The API refuses to send to an opted-out number.

func (*OptOutService) Create ΒΆ

func (s *OptOutService) Create(ctx context.Context, request *CreateOptOutRequest) (*Response[OptOut], error)

Create opts a phone number out.

func (*OptOutService) Delete ΒΆ

func (s *OptOutService) Delete(ctx context.Context, optOutID string) error

Delete deletes an opt-out: the number can receive messages again.

func (*OptOutService) Get ΒΆ

func (s *OptOutService) Get(ctx context.Context, optOutID string) (*Response[OptOut], error)

Get retrieves an opt-out by its ID.

func (*OptOutService) List ΒΆ

func (s *OptOutService) List(ctx context.Context, options *ListOptions) (*Page[OptOut], error)

List lists the opt-outs, 50 a page; options may be nil.

func (*OptOutService) Update ΒΆ

func (s *OptOutService) Update(ctx context.Context, optOutID string, request *UpdateOptOutRequest) (*Response[OptOut], error)

Update updates an opt-out.

type Option ΒΆ

type Option func(*settings) error

Option configures a Client: pass them to NewClient.

func WithAppInfo ΒΆ

func WithAppInfo(name, version string) Option

WithAppInfo names your application in the User-Agent header, after the library: WithAppInfo("MyShop", "2.1.0") appends "MyShop/2.1.0", so the requests of each of your applications can be told apart. Characters a header does not allow (spaces, diacritics…) become "-"; the version may be empty.

func WithAuthProvider ΒΆ

func WithAuthProvider(provider AuthProvider) Option

WithAuthProvider authenticates the requests with the token of provider instead of an API key, e.g. to read the key from a secret manager and rotate it without a restart. NewClient then ignores its API key, which may be empty.

Example ΒΆ
package main

import (
	"context"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	// Called before every request: the key can rotate without a restart
	provider := calisero.AuthProviderFunc(func(_ context.Context) (string, error) {
		return os.Getenv("CALISERO_API_KEY"), nil
	})

	client, err := calisero.NewClient("", calisero.WithAuthProvider(provider))
	if err != nil {
		log.Fatal(err)
	}

	_ = client
}

func WithBaseURL ΒΆ

func WithBaseURL(baseURL string) Option

WithBaseURL sends the requests to another base URL than DefaultBaseURL, e.g. a local server in tests. It must be an absolute http or https URL, without a query string.

func WithHTTPClient ΒΆ

func WithHTTPClient(httpClient HTTPDoer) Option

WithHTTPClient sends the requests through httpClient instead of the default HTTP client: an *http.Client with a proxy, TLS settings or instrumentation of your own, or a stub in tests.

It is used as configured, its timeout and its redirect policy included, whereas the default client waits DefaultTimeout and follows no redirect.

func WithTimeout ΒΆ

func WithTimeout(timeout time.Duration) Option

WithTimeout sets how long the default HTTP client waits for each answer, headers and body included: DefaultTimeout otherwise. It is ignored with WithHTTPClient, whose client keeps its own timeout.

type Page ΒΆ

type Page[T any] struct {
	// Data are the items of the page, at most 50; empty on an empty page.
	Data []T

	// Links are the URLs of the neighbouring pages.
	Links PaginationLinks

	// Meta is where the page stands in the whole list.
	Meta PaginationMeta

	// ResponseMeta is what the answer's headers report: the trace ID and the
	// request rate limit.
	ResponseMeta ResponseMeta
}

Page is a page of a list as the API answers it: its items, the links to the other pages, where it stands in the whole list, and what the answer's headers report.

func (*Page[T]) HasNextPage ΒΆ

func (p *Page[T]) HasNextPage() bool

HasNextPage reports whether a next page exists.

type PaginationLinks struct {
	First string `json:"first"`

	// Last is always empty: the API does not count the pages.
	Last string `json:"last"`

	Prev string `json:"prev"`

	// Next is empty on the last page.
	Next string `json:"next"`
}

PaginationLinks are the URLs of the pages around a page; empty when there is none.

type PaginationMeta ΒΆ

type PaginationMeta struct {
	CurrentPage int `json:"current_page"`

	// From is the position of the page's first item in the whole list; nil on an
	// empty page.
	From *int `json:"from"`

	Path string `json:"path"`

	PerPage int `json:"per_page"`

	// To is the position of the page's last item in the whole list; nil on an
	// empty page.
	To *int `json:"to"`
}

PaginationMeta is where a page stands in the whole list.

type RateLimitedError ΒΆ

type RateLimitedError struct {
	APIError

	// RetryAfter is how long to wait before sending again (the Retry-After
	// header); 0 when the answer did not say it in seconds.
	RetryAfter time.Duration

	// RateLimitLimit is how many requests the API user may make a minute (the
	// X-RateLimit-Limit header); 0 when the answer did not say.
	RateLimitLimit int

	// RateLimitRemaining is how many requests the API user may still make this
	// minute (the X-RateLimit-Remaining header): 0 when the request rate limit
	// refused the request.
	RateLimitRemaining int

	// RateLimitReset is when requests are accepted again (the X-RateLimit-Reset
	// header, sent only when the request rate limit refused the request); the zero
	// time when the answer did not say.
	RateLimitReset time.Time
}

RateLimitedError is returned when the request rate limit is exceeded (429): each API user may make 240 requests a minute. A refusal of the account's daily sending limit is a *DailyLimitExceededError, which wraps this type, so test for that one first.

func (*RateLimitedError) Unwrap ΒΆ

func (e *RateLimitedError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As finds it.

type Response ΒΆ

type Response[T any] struct {
	// Data is the resource.
	Data T

	// ResponseMeta is what the answer's headers report: the trace ID, the request
	// rate limit and, for a created message or verification, what is left of the
	// account's daily sending limit.
	ResponseMeta ResponseMeta
}

Response is a single resource as the API answers it: the resource (Data) and what the answer's headers report (ResponseMeta).

type ResponseMeta ΒΆ

type ResponseMeta struct {
	// TraceID is the request's trace ID (X-Trace-Id): quote it to Calisero
	// support, or look the request up in the dashboard under Developers β†’ Debug.
	// Empty when the answer did not carry it.
	TraceID string

	// RateLimitLimit is how many requests the API user may make a minute
	// (X-RateLimit-Limit).
	RateLimitLimit *int

	// RateLimitRemaining is how many requests the API user may still make this
	// minute (X-RateLimit-Remaining).
	RateLimitRemaining *int

	// DailyLimit is how many messages the account can send in a day
	// (X-Daily-Limit); nil when no daily limit applies or the answer is not to a
	// created message or verification.
	DailyLimit *int

	// DailyRemaining is how many messages the account can still send today, after
	// this one (X-Daily-Remaining); nil when no daily limit applies.
	DailyRemaining *int
}

ResponseMeta is what the API reports in the headers of an answer: the request's trace ID, the request rate limit and, for a created message or verification, the account's daily sending limit. A count is nil when the answer did not carry it.

type ServerError ΒΆ

type ServerError struct {
	APIError
}

ServerError is returned for server errors (500, 502, 503, 504).

func (*ServerError) Unwrap ΒΆ

func (e *ServerError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As finds it.

type ShortenedLink struct {
	ID string `json:"id"`

	// OriginalLink is the URL as it was written in the message body.
	OriginalLink string `json:"original_link"`

	// ShortenedLink is the short URL that replaced it in the SMS sent.
	ShortenedLink string `json:"shortened_link"`

	// ClickCount is how many times the short URL was opened.
	ClickCount int `json:"click_count"`

	// LastClick is when the short URL was last opened; nil if it never was.
	LastClick *time.Time `json:"last_click"`

	CreatedAt time.Time `json:"created_at"`
}

ShortenedLink is a link of a message body shortened by Calisero, with its click statistics.

func (*ShortenedLink) UnmarshalJSON ΒΆ

func (l *ShortenedLink) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a shortened link as the API sends it, the way the client does: a required field missing or malformed is an error naming it.

type TransportError ΒΆ

type TransportError struct {
	APIError
}

TransportError is returned when the request got no answer: a network error, a timeout or a cancelled context. Its StatusCode is 0 and the original error is its Err: errors.Is(err, context.DeadlineExceeded) tells a timeout of the context.

func (*TransportError) Unwrap ΒΆ

func (e *TransportError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As and errors.Is find it and the original error.

type UnauthorizedError ΒΆ

type UnauthorizedError struct {
	APIError
}

UnauthorizedError is returned when authentication fails (401): the API key is missing, invalid or revoked, or the request comes from an IP address the key does not allow.

func (*UnauthorizedError) Unwrap ΒΆ

func (e *UnauthorizedError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As finds it.

type UpdateOptOutRequest ΒΆ

type UpdateOptOutRequest struct {
	// Phone is the phone number in international E.164 format. Required.
	Phone string

	// Reason is why the number opted out, at most 600 characters.
	Reason string
}

UpdateOptOutRequest is the new data of an opt-out.

type ValidationError ΒΆ

type ValidationError struct {
	APIError

	// ValidationErrors are the field errors, field name β†’ messages, e.g.
	// {"recipient": ["The recipient field is required."]}; empty when the answer
	// carried none.
	ValidationErrors map[string][]string
}

ValidationError is returned for validation errors (422): the request's data was refused, e.g. an invalid or opted-out recipient, or a wrong verification code.

func (*ValidationError) Unwrap ΒΆ

func (e *ValidationError) Unwrap() error

Unwrap returns the embedded *APIError, so errors.As finds it.

type Verification ΒΆ

type Verification struct {
	ID string `json:"id"`

	Phone string `json:"phone"`

	// Brand is the brand named in the default message; empty when a template was used.
	Brand string `json:"brand"`

	Status VerificationStatus `json:"status"`

	// Template is the custom message, holding {code}; empty for the default message.
	Template string `json:"template"`

	CreatedAt time.Time `json:"created_at"`

	ExpiresAt time.Time `json:"expires_at"`

	// VerifiedAt is when the code was validated; nil until it is.
	VerifiedAt *time.Time `json:"verified_at"`

	// Attempts is how many times a code was checked against this verification.
	Attempts int `json:"attempts"`

	Expired bool `json:"expired"`
}

Verification is a phone verification: a one-time code sent by SMS, to be validated.

func (*Verification) UnmarshalJSON ΒΆ

func (v *Verification) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a verification as the API sends it, the way the client does: a required field missing or malformed is an error naming it.

type VerificationCheckRequest ΒΆ

type VerificationCheckRequest struct {
	// Phone is the phone number the code was sent to, in international E.164
	// format. Required.
	Phone string

	// Code is the 6-character code received, in any case. Required.
	Code string
}

VerificationCheckRequest is a code to check against the latest verification of a phone number.

type VerificationListOptions ΒΆ

type VerificationListOptions struct {
	// Page is the page to read, from 1, each holding 50 items; 0 reads the first one.
	Page int

	// Status keeps only the verifications with this status; all of them when empty.
	Status VerificationStatus
}

VerificationListOptions are the options of VerificationService.List.

type VerificationService ΒΆ

type VerificationService struct {
	// contains filtered or unexported fields
}

VerificationService verifies phone numbers with one-time codes (OTP, 2FA): reach it as Client.Verifications.

func (*VerificationService) Create ΒΆ

Create starts a verification: Calisero generates a code and sends it by SMS.

The code's SMS counts towards the account's daily sending limit: its ResponseMeta holds what is left of it, and once it is reached Create returns a *DailyLimitExceededError.

func (*VerificationService) Get ΒΆ

func (s *VerificationService) Get(ctx context.Context, verificationID string) (*Response[Verification], error)

Get retrieves a verification by its ID.

func (*VerificationService) List ΒΆ

List lists the verifications, newest first, 50 a page; options may be nil.

func (*VerificationService) Validate ΒΆ

Validate checks a code against the latest verification of a phone number.

A wrong, expired or exhausted code (5 attempts) returns a *ValidationError whose ValidationErrors["code"] says which; a phone number without a verification a *NotFoundError. Checking a verified code again succeeds.

Example ΒΆ
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"

	calisero "github.com/calisero/calisero-go"
)

func main() {
	client, err := calisero.NewClient(os.Getenv("CALISERO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// 1. Send a code (6 characters, valid for 5 minutes by default)
	_, err = client.Verifications.Create(ctx, &calisero.CreateVerificationRequest{Phone: "+40742123456", Brand: "YourApp"})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Check the code the user typed
	userInput := "ABC123"

	response, err := client.Verifications.Validate(ctx, &calisero.VerificationCheckRequest{Phone: "+40742123456", Code: userInput})

	var validationError *calisero.ValidationError

	switch {
	case errors.As(err, &validationError):
		// A wrong or expired code, or too many attempts (5)
		fmt.Println(validationError.ValidationErrors["code"])
	case err != nil:
		log.Fatal(err)
	default:
		fmt.Println("Verified at", response.Data.VerifiedAt)
	}
}

type VerificationStatus ΒΆ

type VerificationStatus string

VerificationStatus is the status of a verification; other values may come with future API versions.

const (
	VerificationStatusUnverified VerificationStatus = "unverified"
	VerificationStatusVerified   VerificationStatus = "verified"
)

The statuses of a verification.

Directories ΒΆ

Path Synopsis
examples
account/check_balance command
Check the credit and the daily sending limit before a campaign.
Check the credit and the daily sending limit before a campaign.
account/get_account command
Retrieve the account: details, credit, status and daily sending limit.
Retrieve the account: details, credit, status and daily sending limit.
error_handling_complete command
Every error the library returns, and how to handle each.
Every error the library returns, and how to handle each.
messages/delete_sms command
Cancel a scheduled message: schedule one for tomorrow, then delete it.
Cancel a scheduled message: schedule one for tomorrow, then delete it.
messages/get_sms command
Retrieve a message and its delivery status.
Retrieve a message and its delivery status.
messages/list_sms command
List the messages, newest first, 50 a page.
List the messages, newest first, 50 a page.
messages/send_advanced_sms command
Send an SMS with every option: a body shown masked, a validity, a scheduled time, a delivery status callback, a sender ID and URL shortening.
Send an SMS with every option: a body shown masked, a validity, a scheduled time, a delivery status callback, a sender ID and URL shortening.
messages/send_bulk_sms command
Send the same message to several recipients, one request each, stopping once the account's daily sending limit is used up.
Send the same message to several recipients, one request each, stopping once the account's daily sending limit is used up.
messages/send_simple_sms command
Send a simple SMS message.
Send a simple SMS message.
optouts/create_optout command
Opt a phone number out: the API then refuses to send it messages.
Opt a phone number out: the API then refuses to send it messages.
optouts/delete_optout command
Delete an opt-out: the number can receive messages again.
Delete an opt-out: the number can receive messages again.
optouts/get_optout command
Retrieve an opt-out by its ID.
Retrieve an opt-out by its ID.
optouts/list_optouts command
List the opt-outs, 50 a page.
List the opt-outs, 50 a page.
optouts/update_optout command
Update the reason of an opt-out.
Update the reason of an opt-out.
verifications/create_verification command
Start a verification: Calisero generates a 6-character code and sends it by SMS.
Start a verification: Calisero generates a 6-character code and sends it by SMS.
verifications/get_verification command
Retrieve a verification by its ID.
Retrieve a verification by its ID.
verifications/list_verifications command
List the verifications, newest first, optionally only the verified or the unverified ones.
List the verifications, newest first, optionally only the verified or the unverified ones.
verifications/validate_verification command
Check the code a user received against the latest verification of their phone.
Check the code a user received against the latest verification of their phone.
webhooks/delivery_webhook command
Receive delivery status callbacks.
Receive delivery status callbacks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL