# lark-oapi: the Feishu and Lark server SDK for Python

> lark-oapi is the Python server SDK for the Feishu and Lark Open Platform, wrapping the platform work that sits around every API call: token management, request signing, encryption, event dispatching and typed request and response models. Recent additions include a keyless client assertion mode and one-click app registration through the OAuth device flow.

**larksuite/oapi-sdk-python** — Larksuite development interface SDK

- Repository: https://github.com/larksuite/oapi-sdk-python
- Stars: 563 · Forks: 104
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/larksuite-oapi-sdk-python

## The platform work that sits around every API call

The Feishu Open Platform exposes server-side APIs for messaging, contacts, approval, sheets, Base and many other product capabilities. The SDK's stated job is to wrap the repeated platform work around those calls, and the README lists five of them: token management, request signing, encryption and decryption, event dispatching, and typed request and response models.

That list is the honest description of what a server SDK is for. The interesting API is rarely the hard part; the hard part is that the platform wants a tenant access token, that the token has to be refreshed before it expires and cached so that ten concurrent requests do not each fetch their own, that some endpoints want the request body encrypted, and that inbound events arrive as a signed payload you have to verify before you trust. Each of those is small in isolation and tedious to get right in combination, which is why a vendor SDK is worth taking.

The audience is server-side developers building on Feishu or Lark: internal tooling, bots, integrations with other systems, and event-driven workflows. The topics on the repository and the sample directories in the tree describe the coverage, with samples for api, card, channel, client_assertion, event, registration and ws. The card and websocket samples are worth noting, because they cover the two areas where the platform behaves least like a plain REST API: interactive message cards with callbacks, and a persistent connection for receiving events without a public HTTP endpoint.

The primary language is Python, and the distribution on PyPI is lark-oapi even though the import name is lark_oapi. The package is described as the Lark OpenAPI SDK for Python in its setup metadata, with the README titled the Feishu OpenPlatform Server SDK for Python, which reflects the two brands of the same platform.

## Installing and making the first call

Installation is one command:

```bash
pip install lark-oapi
```

Python 3.8 or later is required, and the setup metadata agrees, with python_requires set to >=3.8 and classifiers listing Python 3.8 onwards.

The first call has a familiar shape. A client is built with a builder, and a request object is built with another builder, and the client method is called:

```python
import lark_oapi as lark
from lark_oapi.api.im.v1 import *

client = lark.Client.builder() \
    .app_id("cli_xxx") \
    .app_secret("your_app_secret") \
    .build()

request = CreateMessageRequest.builder() \
    .receive_id_type("chat_id") \
    .request_body(CreateMessageRequestBody.builder()
        .receive_id("oc_xxx") \
        .msg_type("text") \
        .content("{\"text\":\"hello world\"}") \
        .build()) \
    .build()

response = client.im.v1.message.create(request)
```

Three details are worth pointing at. The API is namespaced by product, so client.im.v1.message.create is the messaging endpoint, and the version segment is part of the path, which is how the platform versions its interfaces.

The request body is a typed object, CreateMessageRequestBody, not a dictionary, and the content field carries a JSON string rather than a nested object, because the platform's message content is itself a JSON document whose shape depends on the message type. That double encoding is a genuine papercut in the API and the SDK passes it through rather than hiding it, so you still have to escape the inner JSON.

The credentials are in the client builder, and they are the app identity of a self-built application, not a user token.

## Keyless mode with ClientAssertion, and what it will not do

There is a second way to authenticate, and it is the most interesting design decision in the SDK.

For self-built apps that use an external signing service, the SDK can fetch tenant tokens with client_assertion instead of app_secret. The important constraint is stated plainly: the SDK does not generate, parse, sign, or store JWT private keys. Your provider supplies the final assertion string.

```python
import os

import lark_oapi as lark
from lark_oapi.core.client_assertion import ClientAssertionToken


class EnvClientAssertionProvider:
    def retrieve_token(self, aud: str) -> ClientAssertionToken:
        return ClientAssertionToken(os.environ["LARK_CLIENT_ASSERTION"])


client = lark.Client.builder() \
    .app_id(os.environ["LARK_APP_ID"]) \
    .client_assertion_provider(EnvClientAssertionProvider()) \
    .build()
```

The provider is an interface with one method, retrieve_token, which receives the audience and returns a ClientAssertionToken. The example reads the assertion from an environment variable, which is the right shape: the SDK treats the assertion as an opaque string, and the organisation that owns the signing service keeps the key material and the signing logic out of the process that talks to Feishu.

