Lark guide

Bring your own Lark (open.larksuite.com) or Feishu (open.feishu.cn) custom or App Store app. Configure its event URL and connect a direct chat or group. App Store bots also require an enabled installation in that organization.

Prerequisites

Use a custom or App Store app in the Lark Developer Console (or the Feishu console) with the Bot capability enabled. Grant the permissions below; each item explains what stops working without that permission:

  • im:message, im:message.p2p_msg:readonly — the bot receives nothing in a direct chat.
  • im:message.group_at_msg:readonly — the bot receives nothing when mentioned in a group.
  • im:message.group_msg — ordinary group follow-ups without a bot mention are not delivered; requires administrator approval.
  • im:message:send_as_bot — the bot cannot reply.
  • im:message:update — a streamed reply cannot be revised — it arrives as one block at the end.
  • im:message:recall — a reply that gets shorter leaves its stale tail behind.
  • im:message.reactions:write_only or im:message — add and remove working markers (OnIt → DONE).
  • im:message.reactions:read or im:message:readonly — recover interrupted removals when the marker’s handle is known. Normal confirmed add/remove works with write permission alone; uncertain operations may require administrator recovery, and an uncertain removal blocks re-adding that same marker even after cleanup.

Automated status reactions belong to their current turn. An explicit reaction request that collides with one returns 409 reaction_owner_conflict; choose a different emoji. The service removes the previous turn’s status before starting the next one, and leaves explicitly added reactions alone. Finishing a Lark turn requires the matching marker_ref from its reply instructions; a reply without that reference still sends, but does not clear the status.

Permissions take effect when you publish an app version. Request the all-group-messages scope only when you want group conversations to continue without a mention; it permits receiving messages in every group containing the bot. With mention-only permission, mention the bot on every turn. See the official SDK permission guidance.

  • Its App ID and App Secret (Credentials & Basic Info) and the Verification Token from Event Subscriptions. If you set an Encrypt Key there, pass it too — deliveries then arrive encrypted and signed.
  • An AgentSky API token with write scope.

Register the channel app

The developer console walks the same checklist, or over the API:

bash
curl -s -X POST https://api.agentsky.dev/v1/channels/apps \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"platform": "lark", "label": "My Lark app", "credentials": {"app_id": "cli_...", "app_secret": "...", "verification_token": "...", "domain": "lark"}}'
# -> setup: { "events_url": "https://.../webhooks/lark/app_..." }

Custom-app registration mints a tenant token and reads the bot's identity — a wrong secret answers 400 lark_error. The stored credential keys grow bot_open_id and bot_name. domain is lark (default) or feishu; encrypt_key is optional.

Finish in the console. Lark has no API for event subscriptions: paste setup.events_url as the Request URL under Event Subscriptions (Lark verifies it with a challenge immediately), add the im.message.receive_v1 event, then publish an app version so the bot is available to your organization.

App Store applications

Choose App Store app when the bot is installed in multiple organizations and set app_mode: store. Registration returns the events URL; it does not prove that an organization has installed the app. Send app_ticket, app_open, app_status_change and app_uninstalled events to that same URL, in addition to im.message.receive_v1. Messaging is refused until the tenant is enabled. Removing or stopping an installation prevents later sends and queued file reads.

To migrate an existing installation, an administrator may register its current app_ticket and adopt the existing conversation with adopt.tenant_key. The service checks the provider's current application and chat state before binding that organization. A missing, disabled or mismatched installation is refused; registration alone is never a completed migration.

Files

Upload a file through the Files API, then send its file_id as a {type: "file", file_id} part on the existing thread-messages endpoint. The file must be visible in the connection's workspace. Lark receives the original bytes; incoming file/image resources become scoped Files identifiers in the signed event. Images are limited to 10 MiB each and other files to 30 MiB each; the combined message attachment budget is 32 MiB. Queued downloads recheck the current app credentials, so disabled apps cannot retrieve previously queued resources. If delivery reports a partial or unknown result, inspect the chat before sending again with a new idempotency key.

Connect

bash
curl -s -X POST https://api.agentsky.dev/v1/channels/connections \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"platform": "lark", "app": "app_...", "destination": {"session": "sess-..."}}'
# -> connect: { "url": "https://applink.larksuite.com/client/bot/open?appId=cli_...", "code": "LINK:…", "app_id": "cli_...", "expires_at": "…" }

connect.url opens the bot's chat inside Lark; the end user sends connect.code there and the chat claims the connection. To connect a group, add the bot to the group and send the code in the group with an @mention of the bot. The connection's thread is then lark:{app}:GROUP:{chat_id} and the service subscribes that conversation after the claim. With administrator-approved im:message.group_msg permission, subsequent messages need no mention; with mention-only permission, every follow-up must mention the bot. Unconnected, unmentioned groups are not routed to an agent. Codes expire after 15 minutes, and a code minted for your app is refused if sent to any other bot. The hosted connect page renders this ceremony for you.

Capabilities

threadsreactionsmarkersproactivemarkdownmodalsephemeralstreaming
✗✓✓✓✓✗✗✓

Markdown renders as an interactive card (Lark's markdown element: bold, italic, links, inline code, code blocks, lists, headings), which is also what lets replies be edited in place. A raw part is sent as a plain text message. Working markers use Lark's reaction set — OnIt while working, DONE on completion, CrossMark on failure. Thread ids are lark:P2P:{chat_id} / lark:GROUP:{chat_id} for the main conversation (with the app id as the second segment on your own app). Private replies carry lark:P2P:{chat_id}:REPLY:{root_message_id} for a flat reply or lark:P2P:{chat_id}:TOPIC:{root_message_id} for a Lark sub-thread; group replies use the corresponding lark:GROUP:{chat_id}:REPLY:{root_message_id} or lark:GROUP:{chat_id}:TOPIC:{root_message_id}. Use the matching chat-level channel id (lark:P2P:{chat_id} or lark:GROUP:{chat_id}) to send to the top-level conversation regardless of which reply-scoped thread id delivered the inbound message.

Troubleshooting

  • The console refuses the Request URL — the app was registered with a different verification token than the console shows; rotate the app's credentials (PATCH /v1/channels/apps/{id}) with the current token and try again.
  • Nothing arrives from a group — the bot only receives group messages that @mention it unless the app holds the receive-all-group-messages scope; mention the bot, and check the event subscription includes im.message.receive_v1.
  • Replies fail with a permission error — grant im:message:send_as_bot (and the reactions scope for markers) and re-publish the app version; permissions take effect on publish.
  • Encrypted deliveries answer 401 — the encrypt_key credential does not match the console's Encrypt Key.