Skip to content

vbcherepanov/bitrix24gosdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bitrix24 Go SDK

Lightweight, zero-dependency Go SDK for Bitrix24 REST API with built-in rate limiting, circuit breaker, and batch support.

Features

  • Rate Limiting: Leaky bucket algorithm respecting Bitrix24 limits
  • Operating Time Tracking: Monitors server-side execution time to avoid OPERATION_TIME_LIMIT errors
  • Circuit Breaker: Protects against cascading failures
  • Batch Requests: Execute up to 50 commands in a single request
  • Pagination: Iterator-based pagination with generics support
  • Token Refresh: Automatic OAuth token refresh with callback
  • Custom Logger: Pluggable logging interface
  • Zero External Dependencies: Only uses Go standard library

Requirements

  • Go 1.25+

Installation

go get github.com/vbcherepanov/bitrix24gosdk

Quick Start

Webhook Authentication (Simplest)

package main

import (
    "context"
    "fmt"
    "log"

    b24 "github.com/vbcherepanov/bitrix24gosdk"
)

func main() {
    client, err := b24.New(
        b24.WithWebhook("https://your-company.bitrix24.ru/rest/1/your_webhook_token/"),
    )
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()

    // Get current user
    resp, err := client.Call(ctx, "user.current", nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Result: %s\n", resp.Result)
}

OAuth Authentication (Cloud)

client, err := b24.New(
    b24.WithOAuth("your-company", "your_access_token"),
    b24.WithOAuthCredentials("client_id", "client_secret", "refresh_token"),
    b24.WithTokenRefreshCallback(func(access, refresh string, expiresAt time.Time) {
        // Save new tokens to database
        log.Printf("Token refreshed, expires at: %v", expiresAt)
    }),
)

Enterprise / On-Premise

client, err := b24.New(
    b24.WithOnPremise("https://your-bitrix.company.com/rest/", "access_token"),
    b24.WithEnterprise(), // Uses higher rate limits (250 bucket, 5 req/sec)
)

With Custom Logger

// Using standard logger
logger := b24.NewStdLogger(
    b24.WithLevel(b24.LogLevelDebug),
    b24.WithPrefix("[bitrix24] "),
)

client, err := b24.New(
    b24.WithWebhook("https://..."),
    b24.WithLogger(logger),
)

// Using log/slog
import "log/slog"

slogLogger := slog.Default()
client, err := b24.New(
    b24.WithWebhook("https://..."),
    b24.WithLogger(b24.NewSlogAdapter(slogLogger.Log)),
)

// Implement custom logger
type MyLogger struct{}

func (l *MyLogger) Debug(ctx context.Context, msg string, keysAndValues ...any) { /* ... */ }
func (l *MyLogger) Info(ctx context.Context, msg string, keysAndValues ...any)  { /* ... */ }
func (l *MyLogger) Warn(ctx context.Context, msg string, keysAndValues ...any)  { /* ... */ }
func (l *MyLogger) Error(ctx context.Context, msg string, keysAndValues ...any) { /* ... */ }

API Limits

The SDK automatically handles Bitrix24 rate limits:

Plan Bucket Size Drain Rate
Cloud 50 2 req/sec
Enterprise 250 5 req/sec

Operating Time Limit

Bitrix24 limits server-side execution time to 480 seconds per method within 10 minutes. The SDK tracks this automatically.

Making API Calls

Simple Call

resp, err := client.Call(ctx, "user.get", map[string]any{
    "ID": 1,
})

var user User
if err := json.Unmarshal(resp.Result, &user); err != nil {
    log.Fatal(err)
}

Call with JSON Unmarshal

var user User
err := client.CallJSON(ctx, "user.get", map[string]any{
    "ID": 1,
}, &user)

With Filter and Select

resp, err := client.Call(ctx, "tasks.task.list", map[string]any{
    "filter": map[string]any{
        "STATUS":         []int{2, 3},   // In progress, pending
        "RESPONSIBLE_ID": 1,
        ">DEADLINE":      "2024-01-01",
    },
    "select": []string{"ID", "TITLE", "STATUS", "DEADLINE"},
    "order":  map[string]string{"DEADLINE": "asc"},
    "start":  0,
})

Batch Requests

Execute multiple commands in a single request (max 50):

builder := b24.NewBatchBuilder()

// Add commands with auto-generated IDs
id1 := builder.Add("user.get", map[string]any{"ID": 1})
id2 := builder.Add("user.get", map[string]any{"ID": 2})
builder.Add("tasks.task.list", map[string]any{
    "filter": map[string]any{"RESPONSIBLE_ID": 1},
})

// Execute batch
resp, err := client.Batch(ctx, builder)
if err != nil {
    log.Fatal(err)
}

// Get specific result
var user User
if err := resp.GetResult(id1, &user); err != nil {
    log.Fatal(err)
}

// Check for errors
if resp.HasErrors() {
    for id, apiErr := range resp.ResultErrors {
        log.Printf("Command %s failed: %s", id, apiErr)
    }
}

Batch with Named IDs

builder := b24.NewBatchBuilder()

builder.AddWithID("tasks", "tasks.task.list", map[string]any{"limit": 1})
builder.AddWithID("user", "user.get", map[string]any{
    // Reference first task's responsible ID
    "ID": "$result[tasks][tasks][0][responsibleId]",
})

Halt on Error

builder := b24.NewBatchBuilder()
builder.SetHaltOnError(true) // Stop on first error

builder.Add("user.get", map[string]any{"ID": 1})
builder.Add("user.get", map[string]any{"ID": 999999}) // Will fail
builder.Add("user.get", map[string]any{"ID": 2})      // Won't execute

Pagination

Iterator (Recommended)

type User struct {
    ID       int    `json:"ID"`
    Name     string `json:"NAME"`
    LastName string `json:"LAST_NAME"`
}

params := &b24.ListParams{
    Filter: map[string]any{"ACTIVE": true},
    Select: []string{"ID", "NAME", "LAST_NAME"},
}

it := b24.ListIterator(
    client,
    "user.get",
    params,
    b24.ParseArrayResult[User],
)

for it.Next(ctx) {
    user := it.Item()
    fmt.Printf("User: %s %s\n", user.Name, user.LastName)
}

if err := it.Err(); err != nil {
    log.Fatal(err)
}

fmt.Printf("Total users: %d\n", it.Total())

Fetch All at Once

users, err := b24.ListAll(
    ctx,
    client,
    "user.get",
    &b24.ListParams{Filter: map[string]any{"ACTIVE": true}},
    b24.ParseArrayResult[User],
)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Fetched %d users\n", len(users))

Fetch Single Page

page, err := b24.FetchPage(
    ctx,
    client,
    "user.get",
    &b24.ListParams{Start: 0},
    b24.ParseArrayResult[User],
)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Items: %d, Total: %d, HasMore: %v\n",
    len(page.Items), page.Total, page.HasMore)

