Loading...
Loading...
The SignBolt Go SDK is a small, idiomatic Go client for the SignBolt REST API. Context-aware, interface-based, and production-ready for any Go service.
Idiomatic Go API with context.Context on every call
Fully typed request and response structs
Pluggable http.Client for testing and custom transports
Webhook signature verification helper
Zero runtime dependencies beyond the Go standard library
Go 1.21 or newer required. The SDK uses only the Go standard library and has no third-party dependencies. Add to your project with `go get`.
go get github.com/signbolt/signbolt-go# Requires Go 1.21+Generate an API key from your SignBolt dashboard. Load from environment variables or a secrets manager. Never commit keys to source.
package main
import (
"context"
"log"
"os"
"time"
"github.com/signbolt/signbolt-go/signbolt"
)
func main() {
client := signbolt.NewClient(
os.Getenv("SIGNBOLT_API_KEY"),
)
// With options
client = signbolt.NewClient(
os.Getenv("SIGNBOLT_API_KEY"),
signbolt.WithBaseURL("https://signbolt.au/api/v1"),
signbolt.WithTimeout(30*time.Second),
signbolt.WithMaxRetries(3),
)
// With custom HTTP client (e.g. for testing or a custom transport)
customHTTP := &http.Client{Timeout: 45 * time.Second}
client = signbolt.NewClient(
os.Getenv("SIGNBOLT_API_KEY"),
signbolt.WithHTTPClient(customHTTP),
)
log.Println("SignBolt client initialized")
_ = client
_ = context.Background()
}Send a document to a recipient for signature.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/signbolt/signbolt-go/signbolt"
)
func sendContract(ctx context.Context, client *signbolt.Client) (*signbolt.SendResponse, error) {
// Read PDF from disk
pdfBytes, err := os.ReadFile("contracts/service-agreement.pdf")
if err != nil {
return nil, fmt.Errorf("read pdf: %w", err)
}
resp, err := client.Documents.Send(ctx, &signbolt.SendRequest{
File: pdfBytes,
Filename: "service-agreement.pdf",
Recipients: []signbolt.Recipient{
{
Email: "client@example.com",
Name: "Jane Client",
Role: signbolt.RoleSigner,
},
},
Subject: "Please sign: Service Agreement",
Message: "Hi Jane, please review and sign the attached service agreement.",
Fields: []signbolt.SignatureField{
{
Type: signbolt.FieldSignature,
Page: 1,
X: 100,
Y: 500,
Width: 200,
Height: 60,
Required: true,
RecipientEmail: "client@example.com",
},
{
Type: signbolt.FieldDate,
Page: 1,
X: 350,
Y: 510,
Width: 120,
Height: 40,
Required: true,
RecipientEmail: "client@example.com",
},
},
WebhookURL: "https://api.yoursite.com/webhooks/signbolt",
ExpiresInDays: 14,
})
if err != nil {
return nil, fmt.Errorf("send document: %w", err)
}
fmt.Printf("Document ID: %s
", resp.DocumentID)
fmt.Printf("Signing URL: %s
", resp.SigningURLs[0].URL)
fmt.Printf("Status: %s
", resp.Status)
return resp, nil
}
func main() {
client := signbolt.NewClient(os.Getenv("SIGNBOLT_API_KEY"))
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
_, err := sendContract(ctx, client)
if err != nil {
log.Fatalf("failed to send contract: %v", err)
}
}Verify webhook signatures using net/http.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/signbolt/signbolt-go/signbolt"
)
type WebhookEvent struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
type SignedEventData struct {
DocumentID string `json:"documentId"`
SignerEmail string `json:"signerEmail"`
}
func main() {
client := signbolt.NewClient(os.Getenv("SIGNBOLT_API_KEY"))
webhookSecret := os.Getenv("SIGNBOLT_WEBHOOK_SECRET")
http.HandleFunc("/webhooks/signbolt", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
signature := r.Header.Get("X-SignBolt-Signature")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body error", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Verify signature
if !client.Webhooks.Verify(body, signature, webhookSecret) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
switch event.Type {
case "document.signed":
var data SignedEventData
if err := json.Unmarshal(event.Data, &data); err != nil {
log.Printf("parse signed event: %v", err)
break
}
log.Printf("Document %s signed by %s", data.DocumentID, data.SignerEmail)
// Update database, enqueue notification
case "document.completed":
var data SignedEventData
if err := json.Unmarshal(event.Data, &data); err == nil {
log.Printf("Document %s fully signed", data.DocumentID)
// Download final PDF
}
case "document.declined":
log.Printf("Signer declined")
case "document.expired":
log.Printf("Document expired")
default:
log.Printf("Unhandled event type: %s", event.Type)
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{"received":true}`)
})
log.Println("Webhook listener on :3000")
log.Fatal(http.ListenAndServe(":3000", nil))
}Handle errors using errors.As and the typed signbolt.Error.
package main
import (
"context"
"errors"
"log"
"os"
"github.com/signbolt/signbolt-go/signbolt"
)
func sendWithErrorHandling(ctx context.Context, client *signbolt.Client, pdfBytes []byte) (*signbolt.SendResponse, error) {
resp, err := client.Documents.Send(ctx, &signbolt.SendRequest{
File: pdfBytes,
Filename: "contract.pdf",
Recipients: []signbolt.Recipient{
{Email: "client@example.com", Name: "Client"},
},
Fields: []signbolt.SignatureField{
{Type: signbolt.FieldSignature, Page: 1, X: 100, Y: 500, Width: 200, Height: 60},
},
})
if err != nil {
var sbErr *signbolt.Error
if errors.As(err, &sbErr) {
switch sbErr.StatusCode {
case 400:
log.Printf("Bad request: %s", sbErr.Message)
// sbErr.Details has field-level errors
case 401:
log.Println("Invalid API key β check credentials")
case 403:
log.Println("API access requires Business plan β upgrade at /pricing")
case 413:
log.Println("PDF exceeds 25MB β compress or split")
case 429:
log.Println("Rate limited β SDK will retry automatically")
case 500, 502, 503:
log.Println("SignBolt server error β transient, safe to retry")
default:
log.Printf("SignBolt error: %s", sbErr.Message)
}
} else if errors.Is(err, context.DeadlineExceeded) {
log.Println("Request timed out")
} else {
log.Printf("Unexpected error: %v", err)
}
return nil, err
}
return resp, nil
}
func main() {
client := signbolt.NewClient(os.Getenv("SIGNBOLT_API_KEY"))
_ = client
}List signed documents with pagination.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/signbolt/signbolt-go/signbolt"
)
func getRecentSigned(ctx context.Context, client *signbolt.Client) ([]signbolt.Document, error) {
var allDocs []signbolt.Document
cursor := ""
for {
resp, err := client.Documents.List(ctx, &signbolt.ListRequest{
Status: "completed",
Limit: 50,
Cursor: cursor,
})
if err != nil {
return nil, fmt.Errorf("list documents: %w", err)
}
allDocs = append(allDocs, resp.Data...)
if resp.NextCursor == "" {
break
}
cursor = resp.NextCursor
}
return allDocs, nil
}
func main() {
client := signbolt.NewClient(os.Getenv("SIGNBOLT_API_KEY"))
ctx := context.Background()
// Filter by date range
start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
resp, err := client.Documents.List(ctx, &signbolt.ListRequest{
CreatedAfter: &start,
CreatedBefore: &end,
Limit: 100,
})
if err != nil {
log.Fatalf("list documents: %v", err)
}
for _, doc := range resp.Data {
fmt.Printf("%s β signed by %s on %s
", doc.Filename, doc.SignerEmail, doc.SignedAt.Format(time.RFC3339))
}
}The SignBolt API enforces rate limits to ensure fair use across all customers. The SDK respects these limits with automatic retries.
Standard rate limit: 60 requests per minute per API key.
Burst allowance: up to 120 requests in a 60-second window.
Hourly limit: 3,000 requests per API key per hour.
Send endpoint: limited to 10 documents per minute.
SDK retries rate-limited requests automatically using exponential backoff respecting context deadlines.
Yes. The SDK has no third-party dependencies and initialises quickly β no cold-start penalty. For Lambda, initialise the client at package level to reuse across warm invocations. For Cloud Run and Fly.io, the standard HTTP server pattern works with the SDK injected as a dependency.
Yes. Every API method takes a `context.Context` as its first argument. Pass timeouts, deadlines, and cancellation through context. The SDK respects context cancellation during retries.
Yes. Use `WithHTTPClient` to inject a custom `*http.Client`. Wrap the transport with middleware for logging, metrics, or distributed tracing. The SDK accepts any implementation of `*http.Client`.
Yes. `signbolt.Client` is safe to use from multiple goroutines concurrently. Internally it uses the standard `*http.Client` which is goroutine-safe.
The SDK retries rate-limited requests automatically with exponential backoff. The context deadline is respected β if the deadline expires during retries, the call returns `context.DeadlineExceeded`. For longer-term backoff in your application, catch the rate limit error and back off at the queue level.
The SDK targets Go 1.21+ and uses generics sparingly β most API calls have concrete types so users don't need to instantiate generic types. Pagination helpers and list methods use iterator patterns that work well without heavy generics.
The SDK emits standard HTTP traces via the underlying `*http.Client`. For OpenTelemetry integration, wrap the HTTP client transport with `otelhttp.NewTransport` before passing it to `WithHTTPClient`. All SDK requests will then appear as spans in your tracing backend.
API access is included in the Business plan ($24/month). Upgrade to get your API key.