Two limits are stated in the README and both matter. If you use a custom OpenAPI domain, you also have to configure oauth_base_url(...) so the SDK can derive the OAuth audience correctly, which is easy to miss and produces a confusing failure. And keyless mode is for self-built apps only and does not support AppAccessToken-only APIs, so a mixed application needs the normal app_secret path for those endpoints.

The result is a narrower credential story than a typical SDK: the app secret is a long lived string you store, and this mode removes it from the process entirely at the cost of a dependency on an external signer.

## One-click app registration through the OAuth device flow

register_app creates an application through the OAuth device flow, and the mechanism is the same one you would recognise from a command line login: the SDK shows a URL, the user opens it, and the SDK polls for the result.

```python
import lark_oapi as lark

def on_qr_code(info):
    print(info["url"])


result = lark.register_app(
    on_qr_code=on_qr_code,
    app_preset={
        "avatar": [
            "https://example.com/a.png",
            "https://example.com/b.webp",
        ],
        "name": "{user}'s app",
        "desc": "Created by the business platform",
    },
)

print(result["client_id"])
```

The callback receives a dictionary with the url and an expire_in, and the README says to render the URL as a QR code or show it as a link for the user to open in Feishu or Lark. The preset values are pre-filled into the app creation page, and the name and description support a {user} placeholder that the web page replaces with the scanning user's name, so a tool that provisions apps for many users can produce a personalised name without knowing the user's name in advance. Avatar URLs are passed as raw values and the SDK URL-encodes them, with one to six URLs supported and the first selected by default, and the allowed formats handled by the page rather than by the SDK.

The parameter table in the README shows the surface in full, and a few entries are worth calling out. The source parameter is appended to the QR URL as python-sdk/{source}, which is how a platform tracks which provisioning flow created an app. The domain and lark_domain parameters default to the Feishu and Lark accounts base URLs and exist for deployments that use a custom accounts host. A cancel_event, a threading.Event, can be used to cancel the synchronous polling, and an on_status_change callback reports polling status changes, with status values including polling, slow_down and domain_switched. The slow_down value in particular is the device flow telling you to back off, and having it surfaced as a callback rather than a silent delay is a sensible touch.

There is also a sample for a real manual end-to-end run without mocked registration responses:

```bash
python3 samples/registration/app_preset_live_e2e.py --open
```

The --open flag means the script opens the browser itself, which makes the flow usable as a command line tool rather than only as a library call.

## The addons mechanism, and its five documented limits

The register_app flow can also update an existing app rather than create one, and that is where the addons parameter comes in.

```python
result = lark.register_app(
    on_qr_code=on_qr_code,
    addons={
        "scopes": {
            "tenant": ["im:message:send_as_bot"],
            "user": ["calendar:calendar:read"],
        },
        "events": {"items": {"tenant": ["im.message.receive_v1"]}},
        "callbacks": {"items": ["card.action.trigger"]},
    },
    create_only=True,
)
```

The idea is that a base template is pre-filled into the confirmation page shown after the user scans, and the addon items are merged on top of it, taking effect once the user confirms. The scopes are split into tenant and user identities, and the events likewise, because a platform scope and a user scope are different permissions with different consent implications. The create_only flag restricts the call to creation, and passing an app_id instead adds the scopes to an app that already exists.

The README is unusually explicit about the limits, and they are the kind of thing that would otherwise be discovered the hard way.

First, addons is additive only. Items are merged on top of the base template, and base permissions can never be removed, so this cannot be used to strip access from an app.

Second, addons.preset chooses the base template. Omitted or True keeps the default base template, and False switches to the minimal base template so the final configuration contains only what addons declares. With preset False, an addons with no incremental item is also valid, which is the way to create a bare app.

Third, only five public configuration types travel through addons: tenant and user scopes, tenant and user events, and callbacks. Sensitive configuration such as event request URLs, security.* settings or encrypt keys cannot travel through addons at all, so those have to be configured in the console by a human.

Fourth, the SDK validates the shape but not the item names, so a name that is unknown to the platform catalog is simply ignored by the confirm page rather than raising an error. That is a quiet failure mode: a typo in a scope name produces an app that is missing a permission, and nothing in the SDK says so.

Taken together, these describe a provisioning helper rather than a configuration management system, and that is the right frame for it.

## Dependencies, extras and the shape of the package