Tasks Result Parser

For methods like tasks.task.list that wrap results in {"tasks": [...]}:

type Task struct {
    ID    string `json:"id"`
    Title string `json:"title"`
}

tasks, err := b24.ListAll(
    ctx,
    client,
    "tasks.task.list",
    nil,
    b24.ParseTasksResult[Task], // Uses "tasks" wrapper
)

Error Handling

resp, err := client.Call(ctx, "user.get", map[string]any{"ID": 1})
if err != nil {
    // Check specific errors
    switch {
    case b24.IsRateLimitError(err):
        log.Println("Rate limited, waiting...")
        time.Sleep(5 * time.Second)

    case b24.IsOperationTimeLimitError(err):
        log.Println("Operation time exceeded, try later")

    case b24.IsTokenExpiredError(err):
        log.Println("Token expired")

    case errors.Is(err, b24.ErrCircuitOpen):
        log.Println("Circuit breaker open, service unavailable")

    case errors.Is(err, b24.ErrNotFound):
        log.Println("Entity not found")
    }

    // Get detailed error info
    var apiErr *b24.APIError
    if errors.As(err, &apiErr) {
        log.Printf("API Error [%s]: %s\n", apiErr.Code, apiErr.Description)
        log.Printf("Method: %s, Status: %d\n", apiErr.Method, apiErr.StatusCode)

        if apiErr.IsRetryable() {
            // Retry logic
        }
    }
    return
}

Error Codes

// Authentication errors
b24.ErrCodeExpiredToken       // "expired_token"
b24.ErrCodeInvalidToken       // "invalid_token"
b24.ErrCodeNoAuthToken        // "NO_AUTH_TOKEN"
b24.ErrCodeAccessDenied       // "ACCESS_DENIED"

// Rate limiting
b24.ErrCodeQueryLimitExceeded // "QUERY_LIMIT_EXCEEDED"
b24.ErrCodeOperationTimeLimit // "OPERATION_TIME_LIMIT"

// Request errors
b24.ErrCodeNotFound           // "NOT_FOUND"
b24.ErrCodeInvalidParameter   // "INVALID_PARAMETER"

Client Statistics

stats := client.GetStats()

fmt.Printf("Throttler tokens available: %d\n", stats.ThrottlerTokens)
fmt.Printf("Circuit breaker state: %s\n", stats.CircuitBreaker.State)
fmt.Printf("Circuit breaker failures: %d\n", stats.CircuitBreaker.Failures)

All Options

client, err := b24.New(
    // Authentication (choose one)
    b24.WithWebhook("https://..."),                           // Webhook URL
    b24.WithOAuth("domain", "access_token"),                  // OAuth
    b24.WithOnPremise("https://...", "access_token"),         // On-premise

    // OAuth credentials for token refresh
    b24.WithOAuthCredentials("client_id", "client_secret", "refresh_token"),

    // Token management
    b24.WithTokenExpiration(time.Now().Add(time.Hour)),
    b24.WithTokenRefreshCallback(func(access, refresh string, expiresAt time.Time) {
        // Save tokens
    }),

    // Components
    b24.WithLogger(logger),
    b24.WithHTTPClient(&http.Client{Timeout: time.Minute}),

    // Behavior
    b24.WithTimeout(30 * time.Second),
    b24.WithEnterprise(), // Higher rate limits
)

Project Structure

bitrix24gosdk/
├── go.mod
├── client.go           # Main HTTP client with Call, Batch, Pagination
├── options.go          # Functional options and New() constructor
├── errors.go           # Error types, codes, and helpers
├── logger.go           # Logger interface and implementations
├── doc.go              # Package documentation
└── internal/
    ├── batch/          # Batch request builder and executor
    ├── circuit/        # Circuit breaker implementation
    ├── constants/      # All constants and limits
    ├── optime/         # Operating time tracker
    ├── pagination/     # Pagination iterators and helpers
    └── throttle/       # Rate limiter (leaky bucket)

License

MIT License

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages