Go SDK
The Go SDK lets your server call the server REST API (OpenAPI below) from Go, and receive event callbacks and sync callbacks. This page covers installation, creating a client and every config option, the first calls, and how the SDK gets and caches the App Token and how context controls each call's time limit.
- For calling APIs, paging, batch results and errors, see Calling the API.
- For receiving callbacks, see Receiving callbacks.
- For which REST API each method maps to, see Services and methods.
- To try the chat demo, configure device identity and signed callbacks, see the Demo guide.
The SDK's package name here is drim, and it acts as the app. For the Go client SDK (package im) that signs in as a user and sends and receives messages over a persistent connection, see the client docs.
| You want to | See |
|---|---|
| Install the SDK | Download and install |
| Create a client, set timeouts, proxies and self-signed certificates | Create a client, Config options |
| Issue sign-in tickets for your app | Issue sign-in tickets |
| Send messages as a notification account or the system | Send messages |
| Share the App Token across instances, rotate the Secret | Getting and caching the App Token |
| Control each call's time limit, add a trace ID | Context and timeouts |
Download and install
The current release is v1.0.3 (MIT license). In your Go project, run:
go get github.com/DeepRespond-AI/deeprespond-im-server-go-sdk@v1.0.3If you need the demo or offline source: download the Go server SDK + demo (1.0.3) · SHA256 checksums. Unzipped, deeprespond-im-server-go-sdk-1.0.3 is the Go module root, containing the SDK, the demo, the MIT license and the full test data. To try it directly, see Start.
For offline development you can add a local replace pointing to the unzipped root:
go mod edit -replace=github.com/DeepRespond-AI/deeprespond-im-server-go-sdk=/绝对路径/deeprespond-im-server-go-sdk-1.0.3Save the Go examples below and run go mod tidy. When upgrading an existing project, remove the old local replace and update import paths to the public module paths in the table below, including the callback and drimtest subpackages.
- Needs Go 1.23 or later: iterator methods (such as
client.Users.All) return Go 1.23'siter.Seq2, used withfor ... range. - Depends only on the standard library, with no third-party modules.
- The module has three packages:
| Package | Import path | Use |
|---|---|---|
drim | github.com/DeepRespond-AI/deeprespond-im-server-go-sdk | Calling OpenAPI: the client, each service's methods, parameter and result types, errors |
callback | github.com/DeepRespond-AI/deeprespond-im-server-go-sdk/callback | Receiving callbacks: verifying signatures, preventing replay, de-duplicating, dispatching events and sync callbacks |
drimtest | github.com/DeepRespond-AI/deeprespond-im-server-go-sdk/drimtest | Testing helpers: a fake OpenAPI server, signed callback requests, signature test vectors |
The module path is the public standalone GitHub repository, while the package name stays drim, so name it explicitly when importing:
import drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"Server side only
The Client Secret and App Token are as powerful as the app administrator's password: they can create and delete users and send messages as anyone. Never put them in a code repository, an app or a web page. For clients, use a client SDK such as the Web SDK.
Create a client
package main
import (
"log"
"os"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func main() {
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
})
if err != nil {
log.Fatal(err) // 配置不合规,如 BaseURL 不是 https 地址
}
defer client.Close()
// 把 client 交给你的业务代码使用……
}- One client per app.
OrgNameandAppNameare the parts before and after#in the AppKey (org_name#app_name), shown in the app's details in the console. For the Client ID and Client Secret, see Authentication and App Token. To manage several apps in one process, create a client for each. - Reuse it across the process. A client can be used by many goroutines at once, sharing the App Token, connection pool and rate-limit waits. Create one at startup instead of one per request.
- Creating it sends no requests.
Newonly checks the config; the App Token is fetched on the first API call. An invalid config returns a plainerror(not*drim.Error). Closestops background token fetching and closes idle connections of the HTTP client the SDK created. Later calls return aclient_unavailableerror.
What New checks:
BaseURLmust be an absolutehttporhttpsURL with no query, fragment, username or password; a trailing/is removed. It may have a path prefix, such ashttps://gw.example.com/imbehind a gateway.http://is only allowed for the local machine (localhost,127.0.0.0/8,::1) and private networks (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16); everything else must usehttps://, so credentials are never sent in clear text.OrgName,AppName,ClientIDandClientSecretcan't be empty; durations and counts can't be negative; withRateLimitset,PerSecondmust be greater than 0.
Config options
| Field | Type | Required | Description |
|---|---|---|---|
BaseURL | string | Yes | The IM service address, such as https://im.example.com |
OrgName | string | Yes | The tenant's unique ID, the part of the AppKey before # |
AppName | string | Yes | The app name, the part of the AppKey after # |
ClientID | string | Yes | The app's Client ID |
ClientSecret | string | Yes | The app's Client Secret |
HTTPClient | *http.Client | No | HTTP client for requests; see below |
Timeout | time.Duration | No | Time limit per attempt (from sending the request to reading the whole response), 30 seconds by default. Completing an upload always gets 150 seconds, unaffected by this |
Retry | drim.RetryPolicy | No | Automatic retry settings; see below |
MaxRetryAfter | time.Duration | No | Longest Retry-After to wait automatically when rate-limited, 60 seconds by default; longer ones aren't waited for and return rate_limited |
RateLimit | *drim.RateLimit | No | Local rate limiting, off by default; see Rate limits |
TokenStore | drim.TokenStore | No | Shared storage so several service instances use one App Token; in process memory only by default. See Sharing the App Token across instances |
Logger | *slog.Logger | No | Logging, silent by default; see Logs and troubleshooting |
Hooks | drim.Hooks | No | Hooks before and after requests, for injecting trace headers and recording metrics; see Logs and troubleshooting |
UserAgentSuffix | string | No | Appended to User-Agent, such as order-svc/2.3. The SDK's User-Agent is deeprespond-im-server-sdk/go/{version} |
HTTP client
Without HTTPClient, the SDK creates its own: at most 100 idle connections per host, closed after 90 idle seconds, 10-second connect and TLS handshake timeouts, proxy from the environment (HTTPS_PROXY and so on), and no redirects (OpenAPI never redirects, so a 3xx is treated as invalid_response). Close closes its idle connections.
To customize it (for example a private deployment with a self-signed certificate, or no proxy), pass your own *http.Client. The SDK neither changes nor closes it. Don't set http.Client.Timeout; the SDK controls each attempt's limit through Timeout (completing an upload needs 150 seconds).
package main
import (
"crypto/tls"
"crypto/x509"
"log"
"net/http"
"os"
"time"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func newClient() (*drim.Client, error) {
// 私有化部署:信任自己的 CA 证书(SDK 没有跳过证书校验的选项)
pem, err := os.ReadFile("/etc/im/ca.pem")
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pem)
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil // 内网直连,不走代理
transport.TLSClientConfig = &tls.Config{RootCAs: pool}
return drim.New(drim.Config{
BaseURL: "https://im.internal.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
HTTPClient: &http.Client{Transport: transport},
Timeout: 10 * time.Second,
})
}
func main() {
client, err := newClient()
if err != nil {
log.Fatal(err)
}
defer client.Close()
}Retry settings
Retry fields:
| Field | Type | Description |
|---|---|---|
MaxAttempts | int | Maximum attempts (including the first) on network errors, timeouts and 5xx; 4 by default, so up to 3 retries. 1 means no retries |
MaxRateLimited | int | How many times to wait and resend after rate_limited; 3 by default |
Disabled | bool | When true, neither kind is retried. Getting a new token and resending once after the App Token becomes invalid still happens |
For which requests are retried and how, see Retries and idempotency.
First calls
Issue sign-in tickets
The recommended way to sign in is with a sign-in ticket: after a user signs in to your system, your server issues an IM sign-in ticket for them, and the app signs in with it through a client SDK (valid for 5 minutes, single use). See Issue a sign-in ticket.
Below is the "get IM sign-in ticket" endpoint your server gives the app. With AutoCreate: true, a missing user is created automatically, and your system's user ID is used directly as the IM username:
package main
import (
"encoding/json"
"log"
"net/http"
"os"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func main() {
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// App 登录业务系统后调用这个接口取 IM 的登录凭证;凭证无效时 App 会再取一次,这里要允许重复调用
http.HandleFunc("POST /api/im-ticket", func(w http.ResponseWriter, r *http.Request) {
userID := currentUser(r) // 由业务系统的登录态得出
ticket, err := client.Users.IssueLoginTicket(r.Context(), userID, drim.IssueLoginTicketParams{AutoCreate: true})
if drim.IsCode(err, drim.CodeUserDisabled) {
http.Error(w, "账号已被封禁", http.StatusForbidden)
return
}
if err != nil {
log.Printf("签发 IM 登录凭证失败:%v", err) // 错误信息中不含凭证和令牌
http.Error(w, "暂时不可用", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"ticket": ticket.Ticket, "expires_in": ticket.ExpiresIn})
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func currentUser(r *http.Request) string { return r.Header.Get("X-User-ID") }- Each call issues a new ticket (starting with
ult_), and earlier tickets still work until they expire, so the SDK retries this request automatically; only give the app the last one. ticket.UserCreatedsays whether the user was created this time.- A ticket is a temporary password: only return it to that user's app over HTTPS, and never log it.
Send messages
Send a one-to-one message to a user from your notification account:
package main
import (
"context"
"log"
"os"
"time"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
func main() {
client, err := drim.New(drim.Config{
BaseURL: "https://im.example.com",
OrgName: "1100250925",
AppName: "demo",
ClientID: os.Getenv("IM_CLIENT_ID"),
ClientSecret: os.Getenv("IM_CLIENT_SECRET"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 整次调用(含重试和限流等待)最多 10 秒
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
body, err := drim.Marshal(map[string]any{"card": "order", "order_id": "1001", "title": "订单已支付"})
if err != nil {
log.Fatal(err)
}
result, err := client.Messages.Send(ctx, drim.SendMessageParams{
From: "notice",
ToUser: "zhangsan",
ClientMsgID: "order-1001-paid", // 业务单号作为去重 ID:业务层重试也不会重复发送
Type: "custom",
Body: body,
Push: &drim.MessagePushOptions{Title: "订单通知", Body: "您的订单已支付"},
})
if err != nil {
log.Fatal(err)
}
if result.Duplicate {
log.Printf("重复提交,返回的是第一次发送的消息 %s", result.MessageID)
}
log.Printf("已发送:会话 %s,序号 %d", result.ConversationID, result.Seq)
// 群聊:不给 From 时以系统身份发送
_, err = client.Messages.Send(ctx, drim.SendMessageParams{
ToGroup: "1840012345678901",
Type: "text",
Body: drim.JSON(`{"text":"今晚 8 点系统维护"}`),
})
if err != nil {
log.Print(err)
}
}- The message content
Bodyis ajson.RawMessage; for its format, see Message format. Write JSON text directly withdrim.JSON, or encode Go values withdrim.Marshal(which doesn't escape<,>and&; message size counts the bytes actually sent). - For send parameters and results, see Send messages; for what
ClientMsgIDdoes, see Retries and idempotency.
Getting and caching the App Token
Calling OpenAPI needs an App Token (see Authentication and App Token). The SDK fetches, caches and renews it with the Client ID and Client Secret, so you never call the token API yourself:
- Fetched on the first call. The token is kept in the client's memory and added to every later request.
- Fetched once at a time. When several goroutines need a new token at once, the SDK sends one request and the others wait for its result (token requests are rate-limited by source IP and count toward the app's OpenAPI quota).
- Renewed early. When less than 10% of the token's lifetime is left (at least 5 minutes early), the next request fetches a new token in the background while still using the old one, so requests don't wait. With less than 1 minute left, requests wait for the new token before going out. With no requests for a long time, nothing is renewed.
- Expiry is computed from the local clock: the time the token request was sent plus
expires_in. If the local clock is far off from the server, the token may stop working early, which the "token invalid" handling below covers. - Token invalid. When OpenAPI returns
unauthenticatedwithoutdetails.reason(the Secret was revoked or expired, the app was disabled and so on), the SDK drops the token, fetches a new one, and resends the request once. If fetching fails or the resend still getsunauthenticated, the error is returned to you.
When fetching fails
| Result of fetching | What the SDK does |
|---|---|
invalid_client (wrong credentials or no such app), ip_not_allowed (source IP not on the allowlist) | Returns the error and logs it; for the next minute, requests return client_unavailable straight away without fetching, so a bad config doesn't use up the source IP's token quota |
tenant_unavailable, app_unavailable (service paused) | Returns the error; for a random 2 to 5 minutes after, requests return client_unavailable straight away without fetching |
rate_limited | Waits for Retry-After and fetches again, up to 3 times |
Network errors, 5xx | Retries after 1, 2 and 4 seconds, up to 3 times |
In the client_unavailable errors returned during the cooldown, Details["cause"] is the original error code and Details["until"] is when the cooldown ends. For how to check, see Error types and checks.
Errors from fetching the token have Op app.token rather than the name of the API you called.
The client stops working for good after the app is deleted
When OpenAPI returns unauthenticated with details.reason app_deleted or tenant_closed, the app was deleted or the tenant closed, which won't be undone. The SDK marks the client as permanently unavailable: every later call returns client_unavailable straight away with no fetching or retries, and an error is logged. Check with drim.IsPermanentlyUnavailable(err); at that point, clear the saved credentials.
Rotating the Client Secret
After rotating the Secret in the console, the old one has a 24-hour grace period (see Authentication and App Token). Once your service's config has the new Secret, call UpdateCredentials, or create a new client with the new config:
// 配置中心推送了新的 Secret 之后
client.UpdateCredentials(os.Getenv("IM_CLIENT_ID"), os.Getenv("IM_CLIENT_SECRET"))UpdateCredentials drops the current token and ends any cooldown after a failed fetch, so the next request fetches with the new credentials. It's safe to call while other goroutines are using the client.
Sharing the App Token across instances
Each service instance fetching its own token is fine; several valid App Tokens can exist at once. With many instances, to cut down token requests, let them share one token through a TokenStore:
type TokenStore interface {
Get(ctx context.Context, key string) (token string, expiresAt time.Time, ok bool, err error)
Set(ctx context.Context, key string, token string, expiresAt time.Time) error
// CompareAndDelete 只在存储中的令牌等于 token 时删除(避免删掉别的实例刚换来的新令牌)
CompareAndDelete(ctx context.Context, key string, token string) error
}- With no token in memory, the SDK reads from the store first (when
Getreturnsokfalse, this instance fetches one and callsSet). Tokens read from the store are also renewed when less than 10% is left, and written back after fetching. - When a token becomes invalid, it's only deleted from the store if the store still holds that invalid token (
CompareAndDelete). - Store keys look like
drim:token:plus 16 hex characters, derived from the service address,org_name,app_nameand Client ID, without the Secret. - If the store errors (or doesn't answer within 5 seconds), the SDK falls back to fetching in this process, without affecting requests, and logs a warning.
The SDK only depends on the standard library and has no built-in Redis implementation. With go-redis:
package tokenstore
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
drim "github.com/DeepRespond-AI/deeprespond-im-server-go-sdk"
)
// RedisTokenStore 在 Redis 中保存 App Token,值为 "{到期的毫秒}:{令牌}"。
type RedisTokenStore struct {
R *redis.Client
Prefix string
}
var _ drim.TokenStore = RedisTokenStore{}
func (s RedisTokenStore) Get(ctx context.Context, key string) (string, time.Time, bool, error) {
v, err := s.R.Get(ctx, s.Prefix+key).Result()
if errors.Is(err, redis.Nil) {
return "", time.Time{}, false, nil
}
if err != nil {
return "", time.Time{}, false, err
}
ms, token, ok := strings.Cut(v, ":")
n, err := strconv.ParseInt(ms, 10, 64)
if !ok || err != nil {
return "", time.Time{}, false, nil
}
return token, time.UnixMilli(n), true, nil
}
func (s RedisTokenStore) Set(ctx context.Context, key, token string, expiresAt time.Time) error {
value := fmt.Sprintf("%d:%s", expiresAt.UnixMilli(), token)
return s.R.Set(ctx, s.Prefix+key, value, time.Until(expiresAt)).Err()
}
var compareAndDelete = redis.NewScript(`
local v = redis.call('GET', KEYS[1])
if v then
local i = string.find(v, ':', 1, true)
if i and string.sub(v, i + 1) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
end
return 0`)
func (s RedisTokenStore) CompareAndDelete(ctx context.Context, key, token string) error {
return compareAndDelete.Run(ctx, s.R, []string{s.Prefix + key}, token).Err()
}Pass TokenStore: RedisTokenStore{R: rdb, Prefix: "order-svc:"} when creating the client.
Getting the current token
client.Token(ctx) returns the currently valid App Token (fetching one if needed). Use it only for troubleshooting, such as calling an API by hand with curl; never log it or give it to clients. client.AppID() returns the app ID obtained when fetching the token, or an empty string if none has been fetched yet.
Context and timeouts
Every method takes a context.Context first:
ctx's deadline is the total time limit of the whole call, including automatic retries, rate-limit waits and waiting for a token. When a wait would pass the deadline, the SDK doesn't wait and returns an error.Config.Timeoutis the limit per attempt (30 seconds by default), from sending the request to reading the whole response.- When
ctxis cancelled the error code iscanceledanderrors.Is(err, context.Canceled)is true; when the deadline passes or an attempt times out, it'stimeoutanderrors.Is(err, context.DeadlineExceeded)is true. - In an HTTP handler, pass
r.Context()straight to the SDK: when the user disconnects, the SDK call is cancelled with it.
Per-call options go through ctx without changing method signatures, and only affect calls made with that ctx (every page of an iterator uses them):
| Function | Description |
|---|---|
drim.WithRequestID(ctx, id) | Sends your trace ID as the X-Request-ID header; the server uses it as is and logs it, and retries of the same call reuse it. Must be 1 to 64 letters, digits or -, _, .; invalid ones are rejected locally with invalid_params |
drim.WithAttemptTimeout(ctx, d) | Time limit per attempt for this call, overriding Config.Timeout (and the 150 seconds for completing uploads) |
drim.WithMaxAttempts(ctx, n) | Maximum attempts for this call (including the first) on network errors, timeouts and 5xx; 1 means no automatic retries |
drim.WithMaxRetryAfter(ctx, d) | Longest Retry-After this call waits for; 0 means don't wait when rate-limited and return rate_limited |
func notifyPaid(ctx context.Context, client *drim.Client, orderID, username string) error {
// 整次调用最多 3 秒;带上业务的追踪 ID,服务端的日志中能按它查到这次调用
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
ctx = drim.WithRequestID(ctx, "order."+orderID)
// 线上请求被限流时不排队等待,交给业务的消息队列稍后再发
ctx = drim.WithMaxRetryAfter(ctx, 0)
_, err := client.Messages.Send(ctx, drim.SendMessageParams{
From: "notice",
ToUser: username,
ClientMsgID: "order-" + orderID + "-paid",
Type: "text",
Body: drim.JSON(`{"text":"您的订单已支付"}`),
})
return err
}Channel version
The source package on this page (1.0.2) includes the standalone RTC channel service (client.Channels) and types for the 9 channel callbacks; earlier versions may not have them. For how to use them, see Channel methods and Using standalone channels.
