> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK: Orbit quickstart for Go

> Official Orbit Go SDK quickstart — messaging, voice, contacts, campaigns, and OTP verify with typed methods.

# Go SDK

The Orbit Go SDK wraps the platform's core API resources — messaging (SMS, WhatsApp, email), voice, contacts, campaigns, verify (OTP), and webhook signature verification — with typed methods. It requires Go 1.22+ and is safe for concurrent use across goroutines.

<Note>
  **Pre-publish — source-only.** This SDK is not yet mirrored to the public
  Go module repository — `go get github.com/devotel/orbit-go` returns 404
  today. Until first publish, vendor the source from the monorepo
  (`packages/sdk-go/`) or call the [REST API](/api-reference) directly. See
  [Go: core-scope, not full parity](/sdks#go-core-scope-not-full-parity) on
  the SDK index for exactly what is and isn't wrapped, and the low-level
  `client.Request(ctx, method, path, ...)` escape hatch for uncovered routes
  (worked example [below](#covered-route-missing-use-the-escape-hatch)).
</Note>

## Installation

```bash theme={null}
go get github.com/devotel/orbit-go/orbit@latest
```

## Client initialization

The client reads the key straight from your environment — there is no `from_env` helper in Go; use `os.Getenv`:

```go theme={null}
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/devotel/orbit-go/orbit"
)

func main() {
    client, err := orbit.NewClient(os.Getenv("ORBIT_API_KEY"))
    if err != nil {
        panic(err)
    }
    _ = client
    _ = context.Background()
    fmt.Println("ready")
}
```

Tune the transport with functional options:

```go theme={null}
client, err := orbit.NewClient(
    "dv_live_sk_...",
    orbit.WithBaseURL("https://api.orbit.devotel.io/api/v1"),
    orbit.WithHTTPClient(myCustomHTTP),    // e.g. instrumented http.Client
    orbit.WithMaxRetries(5),
    orbit.WithInitialBackoff(500 * time.Millisecond),
)
```

## Quickstart: send your first SMS

A runnable end-to-end — the key comes from `ORBIT_API_KEY`, never from
source. Copy it into `main.go` and run it with `go run .`:

```go theme={null}
client, err := orbit.NewClient(os.Getenv("ORBIT_API_KEY"))
if err != nil {
    log.Fatal(err)
}

msg, err := client.Messages().SendSMS(context.Background(), orbit.SendSMSInput{
    To:   "+14155552671",
    Body: "Hello from Orbit!",
})
if err != nil {
    log.Fatal(err)
}

fmt.Println("message id:", msg.Data.ID)       // msg_abc123
fmt.Println("status:", msg.Data.Status)       // "queued" — watch it reach "delivered"
fmt.Println("status URL:", "/api/v1/messages/"+msg.Data.ID)
```

Set `ORBIT_API_KEY` to a sandbox key prefixed `dv_test_sk_` first — sandbox
sends are simulated, free, and never reach a carrier. Swap in your live key
(`dv_live_sk_...`) when you're ready to send for real; the code does not
change.

The response shape above comes from the
[Messages API reference](/api-reference/endpoints/messaging) — its language
tabs include this exact call.

## Messaging

```go theme={null}
msg, err := client.Messages().SendSMS(context.Background(), orbit.SendSMSInput{
    To:   "+14155552671",
    Body: "Hello from Orbit!",
})
if err != nil {
    panic(err)
}
fmt.Println("queued:", msg.Data.ID)
```

`client.Messages()` also exposes `SendWhatsApp`, `SendEmail`, and `Get` (fetch a message by id).

## Voice

```go theme={null}
call, err := client.Voice().Create(context.Background(), orbit.CreateVoiceCallInput{
    To: "+14155552671",
})
if err != nil {
    panic(err)
}

_ = call.Data.ID
```

`client.Voice()` also exposes `Get`, `List`, and `Hangup(callID)` — place a call, poll its status, then hang up. All outbound calls route through the Orbit API; the SDK never selects a carrier.

## Second example: verify OTP

The send-and-check round trip is the most common first integration on the
platform. Two calls — the key is still just `ORBIT_API_KEY` passed to
`orbit.NewClient`:

```go theme={null}
// 1. Send an OTP (channel: sms | whatsapp | email).
sent, err := client.Verify().Send(context.Background(), "+14155552671", "sms")
if err != nil {
    log.Fatal(err)
}

// 2. Check the code the user typed in, against the verification id Send returned.
check, err := client.Verify().Check(context.Background(), sent.Data.ID, "482901")
if err != nil {
    log.Fatal(err)
}

fmt.Println("valid:", check.Data.Valid)      // true
fmt.Println("status:", check.Data.Status)    // "approved"
```

A wrongly-typed or expired code reports `check.Data.Valid == false` — it
never errors for a bad guess (only for transport/auth failures), so branch
on the flag. The [Verify API reference](/api-reference/endpoints/verify)
covers the full contract, and [Starter examples](/guides/starter-examples)
includes a complete OTP sign-in starter repo (`orbit-otp-nextjs`).

## Verify (OTP)

```go theme={null}
sent, err := client.Verify().Send(context.Background(), "+14155552671", "sms")
if err != nil {
    panic(err)
}

check, err := client.Verify().Check(context.Background(), sent.Data.ID, "123456")
_ = check
```

`client.Verify()` also exposes `Resend` and `GetDetail`.

## Contacts

```go theme={null}
contact, err := client.Contacts().Create(context.Background(), orbit.ContactCreateInput{
    Phone:     "+14155552671",
    FirstName: "Ada",
})
if err != nil {
    panic(err)
}
_ = contact
```

`client.Contacts()` also exposes `List`, `Get`, `Update`, and `Delete`.

## Campaigns

```go theme={null}
campaign, err := client.Campaigns().Create(context.Background(), orbit.CampaignCreateInput{
    Name:    "Launch",
    Channel: "sms",
})
if err != nil {
    panic(err)
}

_, err = client.Campaigns().Send(context.Background(), campaign.Data.ID)
```

`client.Campaigns()` also exposes `List`, `Get`, `Update`, and `Delete`.

## Paginate a list

List endpoints are cursor-paginated — read `meta.pagination.cursor` and
`meta.pagination.has_more` off each response and pass the cursor back as a
query parameter until `has_more` is `false`. Page through SMS messages with
the escape hatch:

```go theme={null}
query := url.Values{"channel": {"sms"}, "limit": {"100"}}
total := 0

for {
    var page map[string]any
    if err := client.Request(context.Background(), "GET", "/messages", query, nil, &page, ""); err != nil {
        log.Fatal(err)
    }

    data := page["data"].([]any)
    total += len(data)

    pagination := page["meta"].(map[string]any)["pagination"].(map[string]any)
    if !pagination["has_more"].(bool) {
        break
    }
    query.Set("cursor", pagination["cursor"].(string))
}

fmt.Printf("Fetched %d messages\n", total)
```

The full pagination model (cursor vs. offset endpoints, page-size caps, and
why cursors are not bookmarkable) is in the
[Pagination guide](/guides/pagination).

## Error handling

All Orbit-originated errors are `*orbit.Error`. Inspect them with the helper predicates:

| Predicate                     | Matches                                                                                                          |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `orbit.IsAuthentication(err)` | 401/403 — rotate the API key                                                                                     |
| `orbit.IsRateLimit(err)`      | 429 — back off; `err.(*orbit.Error).RetryAfter` is set when the API provides it                                  |
| `orbit.IsServerError(err)`    | 5xx — Orbit-side failure, also persistent network errors (`Kind == orbit.KindServer`, `Code == "network_error"`) |

```go theme={null}
resp, err := client.Messages().SendSMS(ctx, in)
switch {
case orbit.IsRateLimit(err):
    // back off; err.(*orbit.Error).RetryAfter is set when the API provides it
case orbit.IsAuthentication(err):
    // rotate the API key
case orbit.IsServerError(err):
    // 5xx — Orbit-side failure
case err != nil:
    // 4xx — caller error
}
_ = resp
```

Every non-GET request automatically carries an `Idempotency-Key` header (UUID v4); override it per call via `orbit.SendSMSInput{ ..., IdempotencyKey: "job-7a3b9d-attempt-1" }` when retrying from your own queue.

## Covered route missing? Use the escape hatch

The typed resources wrap 8 core resources; the rest of the API — contact segments, event sinks, frequency caps, and everything else listed as out of scope on the [SDK index](/sdks#go-core-scope-not-full-parity) — is reachable through `client.Request(ctx, method, path, ...)`. It unmarshals the raw JSON body into whatever `out` you pass (a `map[string]any` when you have no typed struct). Fetch a segment by id:

```go theme={null}
var segment map[string]any
err := client.Request(context.Background(), "GET", "/contacts/segments/seg_01hxyz", nil, nil, &segment, "")
if err != nil {
    panic(err)
}
fmt.Println(segment["data"].(map[string]any)["name"])
```

The escape hatch carries the same auth, retry, and error model as the typed resources — treat it as a first-class client, not a fallback `http.Client`.

## Webhook signature verification

```go theme={null}
event, err := orbit.VerifyWebhook(
    requestBody,
    request.Header.Get("X-Orbit-Signature"),
    os.Getenv("ORBIT_WEBHOOK_SECRET"),
    0,            // tolerance — 0 = use default 5 minutes
    time.Time{},  // now — zero value = time.Now()
)
if err != nil {
    // Forgery attempt — drop, do NOT 200.
    w.WriteHeader(400)
    return
}
// event is a map[string]any — type-assert event["type"], etc.
```
