Node.js SDK for the Cherry messaging Third-party Apps API.
Provides a typed client for managing groups, members, and messages in your app-managed Cherry rooms, plus utilities for verifying and parsing incoming webhooks.
- Runtime: Node.js ≥ 20 (uses global
fetchandcrypto). - No runtime dependencies.
- ESM + CJS dual build, full TypeScript typings.
- Install
- Quick start
- Authentication
- Client
- Groups
- Members
- Messages
- Room owner action locks
- Webhooks
- Types reference
- Error handling
- Changelog
- License
npm install @cherrydotfun/apps-sdk
yarn add @cherrydotfun/apps-sdk
pnpm add @cherrydotfun/apps-sdk
bun add @cherrydotfun/apps-sdkRequirements: Node.js ≥ 20. No runtime dependencies.
import {
CherryAppsClient,
verifyWebhook,
parseWebhook,
CherryAppsError,
} from '@cherrydotfun/apps-sdk';
const cherry = new CherryAppsClient({
baseUrl: 'https://chat.cherry.fun',
appKey: process.env.CHERRY_APP_KEY!,
});
// Identity check
const me = await cherry.me();
console.log('App:', me.appId, 'Bot wallet:', me.botWallet);
// Create a group
const { roomId } = await cherry.groups.create({
ownerWallet: 'OwnerSolanaWallet1111111111111111',
title: 'Dragon Slayers',
description: 'Elite PvP clan',
initialMembers: ['Member1111…', 'Member2222…'],
});
// Send a bot message
await cherry.messages.send(roomId, { content: 'Welcome to the clan!' });Every request to the Cherry API is authenticated with an app key — a single opaque Bearer token of the form:
cha_<appId>_<secret>
The SDK reads it from config.appKey and sets the Authorization header
automatically. Issue / rotate keys through Cherry Admin. See
admin runbook.
Webhook secrets are separate from the app key (32-byte HMAC secret).
Construct a client. The instance is stateless — safe to share across requests.
const cherry = new CherryAppsClient({
baseUrl: 'https://chat.cherry.fun',
appKey: 'cha_<appId>_<secret>',
fetch: globalThis.fetch, // optional
timeout: 30_000, // optional, ms
});| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
baseUrl |
string |
✔ | — | Cherry server base URL, no trailing slash |
appKey |
string |
✔ | — | cha_<appId>_<secret> Bearer token |
fetch |
typeof fetch |
✘ | globalThis.fetch |
Inject a custom fetch (tests, polyfills) |
timeout |
number |
✘ | 30000 |
Request timeout in milliseconds (via AbortController) |
| Property | Type | Description |
|---|---|---|
cherry.groups |
GroupsModule |
Group management |
cherry.members |
MembersModule |
Member management |
cherry.messages |
MessagesModule |
Message management |
cherry.webhooks |
WebhooksHelpers |
{ verify, parse } re-exports |
interface WebhooksHelpers {
verify: typeof verifyWebhook;
parse: typeof parseWebhook;
}Returns the authenticated app's identity, scopes, and rate-limit info.
Signature
me(): Promise<AppMeResponse>Scope: none required.
Response — AppMeResponse:
| Field | Type | Notes |
|---|---|---|
appId |
string |
Stable UUID of your app |
scopes |
string[] |
Granted scopes (e.g. ['groups:create', 'messages:send']) |
botWallet |
string |
Solana wallet that signs / appears on app-bot messages |
botDisplayName |
string | undefined |
Bot display name (admin-configurable) |
rateLimits |
Record<string, number> | undefined |
{ perMinute, perDay } quotas |
Fetch unauthenticated public-safe metadata about any app.
Signature
getPublicAppInfo(appId: string): Promise<AppPublicInfo>Scope: none (no appKey required for this endpoint).
Response — AppPublicInfo:
| Field | Type | Notes |
|---|---|---|
name |
string |
Human-readable app name |
botDisplayName |
string | null |
Bot display name (may be null) |
botAvatarUrl |
string | null |
Bot avatar URL (may be null) |
ownerWallet |
string | null |
App owner's wallet (may be null) |
botWallet |
string | null |
Bot wallet (may be null if not yet provisioned) |
Namespace: cherry.groups. CRUD for app-managed rooms.
Create a new group. ownerWallet becomes an active member with role: 'owner'.
Signature
create(input: CreateGroupInput): Promise<CreateGroupResponse>Scope: groups:create.
CreateGroupInput:
| Field | Type | Required | Notes |
|---|---|---|---|
ownerWallet |
string |
✔ | Solana base58 wallet, will become owner |
title |
string |
✔ | Group name |
description |
string | undefined |
✘ | Optional bio |
avatarUrl |
string | undefined |
✘ | Optional avatar URL |
settings |
Record<string, unknown> |
✘ | Free-form settings object |
gatingRule |
Record<string, unknown> |
✘ | Token-gating rule (see Cherry docs) |
initialMembers |
string[] |
✘ | Wallets auto-invited and auto-accepted |
allowOwnerDetach |
boolean |
✘ | Owner can disconnect the app. Default false for app-created rooms. See locks |
allowOwnerDelete |
boolean |
✘ | Owner can delete the group. Default false |
allowOwnerTransfer |
boolean |
✘ | Owner can transfer ownership. Default false |
Response — CreateGroupResponse:
| Field | Type | Notes |
|---|---|---|
roomId |
string |
ID of the newly created room |
Errors:
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_INPUT |
Missing/invalid wallet or title |
| 403 | MISSING_SCOPE |
Token lacks groups:create |
| 429 | RATE_LIMITED |
App over rateLimits.perMinute/perDay |
Example
const { roomId } = await cherry.groups.create({
ownerWallet: 'ABC1234567890…',
title: 'Dragon Slayers',
description: 'Elite PvP clan',
initialMembers: ['Member1…', 'Member2…'],
allowOwnerDetach: false,
allowOwnerDelete: false,
allowOwnerTransfer: false,
});List groups managed by this app, newest first. Cursor-based pagination.
Signature
list(query?: ListGroupsQuery): Promise<ListGroupsResponse>Scope: groups:manage.
ListGroupsQuery:
| Field | Type | Required | Notes |
|---|---|---|---|
cursor |
string |
✘ | Opaque cursor from a previous response's nextCursor |
limit |
number |
✘ | Page size (1–100, default 50) |
Response — ListGroupsResponse:
| Field | Type | Notes |
|---|---|---|
rooms |
AppRoom[] |
Page of rooms (see AppRoom) |
nextCursor |
string | undefined |
Pass back as cursor to fetch the next page |
Get a single app-managed group.
Signature
get(roomId: string): Promise<AppRoom>Scope: groups:manage.
Errors:
| HTTP | Code | When |
|---|---|---|
| 403 | ROOM_NOT_MANAGED_BY_APP |
Room exists but room.appId !== self.appId |
| 404 | ROOM_NOT_FOUND |
No such room |
Update group metadata. Empty patches are rejected (400).
Signature
update(roomId: string, patch: UpdateGroupInput): Promise<AppRoom>Scope: groups:manage.
UpdateGroupInput (all fields optional; at least one required):
| Field | Type | Notes |
|---|---|---|
title |
string |
Rename |
description |
string |
Replace description |
avatarUrl |
string |
New avatar URL |
settings |
Record<string, unknown> |
Replace settings object |
allowOwnerDetach |
boolean |
Toggle detach lock |
allowOwnerDelete |
boolean |
Toggle delete lock |
allowOwnerTransfer |
boolean |
Toggle transfer lock |
Returns: the updated AppRoom.
Delete an app-created group (appCreated: true). Cannot delete an
assigned room (appCreated: false) — those must be detached by the owner or
an admin.
Signature
delete(roomId: string): Promise<void>Scope: groups:manage.
Errors:
| HTTP | Code | When |
|---|---|---|
| 403 | CANNOT_DELETE_ASSIGNED_ROOM |
Room was obtained via assignment, not created |
| 403 | ROOM_NOT_MANAGED_BY_APP |
Cross-app isolation |
Namespace: cherry.members. Manage membership of app-managed groups.
Bulk-invite wallets. With autoAccept: true, invited members become active
immediately; otherwise they receive a pending invite they must accept in Cherry.
Signature
invite(roomId: string, input: InviteMembersInput): Promise<InviteMembersResponse>Scope: members:invite.
InviteMembersInput:
| Field | Type | Required | Notes |
|---|---|---|---|
wallets |
string[] |
✔ | List of Solana wallets to invite |
autoAccept |
boolean |
✘ | When true, also accept on behalf of each invitee |
Response — InviteMembersResponse:
| Field | Type | Notes |
|---|---|---|
invited |
string[] |
Wallets that received a fresh invite (or re-invite of a kicked member) |
skipped |
string[] |
Wallets that were already active / pending / banned |
accepted |
string[] |
Wallets activated immediately (only when autoAccept: true) |
Remove a member. They can be re-invited later (unlike ban).
Signature
kick(roomId: string, wallet: string): Promise<void>Scope: members:moderate.
Errors:
| HTTP | Code | When |
|---|---|---|
| 403 | CANNOT_MODERATE_OWNER |
wallet === room.owner |
| 404 | MEMBER_NOT_FOUND |
Wallet is not in the room |
Ban a member. Banned wallets cannot be re-invited until unbanned. Optionally delete all their existing messages.
Signature
ban(roomId: string, wallet: string, opts?: BanMemberInput): Promise<void>Scope: members:moderate.
BanMemberInput:
| Field | Type | Required | Notes |
|---|---|---|---|
deleteMessages |
boolean |
✘ | If true, move all banned-user messages to deleted_messages |
Lift the ban. The user does not rejoin automatically — call
members.invite(...) afterwards if you want them back.
Signature
unban(roomId: string, wallet: string): Promise<void>Scope: members:moderate.
Shadow-mute a member. Their messages persist in their own client view but are not delivered to others. Optionally set an expiry timestamp.
Signature
mute(roomId: string, wallet: string, opts?: MuteMemberInput): Promise<void>Scope: members:moderate.
MuteMemberInput:
| Field | Type | Required | Notes |
|---|---|---|---|
untilTs |
string |
✘ | ISO 8601 expiry. Omit for indefinite mute. |
Lift the mute.
unmute(roomId: string, wallet: string): Promise<void>Scope: members:moderate.
Set a member's role. The 'owner' role is not assignable through this
API — it can only be reached by the room owner via Cherry UI (and is
gated by allowOwnerTransfer for app-managed rooms).
Signature
setRole(
roomId: string,
wallet: string,
role: AppMemberRole, // 'admin' | 'moderator' | 'member'
): Promise<void>Scope: members:moderate.
Errors:
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_ROLE |
role === 'owner' (rejected) |
| 403 | CANNOT_MODERATE_OWNER |
Target is the room owner |
Namespace: cherry.messages.
Send a message as the app bot. The server-derived sender is always
botWallet from cherry.me() — the senderId in any client-supplied
payload is ignored (impersonation prevention).
Signature
send(roomId: string, input: SendMessageInput): Promise<AppMessage>Scope: messages:send.
SendMessageInput:
| Field | Type | Required | Notes |
|---|---|---|---|
content |
string |
✔ | Message body |
messageType |
string |
✘ | Free-form discriminator (e.g. 'text', 'announcement') |
metadata |
Record<string, unknown> |
✘ | Free-form payload. Server merges in senderType: 'app_bot', appId, appBotDisplayName, appBotAvatarUrl |
attachments |
unknown[] |
✘ | Reserved for future attachment support |
Returns the persisted AppMessage (with server-assigned id, createdAt, and merged metadata).
Delete a message. The server moves it to rooms/{roomId}/deleted_messages/.
delete(roomId: string, messageId: string): Promise<void>Scope: messages:delete.
List recent messages, newest first.
Signature
list(roomId: string, query?: ListMessagesQuery): Promise<ListMessagesResponse>Scope: messages:read.
ListMessagesQuery:
| Field | Type | Required | Notes |
|---|---|---|---|
before |
string |
✘ | Return messages older than this message ID (cursor) |
limit |
number |
✘ | Page size (1–100, default 50) |
Response — ListMessagesResponse:
| Field | Type | Notes |
|---|---|---|
messages |
AppMessage[] |
Page of messages (newest first) |
nextCursor |
string | undefined |
Pass back as before to fetch the next page |
For groups your app creates (appCreated: true), you can lock the three
owner-only Cherry actions that are otherwise outside the app's veto:
| Field on the room | Locks the action | Cherry 403 code when blocked |
|---|---|---|
allowOwnerDetach |
POST /api/v1/apps/:appId/disconnect-room (owner Cherry call) |
DETACH_DISABLED_BY_APP |
allowOwnerDelete |
POST /room/group-delete (owner Cherry UI) |
DELETE_DISABLED_BY_APP |
allowOwnerTransfer |
POST /room/group-set-role with role='owner' |
TRANSFER_OWNER_DISABLED_BY_APP |
- Defaults: all three are
false(locked) forappCreated:truerooms. - Scope: only enforced for rooms your app created. Assigned rooms
(
appCreated:false, obtained via owner-accepted assignment invite) ignore the fields — the owner always retains full control. - Setting: pass at
groups.create()time or at any latergroups.update(). - Escape valve: Cherry SUPER_ADMIN can force-detach any room, which
clears
appId,appCreated, and all three flags, returning full control to the owner. The webhook fires withdetachedBy: 'admin'.
// Lock everything at creation
await cherry.groups.create({
ownerWallet, title: 'Locked Clan',
allowOwnerDetach: false,
allowOwnerDelete: false,
allowOwnerTransfer: false,
});
// Later, grant detach
await cherry.groups.update(roomId, { allowOwnerDetach: true });Cherry POSTs webhooks to the URL you configured in admin. Verify the signature before parsing or acting on the payload.
| Header | Type | Notes |
|---|---|---|
X-Cherry-Event |
string |
Event type, e.g. 'message.created'. Same as payload.event. |
X-Cherry-Delivery |
string |
UUID identifying this delivery attempt. Use for idempotency / dedup. |
X-Cherry-Timestamp |
string |
Unix epoch seconds at signing time. |
X-Cherry-Signature |
string |
sha256=<hex> HMAC over `${timestamp}.${rawBody}` with your webhook secret. |
Content-Type |
string |
Always application/json. Read the raw bytes for signature verification. |
Verify the HMAC-SHA256 signature and timestamp drift in constant time.
Signature
verifyWebhook(opts: VerifyWebhookOptions): booleanVerifyWebhookOptions:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
rawBody |
string |
✔ | — | Raw POST body — exactly what the server signed. |
headers |
Record<string, string | string[] | undefined> |
✔ | — | HTTP request headers (case-insensitive lookup is applied). |
secret |
string |
✔ | — | Webhook secret from your app configuration. |
toleranceSeconds |
number |
✘ | 300 |
Allowed clock drift in seconds. Drift > tolerance → false. |
Returns: true if signature and timestamp are both valid; false on
any failure (missing headers, malformed timestamp, drift exceeded, signature
mismatch). Does not throw.
Parse the body into a typed discriminated union. Call after verifying.
Signature
parseWebhook(rawBody: string): AppWebhookPayloadThrows CherryAppsError with code: 'INVALID_PAYLOAD' when:
- Body is not valid JSON.
- Envelope is missing
event,deliveryId,timestamp, ordata. - Field types are wrong (e.g.
timestampis not a number).
Returns: the parsed payload, typed as AppWebhookPayload (discriminated
union — narrow by payload.event).
The envelope shape is constant:
{ event: AppWebhookEventType;
deliveryId: string;
timestamp: number; // unix epoch seconds
data: <event-specific>; }Per-event data shapes:
event |
data interface |
Fields |
|---|---|---|
message.created |
WebhookMessageCreatedData |
roomId, messageId, senderId, content, messageType?, metadata?, attachments?, createdAt: number |
message.deleted |
WebhookMessageDeletedData |
roomId, messageId, deletedBy |
member.joined |
WebhookMemberJoinedData |
roomId, userId, role?: AppMemberRole | 'owner' |
member.left |
WebhookMemberLeftData |
roomId, userId |
member.kicked |
WebhookMemberKickedData |
roomId, userId, kickedBy |
member.banned |
WebhookMemberBannedData |
roomId, userId, bannedBy |
member.unbanned |
WebhookMemberUnbannedData |
roomId, userId, unbannedBy |
member.role_changed |
WebhookMemberRoleChangedData |
roomId, userId, role: AppMemberRole | 'owner', changedBy |
member.muted |
WebhookMemberMutedData |
roomId, userId, mutedBy, untilTs?: string |
member.unmuted |
WebhookMemberUnmutedData |
roomId, userId, unmutedBy |
room.settings_updated |
WebhookRoomSettingsUpdatedData |
roomId, changes: Record<string, unknown> |
room.deleted |
WebhookRoomDeletedData |
roomId |
app.attached_to_room |
WebhookAppAttachedToRoomData |
roomId, appId, ownerWallet, attachedAt: number |
app.detached_from_room |
WebhookAppDetachedFromRoomData |
roomId, appId, ownerWallet, detachedAt: number, detachedBy: 'owner' | 'admin' | 'app' |
assignment_invite.accepted |
WebhookAssignmentInviteAcceptedData |
roomId, appId, inviteId?: string |
assignment_invite.rejected |
WebhookAssignmentInviteRejectedData |
roomId, appId, inviteId?: string |
Type AppWebhookEventType: string literal union of all 16 event names above.
Type AppWebhookPayload: discriminated union — event is the
discriminator. Use a switch over payload.event to narrow.
import express from 'express';
import { verifyWebhook, parseWebhook, CherryAppsError } from '@cherrydotfun/apps-sdk';
const app = express();
app.post(
'/cherry-webhook',
express.raw({ type: 'application/json' }), // raw bytes — required
(req, res) => {
const rawBody = (req.body as Buffer).toString('utf8');
if (!verifyWebhook({
rawBody,
headers: req.headers,
secret: process.env.CHERRY_WEBHOOK_SECRET!,
})) {
return res.status(401).end();
}
let event;
try {
event = parseWebhook(rawBody);
} catch (err) {
if (err instanceof CherryAppsError && err.code === 'INVALID_PAYLOAD') {
return res.status(400).end();
}
throw err;
}
switch (event.event) {
case 'member.joined':
console.log('Joined:', event.data.userId, 'role:', event.data.role);
break;
case 'member.kicked':
console.log('Kicked:', event.data.userId, 'by:', event.data.kickedBy);
break;
case 'message.created':
// event.data.metadata may carry senderType === 'app_bot' etc.
console.log('Msg:', event.data.content);
break;
case 'app.detached_from_room':
// Distinguish source: 'owner' (user disconnected),
// 'admin' (force-detach), 'app' (reserved/future).
console.log('Detached by:', event.data.detachedBy);
break;
// ... handle the rest as needed
}
res.status(204).end();
},
);
app.listen(4000);| Field | Type | Notes |
|---|---|---|
id |
string |
Room ID |
type |
string |
Room type, usually 'group' |
owner |
string |
Owner wallet |
title |
string |
Group name |
description |
string | undefined |
|
avatar |
string | undefined |
Legacy avatar field |
avatarUrl |
string | undefined |
|
settings |
AppRoomSettings | undefined |
See below |
gatingRule |
Record<string, unknown> |
Token-gating config |
app |
RoomApp | undefined |
App binding (see RoomApp below) |
memberCount |
number | undefined |
|
lastMessage |
AppLastMessage | undefined |
Denormalised most-recent message |
createdAt |
string | Date | undefined |
|
updatedAt |
string | Date | undefined |
Nested under AppRoom.app. Present only when the room is managed by an app.
| Field | Type | Notes |
|---|---|---|
appId |
string |
Managing app ID (matches your app) |
appCreated |
boolean |
true if your app created this room; false if assigned by owner |
allowOwnerDetach |
boolean | undefined |
Owner can disconnect the app. Default false for app-created rooms. See locks |
allowOwnerDelete |
boolean | undefined |
Owner can delete the group. Default false |
allowOwnerTransfer |
boolean | undefined |
Owner can transfer ownership. Default false |
Reading locks:
const { room } = await cherry.groups.get(roomId);
if (room.app?.appCreated) {
console.log('Owner detach allowed?', room.app.allowOwnerDetach ?? false);
}interface AppRoomSettings {
isPublic?: boolean;
maxMembers?: number;
[key: string]: unknown; // free-form extension
}| Field | Type | Notes |
|---|---|---|
id |
string |
Message ID |
senderId |
string |
Wallet that sent the message |
content |
string |
Plain-text body |
createdAt |
string | Date |
Sent at |
| Field | Type | Notes |
|---|---|---|
id |
string |
Message ID |
roomId |
string |
Owning room |
senderId |
string |
Server-derived sender |
content |
string |
Plain-text body |
messageType |
string | undefined |
Optional discriminator |
metadata |
Record<string, unknown> | undefined |
For app-bot sends, the server merges in senderType: 'app_bot', appId, appBotDisplayName, appBotAvatarUrl |
attachments |
unknown[] | undefined |
Reserved |
createdAt |
string | Date |
|
deleted |
boolean | undefined |
True if the message has been soft-deleted |
| Field | Type | Notes |
|---|---|---|
userId |
string |
Member wallet |
roomId |
string |
|
status |
string |
E.g. 'active', 'requested', 'rejected' |
role |
AppMemberRole | 'owner' |
Effective role |
banned |
boolean | undefined |
|
muted |
boolean | undefined |
|
mutedUntil |
string | Date | undefined |
Expiry of timed mute |
type AppMemberRole = 'admin' | 'moderator' | 'member';
// Note: 'owner' is intentionally absent — not assignable via SDK.See client.me() and client.getPublicAppInfo(appId).
Summary (see per-method sections above for full details):
| Type | Used by |
|---|---|
CreateGroupInput |
groups.create |
UpdateGroupInput |
groups.update |
InviteMembersInput |
members.invite |
BanMemberInput |
members.ban |
MuteMemberInput |
members.mute |
SetRoleInput |
(internal — members.setRole accepts role as a parameter) |
SendMessageInput |
messages.send |
| Type | Used by | Fields |
|---|---|---|
ListGroupsQuery |
groups.list |
cursor?, limit? |
ListMessagesQuery |
messages.list |
before?, limit? |
| Type | Used by | Shape |
|---|---|---|
CreateGroupResponse |
groups.create |
{ roomId } |
ListGroupsResponse |
groups.list |
{ rooms: AppRoom[], nextCursor? } |
GetGroupResponse |
(internal) | { room: AppRoom } |
InviteMembersResponse |
members.invite |
{ invited, skipped, accepted } |
ListMessagesResponse |
messages.list |
{ messages: AppMessage[], nextCursor? } |
AppMeResponse |
client.me |
{ appId, scopes, botWallet, botDisplayName?, rateLimits? } |
AppPublicInfo |
client.getPublicAppInfo |
{ name, botDisplayName?, botAvatarUrl?, ownerWallet?, botWallet? } |
All SDK methods that hit the server throw CherryAppsError on non-2xx
responses. Webhook helpers throw CherryAppsError with code
INVALID_PAYLOAD for malformed bodies; verifyWebhook never throws.
class CherryAppsError extends Error {
readonly name: 'CherryAppsError';
readonly message: string; // human-readable
readonly code: string; // machine-readable, e.g. 'ROOM_NOT_FOUND'
readonly status: number; // HTTP status (0 for network / timeout / parse errors)
readonly details: unknown; // optional server-provided context
}code |
status |
Meaning |
|---|---|---|
REQUEST_TIMEOUT |
0 |
Aborted by AbortController after config.timeout ms |
NETWORK_ERROR |
0 |
fetch rejected (DNS, TCP, TLS, etc.) |
INVALID_PAYLOAD |
0 |
parseWebhook got malformed JSON / missing envelope fields |
HTTP_ERROR |
various | Server returned non-2xx without a structured {error, message} envelope |
MISSING_SCOPE |
403 |
App token lacks the required scope |
RATE_LIMITED |
429 |
Per-minute / per-day rate limit hit (Retry-After header returned) |
ROOM_NOT_MANAGED_BY_APP |
403 |
Cross-app isolation — room belongs to a different app |
ROOM_NOT_FOUND |
404 |
Room does not exist |
MEMBER_NOT_FOUND |
404 |
Wallet is not a member of the room |
CANNOT_MODERATE_OWNER |
403 |
Cannot moderate the room owner |
CANNOT_DELETE_ASSIGNED_ROOM |
403 |
Cannot delete a room obtained via assignment |
INVALID_ROLE |
400 |
setRole cannot assign 'owner' |
DETACH_DISABLED_BY_APP |
403 |
Owner tried to disconnect a locked room (!allowOwnerDetach) |
DELETE_DISABLED_BY_APP |
403 |
Owner tried to delete a locked room (!allowOwnerDelete) |
TRANSFER_OWNER_DISABLED_BY_APP |
403 |
Owner tried to transfer ownership of a locked room (!allowOwnerTransfer) |
INVALID_INPUT |
400 |
DTO validation failed (missing/wrong-type fields) |
import { CherryAppsError } from '@cherrydotfun/apps-sdk';
try {
await cherry.groups.get('does-not-exist');
} catch (err) {
if (err instanceof CherryAppsError) {
console.log(err.status); // 404
console.log(err.code); // 'ROOM_NOT_FOUND'
console.log(err.message);
console.log(err.details);
}
throw err;
}The example/ directory contains a React playground that
exercises every SDK method through forms and streams live webhooks over
Server-Sent Events. Useful for first-time setup and exploring response
shapes without writing glue code.
cd example
cp .env.example .env # fill CHERRY_APP_KEY + CHERRY_WEBHOOK_SECRET
bun install
bun run dev # http://localhost:5173See CHANGELOG.md.
MIT — see LICENSE.