MINARA

Lark / Feishu

Lark IM messaging via tenant_access_token, with optional AES-256-CBC-encrypted webhook events.

🟡 Outbound-ready, inbound supported, covers both mainland Feishu (open.feishu.cn) and international Lark (open.larksuite.com). Inbound webhook handles all three of Lark's security modes: verification token, encryption, and signed HMAC.

What you get

  • Outbound text messaging via POST /open-apis/im/v1/messages?receive_id_type=chat_id. Tenant access tokens are cached for 2 hours with a 5-minute refresh headroom, so high-volume senders do not hit the per-app token rate limit.
  • Inbound webhook events at /webhooks/lark. Lark lets the operator pick any combination of three security modes (verification token, encrypt key, HMAC signature). Minara honours whichever modes you enable in the developer console.
  • Domain-aware routing. LARK_DOMAIN selects mainland Feishu or Lark international. Both surfaces share the same API shape.
  • Text only in this PR. Rich cards, images, and files are a future enhancement, the foundation (tenant token cache, signed webhook) lands now.
  • 30 000-char limit. Longer text is truncated.

Setup

1. Create an app

  1. Sign in to Lark Developer Console (or Lark international)
  2. Create a Custom App. Note the App ID (starts with cli_) and App Secret
  3. Under "Permissions & Scopes", grant im:message:send_as_bot and, for inbound, the message-receive scope your bot needs (typically im:message)
  4. In "Event Subscriptions", set the request URL to https://<your-host>/webhooks/lark. Copy the Verification Token and (optional) Encrypt Key

2. Find a default chat_id

Send a message in the target chat (1:1 or group), then call POST /open-apis/im/v1/messages/list from the developer console or use a quick curl with your tenant token to extract a chat_id (format: oc_xxxxxxxxxxxxxxxx). Save it as LARK_DEFAULT_CHAT_ID.

3. Configure Minara

minara auth messaging add
# pick `lark` from the list; paste app id, app secret, chat id,
# verification token (and encrypt key if you enabled encryption).

Or directly in your project .env file:

LARK_APP_ID=cli_xxxxxxxxxxxxxxxx
LARK_APP_SECRET=<opaque secret>
LARK_DEFAULT_CHAT_ID=oc_xxxxxxxxxxxxxxxx
LARK_VERIFICATION_TOKEN=<verification token>
LARK_ENCRYPT_KEY=<encrypt key, optional>
LARK_DOMAIN=open.feishu.cn          # or open.larksuite.com

4. Test

minara auth messaging test lark

Inbound webhook

Configure the Lark Event Subscription URL to:

https://<your-host>/webhooks/lark

Three security modes layer on top of each other:

ModeEnv varWhat it does
Verification tokenLARK_VERIFICATION_TOKENDecoded payload's top-level token field must match. Used in plaintext mode and after decryption.
EncryptionLARK_ENCRYPT_KEY (optional)When set, body arrives as {encrypt: "<base64>"} and is decrypted with AES-256-CBC, key = SHA256(encrypt_key), IV = first 16 bytes of the key.
SignatureX-Lark-Signature header (always optional)SHA-256 over timestamp + nonce + encrypt_key + body hex, presented in the header. Enable in the console for an extra integrity check.

URL verification handshake: Lark sends {type:"url_verification", challenge:"..."}, Minara replies with {challenge} after checking the verification token. A wrong token returns 403.

Limits & caveats

  • Tenant token rate limits. Lark enforces ~100 token-fetch calls per minute per app. The 2 h cache stays well under this.
  • content must be a JSON-encoded string. This is a Lark API quirk: even text messages send content: JSON.stringify({text: "..."}). Minara handles this for you.
  • No card interactions yet. Button-click callbacks on interactive cards arrive at a different webhook event type and are not parsed.

Troubleshooting

"Test message returns code 99991663"

  • Tenant access token failed to refresh. Check LARK_APP_ID and LARK_APP_SECRET against the developer console.

"Inbound webhook returns 401"

  • If you enabled the encryption mode in the console but did not set LARK_ENCRYPT_KEY, every inbound POST fails decryption. Either disable encryption in the console or set the env var.
  • If you enabled signature mode, verify the LARK_ENCRYPT_KEY you saved matches what the console shows (it doubles as the signing input).

"content field is required (400)"

  • Your code is calling send_message with text set to empty string. Lark rejects empty bodies.

Reference

On this page