Lark's API ecosystem is massive — calendars, docs, sheets, messaging, approval flows, and more. The challenge isn't "can my OpenClaw bot call Lark APIs?" It's "how do I build a clean, maintainable API integration layer that doesn't turn into spaghetti code?"
Let's architect it properly.
Your OpenClaw Lark bot sits between users and Lark's APIs. The key is building a modular API client that skills can share:
User Message → OpenClaw → Skill Router → API Client → Lark API
↓
Response Formatter
↓
User Response
On your Tencent Cloud Lighthouse instance, create a reusable API client:
// /opt/clawdbot/lib/lark-api.js
const LARK_BASE = 'https://open.larksuite.com/open-apis';
class LarkAPIClient {
constructor(appId, appSecret) {
this.appId = appId;
this.appSecret = appSecret;
this.tokenCache = { token: null, expiresAt: 0 };
}
async getToken() {
if (this.tokenCache.token && Date.now() < this.tokenCache.expiresAt) {
return this.tokenCache.token;
}
const res = await fetch(`${LARK_BASE}/auth/v3/tenant_access_token/internal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: this.appId,
app_secret: this.appSecret
})
});
const data = await res.json();
this.tokenCache = {
token: data.tenant_access_token,
expiresAt: Date.now() + (data.expire - 300) * 1000
};
return this.tokenCache.token;
}
async request(method, path, body = null) {
const token = await this.getToken();
const options = {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
if (body) options.body = JSON.stringify(body);
const res = await fetch(`${LARK_BASE}${path}`, options);
const data = await res.json();
if (data.code !== 0) {
throw new Error(`Lark API error: ${data.msg} (code: ${data.code})`);
}
return data.data;
}
// Convenience methods
async sendMessage(chatId, content, msgType = 'text') {
return this.request('POST', '/im/v1/messages', {
receive_id: chatId,
msg_type: msgType,
content: JSON.stringify(content)
});
}
async getUserInfo(userId) {
return this.request('GET', `/contact/v3/users/${userId}`);
}
async getCalendarEvents(calendarId, startTime, endTime) {
return this.request('GET',
`/calendar/v4/calendars/${calendarId}/events?start_time=${startTime}&end_time=${endTime}`
);
}
async createDocument(folderId, title, content) {
return this.request('POST', '/docx/v1/documents', {
folder_token: folderId,
title,
document: { content }
});
}
}
module.exports = LarkAPIClient;
Each skill imports the shared client:
// /opt/clawdbot/skills/calendar-skill.js
const LarkAPIClient = require('../lib/lark-api');
const client = new LarkAPIClient(process.env.LARK_APP_ID, process.env.LARK_APP_SECRET);
async function handleCalendarQuery(context) {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const events = await client.getCalendarEvents(
context.user_calendar_id,
today.toISOString(),
tomorrow.toISOString()
);
if (events.items.length === 0) {
return "You have no meetings scheduled for today. Enjoy the focus time!";
}
return events.items.map(e =>
`${e.start_time} - ${e.end_time}: ${e.summary}`
).join('\n');
}
Lark sends events to your bot via webhooks. Configure the event subscription:
# /opt/clawdbot/config/lark-api.yaml
channel: lark
lark:
app_id: "${LARK_APP_ID}"
app_secret: "${LARK_APP_SECRET}"
verification_token: "${LARK_VERIFY_TOKEN}"
encrypt_key: "${LARK_ENCRYPT_KEY}"
event_subscriptions:
- im.message.receive_v1 # New messages
- im.message.reaction.created_v1 # Message reactions
- calendar.event.changed_v1 # Calendar changes
- approval.instance.status_changed # Approval updates
Get your API development environment running:
Lark APIs can fail in various ways. Handle each gracefully:
async function safeAPICall(fn, fallbackMessage) {
try {
return await fn();
} catch (err) {
if (err.message.includes('code: 99991663')) {
// Token expired — will auto-refresh on next call
return await fn();
}
if (err.message.includes('code: 230001')) {
return "I don't have permission to access that resource. Please check the app's scopes.";
}
console.error('Lark API error:', err.message);
return fallbackMessage || "Something went wrong with the Lark API. Please try again.";
}
}
Build a test script that validates all your API connections:
#!/bin/bash
# /opt/clawdbot/test-lark-api.sh
echo "=== Lark API Integration Test ==="
echo "1. Token fetch..."
TOKEN=$(curl -s -X POST "https://open.larksuite.com/open-apis/auth/v3/tenant_access_token/internal" \
-H "Content-Type: application/json" \
-d "{\"app_id\":\"$LARK_APP_ID\",\"app_secret\":\"$LARK_APP_SECRET\"}" | jq -r '.tenant_access_token')
[ -n "$TOKEN" ] && echo " OK" || echo " FAILED"
echo "2. Send test message..."
RESULT=$(curl -s -X POST "https://open.larksuite.com/open-apis/im/v1/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"receive_id":"test_chat_id","msg_type":"text","content":"{\"text\":\"API test successful\"}"}')
echo " $(echo $RESULT | jq -r '.msg')"
A well-built API layer is the backbone of every advanced Lark bot feature. Invest in a clean client architecture now, and every new skill you build will be easier.
Good APIs make great bots.