English | 简体中文
oapi-sdk-rust is a production-oriented asynchronous Rust SDK for the Feishu/Lark Open Platform, designed for CLI tools, Tauri desktop applications, backend services, bots, and AI agents. It provides OpenAPI access, authentication and token caching, one-click app registration, event callbacks, WebSocket/Webhook Channel integration, media handling, and streaming message capabilities while preserving the domain semantics of the official Java SDK through idiomatic Rust APIs.
oapi-sdk-rust is an async Rust SDK for the Feishu/Lark Open Platform. It aligns its observable authentication, transport, event, and workflow behavior with the official Go, Python, and Java SDKs while exposing Rust-native builders, enums, traits, and async APIs.
The SDK removes repetitive work around access-token acquisition and caching, request construction, platform error decoding, file upload/download, callback signature verification, encrypted event decoding, and one-click app registration.
Current version:
0.1.0. The transport core, generic OpenAPI access, typedim.v1.message.create, event callback support, and one-click app registration are available. Full generated service coverage is still in progress; useApiRequestfor endpoints that do not yet have typed Rust wrappers.
- One-click app creation guide
- Build and validation report
- Architecture
- Official SDK compatibility matrix
- Roadmap
- Contributing
Use GitHub Issues for defects, service coverage requests, and compatibility reports. Include the SDK/Rust versions, endpoint, token type, platform code/msg/request_id, and a minimal redacted reproduction.
Never post App Secrets, access tokens, App Tickets, Encrypt Keys, or full Authorization headers.
- Rust
1.85or later; - Tokio async runtime;
- rustls TLS by default, without a system OpenSSL dependency;
- Linux, macOS, and Windows;
- Feishu connectivity to
open.feishu.cnandaccounts.feishu.cn; - for Lark, set the API base URL to
open.larksuite.com; app registration automatically switches toaccounts.larksuite.comafter detecting a Lark tenant.
Before the first crates.io release:
[dependencies]
lark-oapi = { git = "https://github.com/partme-ai/oapi-sdk-rust", features = ["registration", "events"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }cargo add lark-oapi --git https://github.com/partme-ai/oapi-sdk-rust --features registration,events
cargo add tokio --features macros,rt-multi-threadFeature flags:
| Feature | Default | Purpose |
|---|---|---|
rustls-tls |
yes | reqwest with rustls TLS |
native-tls |
no | reqwest with system Native TLS |
registration |
yes | one-click app registration and add-on encoding |
events |
yes | callback verification, AES decryption, and event parsing |
Minimal HTTP client:
lark-oapi = { git = "https://github.com/partme-ai/oapi-sdk-rust", default-features = false, features = ["rustls-tls"] }use lark_oapi::Client;
# fn build() -> lark_oapi::Result<()> {
let client = Client::builder("cli_xxx", "app_secret")
.source("my-service")
.build()?;
# let _ = client;
# Ok(())
# }App and tenant access tokens are acquired and cached automatically.
use lark_oapi::{service::im::v1::message::CreateMessageRequest, Client};
# async fn run() -> lark_oapi::Result<()> {
let client = Client::builder("cli_xxx", "app_secret").build()?;
let request = CreateMessageRequest::text("ou_xxx", "Hello from Rust")?
.uuid("idempotency-uuid");
let response = client
.im()
.v1()
.message()
.create("open_id", &request)
.await?;
println!("request_id={:?}", response.request_id);
println!("message={:?}", response.body.data);
# Ok(())
# }use lark_oapi::{ApiRequest, Client};
use serde_json::Value;
# async fn run() -> lark_oapi::Result<()> {
let client = Client::builder("cli_xxx", "app_secret").build()?;
let request = ApiRequest::get("/open-apis/contact/v3/users/:user_id")
.path_param("user_id", "ou_xxx")
.query("user_id_type", "open_id")
.tenant_access_token()
.request_id("business-operation-123");
let response = client.execute::<Value>(request).await?;
println!("status={}", response.status);
println!("request_id={:?}", response.request_id);
println!("body={}", response.body);
# Ok(())
# }Both :user_id and {user_id} path variables are supported and percent-encoded.
use bytes::Bytes;
use lark_oapi::{ApiRequest, MultipartField};
# fn build() -> lark_oapi::Result<()> {
let json = ApiRequest::post("/open-apis/example/v1/resources")
.tenant_access_token()
.json(&serde_json::json!({ "name": "demo" }))?;
let form = ApiRequest::post("https://example.com/token")
.form([("grant_type", "client_credentials")]);
let upload = ApiRequest::post("/open-apis/im/v1/files")
.tenant_access_token()
.multipart(vec![
MultipartField::Text {
name: "file_type".into(),
value: "stream".into(),
},
MultipartField::File {
name: "file".into(),
file_name: "report.pdf".into(),
mime_type: Some("application/pdf".into()),
data: Bytes::from_static(b"file bytes"),
},
]);
# let _ = (json, form, upload);
# Ok(())
# }Use Client::execute_bytes for downloads and non-JSON success responses.
.app_access_token() and .tenant_access_token() use the SDK token manager. It checks the configured TokenCache, obtains a token on cache miss, reserves an expiry safety window, and invalidates/retries once for official invalid-token codes.
User OAuth is intentionally outside the SDK transport. Supply the token explicitly:
use lark_oapi::ApiRequest;
let request = ApiRequest::get("/open-apis/calendar/v4/calendars")
.user_access_token("u-xxxxxxxx");use lark_oapi::{AccessTokenType, ApiRequest};
let request = ApiRequest::get("/open-apis/contact/v3/users")
.explicit_access_token(AccessTokenType::Tenant, "t-xxxxxxxx");When every request uses a caller-provided token, app_secret may be an empty string. It is required only when the SDK must obtain managed app/tenant tokens.
use lark_oapi::{ApiRequest, AppType, Client};
# async fn run() -> lark_oapi::Result<()> {
let client = Client::builder("cli_xxx", "app_secret")
.app_type(AppType::Marketplace)
.build()?;
let request = ApiRequest::get("/open-apis/contact/v3/users")
.tenant_access_token()
.app_ticket("app_ticket_from_event_or_store")
.tenant_key("tenant_key");
let _response = client.execute_json_value(request).await?;
# Ok(())
# }Version 0.1 does not persist App Tickets. Store them securely in the application and supply them with marketplace requests.
The registration feature implements the application registration workflow used by the official Java, Go, and Python SDKs. It starts a device-code session, emits a verification URL, polls user confirmation, switches account domains for Lark tenants, and returns the created App ID/Secret.
git clone https://github.com/partme-ai/oapi-sdk-rust.git
cd oapi-sdk-rust/examples/create-an-app-in-one-click-rust
cp .env.example .env
cargo runThe executable renders a terminal QR code and prints the credentials after explicit user confirmation.
See the complete guide, environment variables, protocol details, troubleshooting, and acceptance checklist:
docs/create-an-app-in-one-click-rust.md
use lark_oapi::registration::{
register_app, AppAddons, AppAddonsCallbacks, AppAddonsEvents,
AppAddonsScopes, AppPreset, RegistrationOptions, RegistrationStatus,
};
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let options = RegistrationOptions::new()
.source("my-agent-cli")
.app_preset(
AppPreset::new()
.avatars([
"https://example.com/avatar-a.png",
"https://example.com/avatar-b.png",
])
.name("{user}'s agent")
.description("Created by the Rust SDK"),
)
.addons(AppAddons {
preset: None,
scopes: AppAddonsScopes {
tenant: vec!["im:message:send_as_bot".into()],
user: vec!["calendar:calendar:read".into()],
},
events: AppAddonsEvents {
tenant: vec!["im.message.receive_v1".into()],
user: vec![],
},
callbacks: AppAddonsCallbacks {
items: vec!["card.action.trigger".into()],
},
})
.create_only(true);
let credentials = register_app(
options,
|qr| println!("Open or scan: {} (expires in {}s)", qr.url, qr.expire_in),
|status| match status {
RegistrationStatus::Polling => println!("Waiting for confirmation"),
RegistrationStatus::SlowDown { interval } => {
println!("Polling interval changed to {interval}s")
}
RegistrationStatus::DomainSwitched => println!("Switched to the Lark account domain"),
},
)
.await?;
println!("app_id={}", credentials.client_id);
# Ok(())
# }Registration add-ons are additive. preset: None or Some(true) keeps the platform default base template; Some(false) selects the minimal base template. Sensitive settings such as event URLs and encryption keys must be configured through the application configuration OpenAPI rather than QR parameters.
EventParser verifies callback signatures, decrypts AES-256-CBC payloads, verifies the configured token, and parses URL-verification challenges or business events.
use http::HeaderMap;
use lark_oapi::event::{EventParser, ParsedEvent};
# fn handle(headers: &HeaderMap, body: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
let parser = EventParser::new()
.verification_token("verification-token")
.encrypt_key("encrypt-key");
let response = match parser.parse(headers, body)? {
ParsedEvent::Challenge { challenge } => {
serde_json::json!({ "challenge": challenge }).to_string()
}
ParsedEvent::Event { event_type, event, .. } => {
println!("event_type={event_type}, payload={event}");
r#"{"code":0}"#.to_owned()
}
};
# Ok(response)
# }skip_signature_verification(true) is for controlled tests only and must not be enabled in production.
use std::{sync::Arc, time::Duration};
use http::{HeaderName, HeaderValue};
use lark_oapi::{AppType, Client, MemoryTokenCache};
# fn build() -> lark_oapi::Result<()> {
let http_client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.build()?;
let client = Client::builder("cli_xxx", "app_secret")
.base_url("https://open.larksuite.com")
.oauth_base_url("https://open.larksuite.com")
.timeout(Duration::from_secs(30))
.app_type(AppType::SelfBuilt)
.enable_token_cache(true)
.token_cache(Arc::new(MemoryTokenCache::default()))
.default_header(
HeaderName::from_static("x-business-unit"),
HeaderValue::from_static("agent-platform"),
)
.source("my-service/1.0")
.http_client(http_client)
.build()?;
# let _ = client;
# Ok(())
# }A caller-provided reqwest client owns its connection pooling, proxy, certificate, and timeout settings. ClientBuilder::timeout does not rebuild that client.
Successful calls return ApiResponse<T> with HTTP status, headers, request/log ID, and decoded body. JSON responses with a non-zero top-level code become Error::Api; non-JSON HTTP failures become Error::HttpStatus.
use lark_oapi::Error;
# fn report(error: Error) {
match error {
Error::Api { code, message, request_id } => {
eprintln!("platform code={code}, message={message}, request_id={request_id:?}");
}
Error::HttpStatus { status, body } => {
eprintln!("http status={status}, body={body}");
}
Error::Http(error) => eprintln!("network error={error}"),
other => eprintln!("sdk error={other}"),
}
# }Preserve request_id for support diagnostics and never log complete access tokens.
The SDK uses tracing for request method/path diagnostics and does not log App Secrets, authorization headers, or cached token values. Applications should install their own subscriber and secret-management policy.
Production recommendations:
- store App Secrets, App Tickets, and one-click registration results in a secret manager;
- do not commit
.envfiles or credential-bearing logs; - validate externally supplied file sizes, MIME types, and endpoint choices;
- encrypt and rotate user tokens;
- never disable event signature verification in production.
| Capability | 0.1.0 |
|---|---|
| Configurable API base URL, HTTP client, timeout, headers, source | ✓ |
| Self-built app/tenant tokens | ✓ |
| Marketplace App Ticket / Tenant Key flow | ✓ |
| User and explicit tokens | ✓ |
| Pluggable async token cache | ✓ |
| Invalid-token retry | ✓ |
| JSON / form / bytes / multipart | ✓ |
| Generic access to every OpenAPI endpoint | ✓ |
Typed im.v1.message.create |
✓ |
| Event signature verification/decryption/parsing | ✓ |
| One-click registration, presets, add-ons, domain switch | ✓ |
| Full generated typed service coverage | in progress |
| WebSocket long connection | planned |
| Client assertion OAuth | planned |
See docs/COMPATIBILITY.md for a detailed official-SDK comparison.
cargo fmt --all -- --check
cargo check --workspace --all-features --locked
cargo test --workspace --all-features --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --lockedThe exact tested toolchain, commands, results, and limitations are recorded in:
This is a community implementation and is not currently an official Feishu/Lark-maintained SDK. Feishu, Lark, and related marks belong to their respective owners.