Feishu Open Platform offers a series of server-side atomic APIs to achieve diverse functionalities. However, actual coding requires additional work, such as obtaining and maintaining access tokens, encrypting and decrypting data, and verifying request signatures. Furthermore, the lack of semantic descriptions for function calls and type system support can increase coding burdens.
To address these issues, Feishu Open Platform has developed the Open Interface SDK, which incorporates these lengthy logic processes, provides a comprehensive type system, and offers a semantic programming interface to improve the coding experience.
- Go 1.17 or later
- Preparations before development (Install SDK)
- Calling Server-side APIs
- Handle Events
- Handle Card Callbacks
- SDK FAQs
The SDK provides a Channel module built on top of WebSocket and the API Client. It encapsulates event listening, message normalization, streaming replies, and media uploads, allowing developers to focus purely on business logic.
The SDK provides registration.RegisterApp for one-click app creation based on OAuth 2.0 Device Authorization Grant (RFC 8628). It returns a verification URL that users can open in Feishu/Lark or render as a QR code. After authorization, the app is created automatically and the SDK returns the App ID and App Secret.
package main
import (
"context"
"errors"
"fmt"
"time"
lark "github.com/larksuite/oapi-sdk-go/v3"
"github.com/larksuite/oapi-sdk-go/v3/scene/registration"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
result, err := registration.RegisterApp(ctx, ®istration.Options{
OnQRCode: func(info *registration.QRCodeInfo) {
fmt.Printf("open or scan this url: %s\n", info.URL)
fmt.Printf("the link expires in %d seconds\n", info.ExpireIn)
},
OnStatusChange: func(info *registration.StatusChangeInfo) {
// status: polling | slow_down | domain_switched
fmt.Printf("registration status: %s", info.Status)
if info.Interval > 0 {
fmt.Printf(", next poll after %d seconds", info.Interval)
}
fmt.Println()
},
})
if err != nil {
var regErr *registration.RegisterAppError
if errors.As(err, ®Err) {
fmt.Printf("register app failed: code=%s, description=%s\n", regErr.Code, regErr.Description)
return
}
panic(err)
}
fmt.Println("App ID:", result.ClientID)
fmt.Println("App Secret:", result.ClientSecret)
client := lark.NewClient(result.ClientID, result.ClientSecret)
_ = client
}When creating an app, use Options.Addons to incrementally request scopes, event subscriptions, and callbacks on top of the platform base template. The values are pre-filled into the confirmation page after the user opens the QR code URL and take effect after confirmation. Options.CreateOnly=true only allows creating a new app. Options.AppID starts the update flow for an existing app.
_, err := registration.RegisterApp(ctx, ®istration.Options{
Addons: ®istration.AppAddons{
Scopes: registration.AppAddonsScopes{
Tenant: []string{"im:message:send_as_bot"},
User: []string{"calendar:calendar:read"},
},
Events: registration.AppAddonsEvents{
Items: registration.AppAddonsEventItems{
Tenant: []string{"im.message.receive_v1"},
},
},
Callbacks: registration.AppAddonsCallbacks{
Items: []string{"card.action.trigger"},
},
},
CreateOnly: true,
OnQRCode: func(info *registration.QRCodeInfo) {
fmt.Println(info.URL)
},
})
if err != nil {
panic(err)
}
_, err = registration.RegisterApp(ctx, ®istration.Options{
AppID: "cli_xxx",
Addons: ®istration.AppAddons{
Scopes: registration.AppAddonsScopes{
Tenant: []string{"drive:drive.metadata:readonly"},
},
},
OnQRCode: func(info *registration.QRCodeInfo) {
fmt.Println(info.URL)
},
})
if err != nil {
panic(err)
}Notes: Addons is additive only and cannot remove config from the base template. The SDK validates shape and non-empty strings, but does not validate whether scope, event, or callback names exist.
AppAddons.Preset selects the base template for app creation (it is unrelated to Options.AppPreset, which pre-fills the app name, description, and avatar):
| Value | Base template | Behavior |
|---|---|---|
unset (nil) |
Platform default template | Same as before this field existed; the encoded payload carries no preset key. |
false |
Minimal base template | The confirmation page shows only the scopes, events, and callbacks declared in Addons. |
true |
Platform default template | Explicitly requests the default base, same as unset. |
With Preset set to false, an Addons without any scope, event, or callback is valid — the page still enters the confirmation flow and creates an app with only the minimal base capabilities:
preset := false
_, err := registration.RegisterApp(ctx, ®istration.Options{
Addons: ®istration.AppAddons{
Preset: &preset,
},
OnQRCode: func(info *registration.QRCodeInfo) {
fmt.Println(info.URL)
},
})
if err != nil {
panic(err)
}| Parameter | Description | Type | Required | Default |
|---|---|---|---|---|
ctx |
Controls timeout and cancellation for the registration flow; canceling the context stops polling. |
context.Context |
Yes | - |
Options.Source |
Source identifier appended to the QR URL as go-sdk/{source}. |
string |
No | go-sdk |
Options.Domain |
Custom Feishu accounts domain. A full base URL such as https://accounts.feishu.cn is supported. |
string |
No | https://accounts.feishu.cn |
Options.LarkDomain |
Custom Lark accounts domain used when tenant_brand=lark is detected. |
string |
No | https://accounts.larksuite.com |
Options.AppPreset |
Pre-filled app creation values; users can still edit them on the page. | *registration.AppPreset |
No | - |
Options.AppPreset.Avatar |
App avatar URLs, 1-6 entries; first entry is selected by default. Pass raw URLs and the SDK encodes them. Page-side display rules are handled by the app creation page. | []string |
No | - |
Options.AppPreset.Name |
App name with {user} placeholder support; pass raw value and the SDK encodes it. |
string |
No | - |
Options.AppPreset.Desc |
App description with {user} placeholder support; pass raw value and the SDK encodes it. |
string |
No | - |
Options.Addons |
Incremental scopes, events, and callbacks pre-filled into the confirmation page. | *registration.AppAddons |
No | - |
Options.Addons.Preset |
Base template selector: unset for the platform default template, false for the minimal base template (empty increments allowed), true for explicitly requesting the default base. |
*bool |
No | - |
Options.Addons.Scopes.Tenant |
App-identity scopes, for example im:message:send_as_bot. |
[]string |
No | - |
Options.Addons.Scopes.User |
User-identity scopes, for example calendar:calendar:read. |
[]string |
No | - |
Options.Addons.Events.Items.Tenant |
App-identity events, for example im.message.receive_v1. |
[]string |
No | - |
Options.Addons.Events.Items.User |
User-identity events, for example calendar.calendar.event.changed_v4. |
[]string |
No | - |
Options.Addons.Callbacks.Items |
Callback names, for example card.action.trigger. |
[]string |
No | - |
Options.CreateOnly |
When true, the landing page only allows creating a new app. When used together with Options.AppID, the page gives create-new-app flow precedence. |
bool |
No | false |
Options.AppID |
Existing app ID carried as clientID in the QR URL for the update flow. |
string |
No | - |
Options.OnQRCode |
Callback invoked when the verification URL is ready. The callback receives { URL, ExpireIn }. |
func(info *registration.QRCodeInfo) |
Yes | - |
Options.OnStatusChange |
Callback for polling status changes. Status can be polling, slow_down, or domain_switched. |
func(info *registration.StatusChangeInfo) |
No | - |
| Field | Type | Description |
|---|---|---|
ClientID |
string |
App ID |
ClientSecret |
string |
App Secret |
UserInfo |
*registration.UserInfo |
Scanning user info |
UserInfo.OpenID |
string |
User open_id |
UserInfo.TenantBrand |
string |
"feishu" or "lark" |
Returned errors usually expose Code and Description through registration.RegisterAppError. More specific types include registration.AccessDeniedError and registration.ExpiredError.
Code |
Description |
|---|---|
access_denied |
User denied authorization |
expired_token |
QR code expired or polling timed out |
invalid_response |
Response is empty or missing required fields |
We also provide common API composition examples and business scenario examples based on the SDK, such as:
- Messages
- Contacts
- Base
- Sheets
- Tutorials
For more examples, see https://github.com/larksuite/oapi-sdk-go-demo
MIT
When no EncryptKey is configured and a nonempty VerificationToken is explicitly supplied, the SDK validates the token before invoking event or callback handlers: top-level token for v1 and header.token for v2. Missing, null, empty, non-string, or mismatched tokens are rejected. Challenge responses also validate explicitly configured verification tokens.
Leaving both credentials empty retains the previous behavior. Encrypted events retain existing decryption and signature checks, and WebSocket clients continue using trusted internal dispatch.
Rollback: reverting this change restores the validation gap for explicitly configured VerificationTokens. Keep independent webhook authentication in place before rolling back.
Register a combined provider during initialization to update webhook credentials without rebuilding the dispatcher. For example, keep credentials obtained from your configuration system or KMS in a local sync/atomic.Value:
var currentCredentials atomic.Value
currentCredentials.Store(initialCredentials) // dispatcher.WebhookCredentials loaded at startup
eventDispatcher := dispatcher.NewEventDispatcher("", "").
WithWebhookCredentialsProvider(func(ctx context.Context) (dispatcher.WebhookCredentials, error) {
if err := ctx.Err(); err != nil {
return dispatcher.WebhookCredentials{}, err
}
return currentCredentials.Load().(dispatcher.WebhookCredentials), nil
})
// When your configuration watcher obtains a complete new key/token pair:
currentCredentials.Store(updatedCredentials) // same concrete WebhookCredentials typeThe example uses context, sync/atomic, and github.com/larksuite/oapi-sdk-go/v3/event/dispatcher. Initialize the value before receiving requests and atomically replace the complete WebhookCredentials{EncryptKey, VerificationToken} snapshot when configuration changes. Do not log credentials.
Registration does not invoke the provider. Handle calls it once per ordinary event, card callback, or challenge request before parsing/decryption, then uses that request's snapshot for every step. In-flight requests keep their snapshot; later requests see the updated value. Trusted WebSocket Do dispatch does not call the provider. No provider (or a nil provider) uses constructor credentials.
A successful empty snapshot preserves static-empty behavior, including the existing challenge token comparison. Provider errors, panics, or request cancellation reject the request without invoking handlers or falling back to static credentials; original error/panic details are not returned or logged. Providers must be thread safe and promptly read local cached configuration. If a provider performs I/O, it must honor the request context and configure its own timeout. The SDK adds no retries or background workers and cannot interrupt a callback that ignores cancellation.
Call WithWebhookCredentialsProvider only during initialization, alongside handler registration; do not change the provider or other dispatcher configuration concurrently with requests. To roll back the opt-in behavior, remove provider registration and restore your static constructor credentials, retaining the token validation fix. Observe provider failures/latency and webhook error rates during rollout without secret or high-cardinality labels.
domain controls HTTP endpoint discovery. The optional connection host replaces only the WebSocket URL host[:port], preserving scheme, path and query. Default DNS and TLS/SNI follow the resulting hostname; existing custom transport settings still apply. Domains below are placeholders.
cli := larkws.NewClient(os.Getenv("APP_ID"), os.Getenv("APP_SECRET"),
larkws.WithDomain("https://bootstrap.example.com"),
larkws.WithConnectionHost("websocket.example.com:443"),
larkws.WithConnectionHeaders(http.Header{"X-Route": []string{"events"}}),
larkws.WithEventHandler(eventHandler),
)Handshake headers are copied at initialization and for each handshake, including reconnects, and are never forwarded to endpoint discovery. Omitting both settings preserves existing behavior. Host, Connection, Upgrade and Sec-WebSocket-* cannot be overridden through headers. Basic host checks reject empty values, schemes/paths and query parameters; other address and header settings are passed to the underlying libraries without additional SDK format checks. These basic host and reserved-header checks run before network I/O (Start returns an error in Go; construction/build throws in other SDKs). Use trusted developer configuration, not headers/hosts supplied by an end-user request.