Go SDK
The official Go client for the SendAfrica REST API. It is idiomatic, dependency-free, and exposes typed resource objects (SMS, Credits, Rates, SenderIDs, Payments, Webhooks) on the Client, plus flat methods for auth, accounts, contacts, campaigns, notifications, and health checks.
- Module:
github.com/SendAfrica/GO-SDK - Go 1.22 or newer. Standard library only — no third-party dependencies.
- Base URL:
https://api.sendafrica.online/v1
Install
go get github.com/SendAfrica/GO-SDKFor the full SDK guide, see the official docs at sdk.sendafrica.online/sdks/go .
Quickstart
package main
import (
"context"
"log"
"os"
sendafrica "github.com/SendAfrica/GO-SDK"
)
func main() {
client := sendafrica.NewClient(os.Getenv("SENDAFRICA_API_KEY"))
result, err := client.SMS.Send(context.Background(), sendafrica.SendSMSRequest{
To: "0712345678",
Message: "Your verification code is 4921.",
})
if err != nil { log.Fatal(err) }
log.Printf("sent: %s status=%s credits=%d", result.MessageID, result.Status, result.CreditsUsed)
}Keep the API key in SENDAFRICA_API_KEY (or pass it directly to NewClient). Credentials must stay server-side and out of source control.
Client setup
client := sendafrica.NewClient(apiKey,
sendafrica.WithBaseURL("https://api.sendafrica.online/v1"),
sendafrica.WithMaxRetries(3),
sendafrica.WithUserAgent("my-app/1.0"),
sendafrica.WithWebhookSecret("whsec_..."),
)If the API key argument is empty, NewClient reads SENDAFRICA_API_KEY from the environment.
| Option | Description |
|---|---|
WithBaseURL(string) | Override the default https://api.sendafrica.online/v1 (e.g. for sandbox). |
WithHTTPClient(*http.Client) | Supply your own http.Client (timeouts, proxies). |
WithMaxRetries(int) | Retry count for transient failures. Default 3. |
WithBearerToken(string) | Authenticate with a JWT (bearer) instead of an API key. |
WithUserAgent(string) | Custom User-Agent header. |
WithWebhookSecret(string) | Secret used by Webhooks to verify signatures. |
Every request also sends an X-Request-Id and sets Accept: application/json. Responses are unwrapped from the standard {success, data, error, request_id, timestamp} envelope.
Sending SMS
Single message
result, err := client.SMS.Send(ctx, sendafrica.SendSMSRequest{
To: "0712345678",
Message: "Welcome to SendAfrica!",
Sender: "SENDAFRICA", // optional; defaults when omitted
})| Field | Type | Notes |
|---|---|---|
To | string | Normalized to E.164 (+255...) automatically. |
Message | string | Required. |
Sender | string | Optional sender ID; must be a registered/usable ID. |
Returns SMSResult with MessageID, Status, Cost, CreditsUsed, and To. Submission success is not handset delivery — use webhooks for delivery state.
Add RequestOptions{IdempotencyKey: "key-123"} to make the send replay-safe:
result, err := client.SMS.Send(ctx, sendafrica.SendSMSRequest{
To: "0712345678", Message: "Retry-safe send",
}, sendafrica.RequestOptions{IdempotencyKey: "order-123"})Send many (client-side, rate limited)
Send N distinct messages while pacing requests and collecting per-message failures instead of aborting on the first error:
res, err := client.SMS.SendMany(ctx, sendafrica.SendManyRequest{
Messages: []sendafrica.SendManyMessage{
{To: "0712345678", Message: "Hi Ali"},
{To: "0765432109", Message: "Hi Zainab", IdempotencyKey: "m2"},
},
RateLimitPerSec: 10.0, // default 10
})
// res.Results → []SMSResult, res.Failed → []BulkSMSFailedItem{Index,To,Error}Server-side bulk SMS
Send one message to many recipients in a single API call. The SDK enforces a 100-recipient safety cap; partial failures are returned per recipient.
res, err := client.SMS.Bulk(ctx, sendafrica.BulkSMSRequest{
To: []string{"0712345678", "0765432109"},
Message: "Black Friday starts now!",
Sender: "SENDAFRICA",
})
// res.Total, res.Sent, res.Failed, res.Results []BulkSMSRecipient{To,Status,MessageID,CreditsUsed,Error}Message logs
logs, err := client.SMS.Logs(ctx, sendafrica.MessageLogQuery{
Page: 1,
PerPage: 25,
Status: "delivered", // pending | sent | delivered | failed
Search: "0712345678",
DateFrom: "2026-09-01T00:00:00Z",
})
// logs.Items []MessageLog, logs.Total/Page/PerPage/TotalPagesClient-side SMS analysis
client.SMS.Analyze(message) returns a local SMSPartInfo (Encoding, Length, Parts, CreditsRequired) — no network call:
info := client.SMS.Analyze("Hello world")
fmt.Println(info.Encoding, info.Parts, info.CreditsRequired)Credits
bal, err := client.Credits.Balance(ctx)
// bal.AccountID, bal.Balance (integer credit count)
history, err := client.Credits.History(ctx, sendafrica.PageQuery{Page: 1, PerPage: 25})
// []CreditTransaction{ID,Type,Amount,BalanceAfter,Description,CreatedAt}Rates
The published rate card is public (no auth).
rates, err := client.Rates.List(ctx) // []CountryRate{Name,ISO2,DialCode,RateTZS}
tz, err := client.Rates.Get(ctx, "TZ") // CountryRate for one country by ISO2 or nameSender IDs
reqs, err := client.SenderIDs.Requirements(ctx) // SenderIDRequirements (eligibility, rules, docs)
usable, err := client.SenderIDs.Usable(ctx, "") // []UsableSenderID available for sending
list, err := client.SenderIDs.List(ctx) // []SenderID registered on the account
newID, err := client.SenderIDs.Create(ctx, sendafrica.CreateSenderIDRequest{
Name: "MYBRAND",
Country: "TZ",
Purpose: "Marketing",
SampleMessage: "Welcome to MYBRAND",
// Documents: []SenderIDDocumentInput{{RequirementUID, Filename, ContentBase64}},
})
one, err := client.SenderIDs.Get(ctx, newID.ID)Payments (vouchers)
Check the current tiered top-up rate, then create a pay-as-you-go voucher:
rate, err := client.Payments.Rate(ctx) // VoucherRate{MinAmountTZS, Tiers}
payment, err := client.Payments.Create(ctx, sendafrica.CreateVoucherRequest{
Amount: 10000, // TZS — converted to credits server-side
Provider: "snippe",
Phone: "0712345678",
})
// payment.ID, payment.Status, payment.CreditAmountWebhooks
Verify and parse outbound events. The signature is an HMAC-SHA256 hex digest of the exact raw body in the X-SendAfrica-Signature header.
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-SendAfrica-Signature")
event, err := client.Webhooks.Parse(body, signature, "") // "" uses the client-level secret
if err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
fmt.Println(event.Type, event.MessageID, event.Data)
w.WriteHeader(http.StatusOK)
}You can also verify a raw payload separately with client.Webhooks.Verify(payload, signature, secret) or the standalone sendafrica.VerifyWebhookSignature(payload, signature, secret). ParseWebhook tolerates both envelope spellings (type/message_id or status) and only ever emits the parsed event after a valid signature.
Auth, accounts, contacts, campaigns, notifications
These are available as flat methods on the client. Auth/account routes are public or bearer-token based; the rest follow the JWT or API-key auth modes.
| Group | Methods |
|---|---|
| Auth & accounts | Register, Login, Refresh, SendVerificationEmail, VerifyEmail, ResetPassword, ResetPasswordConfirm, OAuthExchange, Me, UpdateMe, Logout, ChangePassword, SendPhoneOTP, VerifyPhone, ListAPIKeys, CreateAPIKey, DeleteAPIKey |
| Contacts | ListContactLists, CreateContactList, ListContacts, AddContact, GetContact, UpdateContact, DeleteContact, AddContactPhone, DeleteContactPhone, ExportContacts (CSV bytes), ImportContactsCSV, GetGoogleContactsStatus, GoogleContactsSync, GoogleContactsDisconnect |
| Campaigns | ListCampaigns, CreateCampaign, GetCampaign, CancelCampaign, DeleteCampaign, ListCampaignRecipients |
| Notifications | ListNotifications, UnreadNotificationCount, MarkNotificationRead, MarkAllNotificationsRead |
| Health | Health — unauthenticated GET /health, never retried |
login, err := client.Login(ctx, "dev@example.com", "s3cret")
authed := sendafrica.NewClient("", sendafrica.WithBearerToken(login.AccessToken))
profile, err := authed.Me(ctx)
campaign, err := client.CreateCampaign(ctx, sendafrica.CreateCampaignRequest{
Name: "Friday blast",
Message: "Weekly offer inside",
ContactListID: 7,
ScheduledAt: time.Now().Add(24 * time.Hour),
})Utilities
| Function | Description |
|---|---|
NormalizeTZPhone(string) | Converts 0712345678, 255..., +255... to E.164 +255...; errors on invalid numbers. |
IsValidTZPhone(string) | Returns bool — validation without an error. |
GetSMSPartInfo(string) | Returns encoding, length, part count, required credits. |
DetectEncoding(string) | Returns EncodingGSM7 or EncodingUCS2. |
APIKeyHash(string) | SHA-256 fingerprint of a key for local identification — never store the raw key. |
Errors
NewClient returns an empty client when retries are exhausted; the typed error is *sendafrica.APIError:
if err != nil {
var apiErr *sendafrica.APIError
if errors.As(err, &apiErr) {
fmt.Println(apiErr.Code, apiErr.Message, apiErr.RequestID, apiErr.StatusCode)
switch {
case apiErr.IsRateLimited(): // 429 / rate_limit_exceeded
case apiErr.IsInsufficientCredits(): // 402 / insufficient_credits
case apiErr.IsUnauthorized(): // 401 / invalid_api_key
}
}
if errors.Is(err, sendafrica.ErrInvalidPhone) {
// local phone validation failed
}
}APIError carries StatusCode, Code, Message, RequestID, RetryAfter, Body, and Headers. ErrInvalidPhone and ErrMissingAPIKey are sentinel errors you can match with errors.Is. Network-level transport failures are wrapped with sendafrica: request failed: ....
Retries and backoff
The client retries 429 and 5xx responses (and network failures) up to WithMaxRetries times (default 3) using exponential backoff from 500 ms up to a maximum of 8 s, honoring the API’s Retry-After header when present. Each attempt also sends a fresh X-Request-Id header.
Reference
| Resource / Method | Endpoint | Returns |
|---|---|---|
SMS.Send(ctx, SendSMSRequest, opts...) | POST /sms | SMSResult |
SMS.SendMany(ctx, SendManyRequest, opts...) | Loops POST /sms | BulkSMSResult |
SMS.Bulk(ctx, BulkSMSRequest, opts...) | POST /sms/bulk | BulkSendResult |
SMS.Logs(ctx, MessageLogQuery, opts...) | GET /sms/logs | MessageLogs |
SMS.Analyze(message) | — (local) | SMSPartInfo |
Credits.Balance(ctx, opts...) | GET /credits/balance | CreditBalance |
Credits.History(ctx, PageQuery, opts...) | GET /credits/history | []CreditTransaction |
Rates.List(ctx, opts...) | GET /rates | []CountryRate |
Rates.Get(ctx, country, opts...) | GET /rates/{country} | CountryRate |
SenderIDs.Requirements(ctx, opts...) | GET /sender-ids/requirements | SenderIDRequirements |
SenderIDs.Usable(ctx, provider, opts...) | GET /sender-ids/usable | []UsableSenderID |
SenderIDs.List(ctx, opts...) | GET /sender-ids | []SenderID |
SenderIDs.Create(ctx, CreateSenderIDRequest, opts...) | POST /sender-ids | SenderID |
SenderIDs.Get(ctx, id, opts...) | GET /sender-ids/{id} | SenderID |
Payments.Rate(ctx, opts...) | GET /vouchers/rate | VoucherRate |
Payments.Create(ctx, CreateVoucherRequest, opts...) | POST /vouchers | Payment |
Payments.CreatePayment(ctx, CreatePaymentRequest) | POST /payments (legacy) | Payment |
Webhooks.Verify(payload, signature, secret) | — (verify) | error |
Webhooks.Parse(payload, signature, secret) | — (verify + parse) | WebhookEvent |
Client.Register / Login / Refresh / ... | /auth/* | typed results |
Client.ListContactLists / CreateContactList / ... | /contact-lists/* | typed results |
Client.ListCampaigns / CreateCampaign / ... | /campaigns/* | typed results |
Client.ListNotifications / ... | /notifications/* | typed results |
Client.Health(ctx) | GET /health | HealthStatus |
See the webhooks and SMS API references for the underlying REST contract. The Go SDK targets the same account-scoped surface documented here.