The setup.py tells you what the SDK pulls in and what it can optionally use, and one detail in it is worth knowing about.

The install requirements are requests at 2.25 or later, requests_toolbelt at 0.9 or later, pycryptodome at 3.9 or later for the encryption and decryption work, websockets at 11 or later but below 16, and httpx at 0.24 or later but below 1.0. Two HTTP clients in one dependency list is unusual, and it reflects the two ways the SDK talks to the platform: the synchronous request path and the long-lived connection path.

The extras are the interesting part, because they are where a framework integration would otherwise have gone. There is an aiohttp extra for async use, a fastapi extra that pulls in fastapi and uvicorn, a flask extra, and a test extra with pytest and pytest-asyncio. So instead of the SDK depending on a web framework, the framework is an optional companion, and you install the pair you actually use.

The version handling in setup.py has a comment that explains a real problem. It reads VERSION from lark_oapi/core/const.py without importing the package, so that editable installs work before the runtime dependencies, httpx and websockets among them, are set up. If the regex does not match, it raises a RuntimeError saying the version was not found. That is a small but careful piece of packaging: a version constant that lives with the code it describes, read without triggering the import machinery.

The tree is small and readable. There is the lark_oapi package itself, a doc directory, a samples directory with one folder per scenario, a pytest.ini for the test configuration, a MANIFEST.in for packaging, and README.md alongside a Chinese translation at README.zh.md. The default branch is v2_main, and the repository publishes no GitHub releases, so there is no tag list to consult and the version is whatever pip resolved.

The licence is MIT, recorded in the setup metadata and in the LICENSE file, and the classifiers mark the package as Development Status :: 5 - Production/Stable and Operating System :: OS Independent. The author field names Wenbo Mao and the url points at the larksuite repository, with the package description being Lark OpenAPI SDK for Python and the keywords Lark and OpenAPI.

## Conclusion

Adopt lark-oapi if you are building a server-side integration with Feishu or Lark and you want the token lifecycle, signing, encryption and event plumbing handled by a maintained SDK rather than by hand, particularly if you need the app registration flow to provision an app without a developer console session. Do not adopt it as a general REST client, since every request and response type is tied to a specific platform API, and do not adopt the client assertion mode if your app needs AppAccessToken-only endpoints. Verify first that the API surface you need exists in the version you install, since the repository publishes no GitHub releases and the default branch is v2_main, that your deployment can reach the accounts endpoints for the tenant you target, and that the extras you install match your server framework, because aiohttp, fastapi and flask are separate extras rather than bundled dependencies.

## FAQ

### How do I install the Feishu and Lark Python SDK?

Run pip install lark-oapi. Python 3.8 or later is required, and the import name is lark_oapi while the distribution name is lark-oapi. The runtime dependencies are requests, requests_toolbelt, pycryptodome, websockets and httpx.

### How do I send a message with lark-oapi?

Build a client with lark.Client.builder() using the app_id and app_secret, build a CreateMessageRequest with a CreateMessageRequestBody, and call client.im.v1.message.create(request). The content field carries a JSON string whose shape depends on the message type.

### What is ClientAssertion keyless mode in lark-oapi?

It lets a self-built app fetch tenant tokens with a client_assertion instead of an app_secret, for apps backed by an external signing service. The SDK does not generate, parse, sign or store JWT private keys, your provider supplies the assertion string, and the mode does not support AppAccessToken-only APIs.

### What does lark.register_app do?

It creates an app through the OAuth device flow. The on_qr_code callback receives a dictionary with the url and expire_in so you can render a QR code or a link, app_preset pre-fills the creation page, and addons adds scopes, events and callbacks on top of a base template once the user confirms.

### What can the addons parameter not do in lark-oapi?

It is additive only, so base permissions can never be removed. Only five public configuration types travel through it, tenant and user scopes, tenant and user events, and callbacks, and sensitive settings such as event request URLs, security.* or encrypt keys cannot. The SDK validates the shape but not the item names, so an unknown name is ignored by the confirm page.

## Sources

- [Issues](https://github.com/larksuite/oapi-sdk-python/issues)
- [larksuite/oapi-sdk-python on GitHub](https://github.com/larksuite/oapi-sdk-python)
- [License: MIT](https://github.com/larksuite/oapi-sdk-python/blob/v2_main/LICENSE)
- [README](https://github.com/larksuite/oapi-sdk-python/blob/v2_main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/larksuite-oapi-sdk-python
