A native Go client library for the BMC Remedy AR System REST API.
- JWT authentication with automatic token management
- Entry CRUD operations (Create, Read, Update, Delete, Merge)
- Attachment upload and download
- Type-safe query builder for AR qualifications
- Built-in request serialization (avoids BMC Error 9093)
- Token bucket rate limiting
- Context-aware with cancellation support
- Zero external runtime dependencies (stdlib only)
go get github.com/tphakala/go-remedyRequires Go 1.27 or later.
package main
import (
"context"
"log"
"time"
"github.com/tphakala/go-remedy"
)
func main() {
// Create client with options
client := remedy.New("https://remedy.example.com:8443",
remedy.WithTimeout(30*time.Second),
remedy.WithRateLimit(10), // 10 requests/second
)
defer client.Close()
ctx := context.Background()
// Authenticate
if err := client.Login(ctx, "username", "password"); err != nil {
log.Fatal(err)
}
defer client.Logout(ctx)
// List entries with filtering
entries, err := client.Entries().List(ctx, "HPD:Help Desk",
remedy.WithQualification("'Status' = \"Open\""),
remedy.WithFields("Request ID", "Summary", "Status"),
remedy.WithLimit(100),
)
if err != nil {
log.Fatal(err)
}
for _, entry := range entries.Entries {
log.Printf("Request: %s - %s",
entry.Values["Request ID"],
entry.Values["Summary"])
}
}// Basic client
client := remedy.New("https://remedy.example.com:8443")
// With options
client := remedy.New("https://remedy.example.com:8443",
remedy.WithHTTPClient(customHTTPClient), // Custom HTTP client
remedy.WithTimeout(60*time.Second), // Request timeout
remedy.WithRateLimit(5), // 5 requests/second
remedy.WithTokenLifetime(time.Hour), // JWT token lifetime (default: 1h)
remedy.WithRefreshThreshold(5*time.Minute), // Refresh before expiry (default: 5m)
remedy.WithAutoRefresh(true), // Enable auto-refresh (default: true)
)// Standard login
err := client.Login(ctx, "username", "password")
// Login with additional auth string (for servers requiring extra context)
err := client.LoginWithAuth(ctx, "username", "password", "authString")
// Check authentication status
if client.IsAuthenticated() {
// ...
}
// Logout
err := client.Logout(ctx)BMC Remedy JWT tokens expire after 1 hour. The client automatically handles token refresh:
- Credentials are stored after
Login()for automatic re-authentication - Proactive refresh occurs when the token is within 5 minutes of expiry (configurable)
- Concurrent safety ensures only one refresh occurs even under high load
// Tokens refresh automatically - no action needed for long-running applications
client := remedy.New("https://remedy.example.com:8443")
client.Login(ctx, "user", "pass")
// This works even hours later - token refreshes automatically
entries, _ := client.Entries().List(ctx, "HPD:Help Desk")
// For security-sensitive applications, clear stored credentials when done
client.ClearCredentials() // Disables auto-refresh, credentials removed from memory
// Disable auto-refresh entirely if preferred
client := remedy.New("https://remedy.example.com:8443",
remedy.WithAutoRefresh(false),
)// Get single entry
entry, err := client.Entries().Get(ctx, "HPD:Help Desk", "REQ000001")
// List entries with options
entries, err := client.Entries().List(ctx, "HPD:Help Desk",
remedy.WithQualification("'Status' = \"Open\""),
remedy.WithFields("Request ID", "Summary", "Status"),
remedy.WithSort("Create Date", remedy.SortDesc),
remedy.WithLimit(50),
remedy.WithOffset(100),
)
// Create entry
entry, err := client.Entries().Create(ctx, "HPD:Help Desk", map[string]any{
"Summary": "New ticket summary",
"Description": "Detailed description",
"Status": "New",
})
// Update entry
err := client.Entries().Update(ctx, "HPD:Help Desk", "REQ000001", map[string]any{
"Status": "In Progress",
})
// Delete entry
err := client.Entries().Delete(ctx, "HPD:Help Desk", "REQ000001")
// Delete with force option
err := client.Entries().Delete(ctx, "HPD:Help Desk", "REQ000001",
remedy.DeleteOptionForce)
// Merge entry (create or update based on matching criteria)
entry, err := client.Entries().Merge(ctx, "HPD:Help Desk", map[string]any{
"Summary": "Ticket summary",
// Matching fields determine if create or update
})Build type-safe AR qualification strings:
// Simple query
q := remedy.NewQuery().
And("Status", "=", "Open").
Build()
// Result: 'Status' = "Open"
// Complex query with multiple conditions
q := remedy.NewQuery().
And("Status", "=", "Open").
And("Priority", "<", 3).
Or("Urgency", "=", "Critical").
Build()
// Result: 'Status' = "Open" AND 'Priority' < 3 OR 'Urgency' = "Critical"
// Raw qualification for complex expressions. Raw inserts its argument as
// given, without quoting or escaping. Never pass untrusted input to it.
q := remedy.NewQuery().
And("Status", "=", "Open").
Raw("'Priority' < 3 OR 'Urgency' = \"High\"").
Build()
// Result: 'Status' = "Open" AND ('Priority' < 3 OR 'Urgency' = "High")
// Use with List
entries, err := client.Entries().List(ctx, "Form",
remedy.WithQualification(q),
)Supported value types:
- Strings: wrapped in double quotes, with each embedded double quote doubled:
Robert "Bobby" Smith->"Robert ""Bobby"" Smith". This is the rule BMC documents for AR System 20.02 ("Using fields in qualifications"). Backslashes and all other characters are passed through unchanged. How the AR server treats a backslash inside a string literal is unverified against a real server; if it also treats a backslash before a double quote as an escape, such a pair in the value, or a backslash at its end, could close the literal early or leave it open. - Other types (for example a
fmt.Stringeror anerror): formatted with%vand quoted the same way as strings - Integers:
123->123 - Floats:
3.14->3.14 - Booleans:
true->1,false->0 - Nil:
nil->$NULL$
// Download attachment
reader, err := client.Attachments().Get(ctx, "Form", "EntryID", "FieldName")
if err != nil {
log.Fatal(err)
}
defer reader.Close()
data, err := io.ReadAll(reader)
// Upload attachment
file, err := os.Open("document.pdf")
if err != nil {
log.Fatal(err)
}
defer file.Close()
if err := client.Attachments().Upload(ctx, "Form", "EntryID", "FieldName",
"document.pdf", file); err != nil {
log.Fatal(err)
}entry, err := client.Entries().Get(ctx, "Form", "InvalidID")
if err != nil {
// Check for specific error types
if errors.Is(err, remedy.ErrNotFound) {
log.Println("Entry not found")
} else if errors.Is(err, remedy.ErrUnauthorized) {
log.Println("Authentication required")
} else if errors.Is(err, remedy.ErrForbidden) {
log.Println("Permission denied")
} else if errors.Is(err, remedy.ErrNoCredentials) {
log.Println("Token expired and no credentials for refresh")
}
// Get detailed API error
var apiErr *remedy.APIError
if errors.As(err, &apiErr) {
log.Printf("API Error %d: %s - %s",
apiErr.MessageNumber,
apiErr.MessageText,
apiErr.MessageAppendedText)
}
}BMC Remedy enforces per-user session limits. Concurrent requests with the same user account trigger Error 9093: "User is currently connected from another machine or incompatible session".
This library automatically serializes requests to prevent this error. All API calls pass through an internal queue ensuring only one request executes at a time per client.
The library provides interfaces for all services, making it easy to mock in tests:
// Your code uses interfaces
type EntryServicer interface {
Get(ctx context.Context, form, entryID string, opts ...QueryOption) (*Entry, error)
List(ctx context.Context, form string, opts ...QueryOption) (*EntryList, error)
Create(ctx context.Context, form string, values map[string]any) (*Entry, error)
Update(ctx context.Context, form, entryID string, values map[string]any) error
Delete(ctx context.Context, form, entryID string, opts ...DeleteOption) error
Merge(ctx context.Context, form string, values map[string]any) (*Entry, error)
}Use mockery to generate mocks:
mockery --all --dir=. --output=mocks --outpkg=mocksMIT License - see LICENSE for details.