API & SDK
Everything the Logbook web app does can be done without it. The web app is just one client of the protocol.
#Four ways to integrate
| You want to | Use |
|---|---|
| Let an AI agent (claude.ai, Claude Code, Cursor, your own bot) run polls and vote | MCP: remote connector or local server; or draft links with no integration |
| Create campaigns, respond, manage address lists, read private results from your own code | TypeScript SDK |
| Create or read groups from your own code | TypeScript SDK, as building blocks (see Groups in the SDK) |
| Show public campaigns and results in a dashboard, a bot or a non-JavaScript app | REST API (read-only) |
| Full control, any language with a Sui SDK | The Move package directly |
#TypeScript SDK
npm install @logbookprotocol/sdk @mysten/sui @mysten/seal
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { LogbookClient } from '@logbookprotocol/sdk';
const logbook = new LogbookClient({ signer: Ed25519Keypair.fromSecretKey(process.env.SUI_KEY) });
const { campaignId, url, password } = await logbook.createCampaign({
title: 'Board election',
questions: [{ text: 'Candidate', type: 'single_choice', options: ['Ana', 'Boris', 'Chen'] }],
endTime: Date.now() + 3 * 24 * 3600 * 1000,
access: 'allowlist', // 'open' | 'link' | 'allowlist'
visibility: 'after_end', // 'everyone' | 'access' | 'voters' | 'creator' | 'after_end'
allowlist: ['0x...'],
});
await voter.submitResponse(campaignId, [0]);
const { results, complete } = await logbook.getResults(campaignId);
The SDK covers reading (getCampaign, openCampaign, listCampaigns, getResults, getDocuments, getReport), writing (createCampaign, updateCampaign, deleteCampaign, updateAllowlist, submitResponse) and transaction preparation for apps that sign elsewhere (prepareCreateCampaign, prepareSubmitResponse). It works in Node 22.12+ and in browsers, with a Sui keypair or with your own signing callbacks (wallets, KMS); @mysten/sui and @mysten/seal are peer dependencies.
Answers are validated against the contract's rules before anything is sent. If the creator edits the campaign or removes an address from its list between reading and sending, the contract refuses the response, and submitResponse prepares it again and resends it once. Results come back decrypted for whatever your account is allowed to read; complete says whether every response was counted, and locked, failed and invalid responses are counted separately. The creator of a password campaign gets its password back at any time with getCampaignPassword.
#Groups in the SDK
From SDK 0.7.0 LogbookClient has methods for groups, built on the same code the site runs:
| Method | What it does |
|---|---|
listGroups({ limit, cursor, search }) | the public groups, a page at a time |
getGroup(id, { invitationLink?, personalLink? }) | one group: its texts (a private group's for its owner, admins and current members), how people join, who counts once, what the account is to it and which ways in are open to it now |
myGroups() | the groups of the account, the campaigns that wait for its response, the groups it was appointed to or offered |
listGroupMembers(id), listGroupCampaigns(id) | the members now (with what their cards show to members), and the campaigns for the members of a group |
joinGroup(id, { invitationLink?, personalLink?, card? }) | join by the Join button, an invitation link, or the personal link of a seat on a list |
applyToGroup(id, { text?, card? }), withdrawApplication(id), leaveGroup(id) | apply (the text and the card are encrypted for the admins), take the application back, leave |
createGroup({ name, private?, ways, cardFields?, cardRequired? }) | a group of your own, public or private, with how people join and its member card |
createInvitation(id, { places, expiresAt }) | an invitation link (a secret, returned once) |
listApplications(id), reviewApplications(id, { approve?, reject? }), removeGroupMembers(id, members) | for the owner and admins |
createCampaign({ ..., groups, waitingPeriodMs? }) | a campaign for the members of groups you run |
NOT_ALLOWED before anything is signed. A group that needs a check the client cannot get fails with NOT_AVAILABLE: a World ID check (not built), or an email check where the site has none switched on, or where the client holds no Google sign-in to show the check server. A client that does hold one passes googleIdToken (a function that returns Google's id_token of the session while it is fresh; it is sent to checkServerUrl only, by default https://logbook.zone, never to appUrl): joinGroup then asks the check server once and the join carries its statement. An agent's session keeps no Google token, so such a group is joined on the site; after that the account has its mailbox confirmed in the community, and joinGroup opens the other groups of that community without asking again. Who pays follows the rules of the site (see Who Pays in Groups). A group is changed the way it was created: one created with sponsored gas takes its owner's and admins' calls only through the sponsor (SPONSOR_REQUIRED otherwise), one created on its creator's own gas only on own gas. What the Logbook treasury paid for, only the treasury frees: an application it paid for is approved, and withdrawn, through the sponsor in any group, so reviewApplications sends a selection of both kinds in two transactions, and a client without sponsor gets SPONSOR_REQUIRED before anything is signed. The one application that cannot be approved at all (see Who Pays in Groups) is named in skipped and not sent.
Not on the client: transferring a group, appointing and removing admins, the tree of groups, a list of seats, the settings of a group, archiving and deleting. They are done on the site, or with the building blocks below.
The building blocks are exported too:
GroupTx: builds every call of the modulegroupsinto one transaction.createGroup(input, configure)begins a group, sets it up and shares it; there are builders for invitations, joining with a card, applications and their approval, admins and ownership, the tree of groups, lists, andaudience(...)withcreateGroupCampaignfor a campaign for the members of groups- Readers:
fetchGroup,fetchMemberShards,fetchMembership,fetchMembersPage,fetchRequestsPage,fetchInvitations,fetchScopeGroups,fetchGroupCampaigns,fetchMyGroups,fetchGroupCatalogueand others - Lists as Merkle trees (
buildCommunityRoster,buildFilterList,verifyRosterPath), invitation links and personal links (generateLinkKey,signInvite,signRosterEntry) - The card form and cards, and sealing and opening with Seal (
sealGroupMetadata,openGroupMetadata,sealCard,openCardParts,sealRequestText,sealAdminFile) GROUP_ERRORand messages for the abort codes of groups inexplainTransactionError
signAndExecute sends it with the client's signer). The type declarations of the package (dist/index.d.ts) name them all.
A campaign for the members of groups (access group) is read by the client, the REST API and the MCP tools: getCampaign, GET /api/v1/campaigns/{id} and logbook_get_campaign return audience (the groups, when the participants were frozen, the waiting period for new members, who counts once). From SDK 0.7.0 the client also answers it and decrypts it, for an account that is one of its participants (a member of one of its groups when it was created): submitResponse sends the response through the account's own record in the group, and openCampaign, getResults and getReport open what its visibility lets that account read; its creator reads it too. Any other account gets NO_ACCESS with the reason, before anything is signed or sent, and getParticipation says in advance whether the account is a participant. The REST API has no account and returns only what is public. deleteCampaign of such a campaign uses the deletion of the groups contract; it cannot be edited.
The full reference is in the SDK README.
#AI agents: MCP
The Model Context Protocol gives an AI agent the Logbook protocol as tools: list and read campaigns, read the documents a campaign is about, create a poll, manage an address list, respond, read results, and work with groups (below). There are two ways to connect an agent, and they differ in one thing: where the key that signs for you lives.
| What | Remote connector | Local server |
|---|---|---|
| Address | https://logbook.zone/mcp | runs on your computer |
| Works in | any chat with connectors: claude.ai, the Claude apps, ChatGPT, bots run by others | Claude Code, Claude Desktop, Cursor, your own agent |
| Setup | add the connector, sign in with Google | add npx -y @logbookprotocol/mcp to the client's config |
| Account | your Google (zkLogin) account, or read-only | your Google account, the agent's own Sui key, or read-only |
| Where the session key is | on Logbook servers, encrypted, for the session only | on your computer; Logbook never sees it |
| Private data | decrypted on Logbook's edge servers for the connected account, within limits per call: 60 encrypted responses, 6 names of private groups, 30 member cards, 10 applications | decrypted on your computer, without those limits |
| Ending a session | Connected apps on your account page (Settings), disconnecting the app, or asking it to sign out; revoked immediately. It also ends when its proof expires, after 7 days at most | sign the agent out; the key is deleted |
https://logbook.zone/mcp as a custom connector (in claude.ai: Settings → Connectors → Add custom connector). Logbook shows a consent page; choose Continue with Google to let the app act as your account for up to 7 days (the session ends when its zkLogin proof expires), or Continue without an account for read-only access. Standard OAuth 2.1 with dynamic client registration and PKCE, so other MCP clients work the same way. The trade-off is stated on the consent page: to sign for you without anything installed on your computer, Logbook keeps a temporary signing key for the session on its servers, encrypted. Anyone with that key could act as you during the session, so Logbook itself has to be trusted for its duration. For the same reason, private campaigns and answers your account may read are decrypted on Logbook's edge servers when the app asks for them; the SDK and the local server decrypt on your own machine. One results call decrypts at most 60 encrypted responses there: the rest count as failed and the tally is marked incomplete, so read larger private campaigns on the site, with the SDK or with the local server. In exchange the session can be revoked at any moment: under Connected apps on your account page (Settings), by disconnecting the app, or with the logbook_logout tool.
Local server. Nothing custodial: the agent's own Sui key stays in the process on your machine, and a Google session is a time-limited proof bound to a temporary key that never leaves your computer (2 days by default, 7 at most). Logbook cannot act as you and cannot revoke the session either; signing the agent out deletes the key.
The package is on npm and runs with npx (Node 22 or newer). In Claude Code, add it for your user, so its configuration, and a key if you give one, stays out of your repositories:
claude mcp add logbook --scope user -- npx -y @logbookprotocol/mcp
Other clients take an entry like the one below. A project's .mcp.json is meant to be committed, so never write a key into it: reference an environment variable, which Claude Code fills in when it starts the server.
{
"mcpServers": {
"logbook": {
"command": "npx",
"args": ["-y", "@logbookprotocol/mcp"],
"env": { "LOGBOOK_PRIVATE_KEY": "${LOGBOOK_PRIVATE_KEY}", "LOGBOOK_SPONSOR": "1" }
}
}
}
Without LOGBOOK_PRIVATE_KEY the server is read-only until you sign it in with your Google account (npx -y @logbookprotocol/mcp login, or ask the agent to sign in).
Groups. From MCP 0.8.0 both servers have tools for groups:
| Tool | What it does |
|---|---|
logbook_list_groups, logbook_get_group, logbook_my_groups, logbook_list_members | read: the public groups; one group with how people join, who counts once and what the account may do next; the groups of the account and what waits for it; the members of a group |
logbook_join_group, logbook_apply_to_group, logbook_withdraw_application, logbook_leave_group | the account's own membership: join by the Join button, an invitation link or the personal link of a seat; apply with a text and a card; take an application back; leave |
logbook_create_group, logbook_create_invitation, logbook_list_applications, logbook_review_application, logbook_remove_member | for the owner and admins: create a group, make an invitation link, read and decide applications, remove a member |
logbook_create_campaign takes groups (the account must be their owner or admin), logbook_list_campaigns lists those of one group (groupId) and those that wait for the account's response (waiting), and logbook_get_campaign, logbook_submit_response and logbook_get_results read, answer and tally one for an account that is one of its participants.
The agent is told the rules that matter here. Who is a member of a group, and who applied, is public and permanent on-chain, of a private group too, so it confirms joining, applying, leaving, creating a group, an invitation, a decision on an application and a removal with you first, saying exactly what will be sent. An invitation link is a secret: it is given to you and to nobody else. Names, descriptions, cards and application texts are written by other people and are labelled as untrusted content, never as instructions. A group whose community requires a World ID check cannot be joined yet. An email check is made in a browser, with a Google sign-in on the site: the tools hold no Google sign-in, so they say where to join, and once the account has its mailbox confirmed in a community they open the other groups of that community.
Not tools, on purpose: transferring a group, appointing and removing admins, the tree of groups, a list of seats, the settings of a group, archiving and deleting. They are done on the site.
On the remote connector one call reads a bounded part of the chain and opens a bounded number of encrypted items (the table above); a result says what it left out (complete, locked, not-opened), and the local server has no such limits.
Both servers expose the same tools and the same rules: responses are permanent, so the agent is instructed to confirm answers with the user first, and on-chain text is labelled as untrusted content. Use the remote connector when convenience matters and the stakes are ordinary; use the local server when nobody but you may ever hold a key that can vote as you. Setup details and the tool list are in the MCP README.
Agents that only need public data can use the REST API below; it is described for machines at /api/v1/openapi.json and /llms.txt.
#Draft links: no integration at all
The third way for an agent, and the only one that needs nothing installed and nothing connected: a link. A chat composes the campaign and gives you a link to the creation form, pre-filled; you open it, review, sign in and publish. For voting, a link to the campaign page with the answers pre-selected; you check them and submit. Nothing is created or sent by the link itself, so the keys stay with you and the trust model is untouched: whoever signs is the person who clicks.
https://logbook.zone/campaigns/new#title=Lunch&q1=Where?|Pizza|Sushi&q1x=1&q2=Wishes&q2opt=1&end=3d&access=link&visibility=voters
https://logbook.zone/campaigns/0x…/participate#a1=2&a2=1,3&a3=No+onions
Questions are q1, q2… with options after | (none = free text), q1m=1 for multiple choice, q1x=1 for an "Other" option, q1opt=1 for optional; end is a date or a duration (3d, 2h); access (open, link, allowlist, with allowlist=0x…,0x…) and visibility set who can participate and who can read the answers. Answers are option numbers as shown on the page. The whole draft goes after the #, together with the password of a password campaign in an answers link (#key=…&a1=2): browsers never send that part of a link to a server, so the questions of a private campaign, its address list and the answers to it stay out of server logs. Older links with the draft or the answers after a ? (?a1=2) still open, once: the page then rewrites the address, so that a reload does not send them to the server again. The full format is in llms.txt. Both MCP servers use the same links when a connection has no account: instead of failing, logbook_create_campaign and logbook_submit_response return a draftUrl, and logbook_draft_link builds one on request. Anyone can write such a link, so the page says where the draft came from, lists what it sets besides the wording (who can participate, who can read the answers and every address on the list) and asks you to review it before publishing.
#Gas sponsorship
By default the signer pays gas. If your users have no SUI, the Logbook treasury can pay for Logbook transactions:
new LogbookClient({ signer, sponsor: true }); // anonymous quota
new LogbookClient({ signer, sponsor: { apiKey: 'lbk_...' } }); // integrator quota
Sponsorship takes two requests, so that nothing is charged to a sender address before its owner has signed. The SDK and the local MCP server make both for you (SDK 0.3.0 and MCP 0.4.0 or later: earlier versions make a single request, get no signature back and can no longer use sponsorship, so update them). From another language:
- 1
POST /api/sponsorwith{ "txSerialized": …, "sender": … }: the transaction data as JSON (JSON.stringify(tx.getData()), plain values in base64; the olderTransaction.serialize()format is still accepted, but takes up to five times the space), and the sender's address. The treasury checks it, adds its gas, simulates it and answers{ txBytes, ticket, sponsorAddress, expiresAt }: the finished transaction in base64, and a ticket that is valid for about 5 minutes. - 2Check that
txBytesholds the calls you asked for, with you as the sender and someone else paying, and sign it. ThenPOST /api/sponsor/signwith{ "txBytes": …, "ticket": …, "signature": … }answers{ sponsorSignature }. ExecutetxByteswith both signatures, yours and the sponsor's.
- Send the API key, if you have one, as the
X-Logbook-Api-Keyheader of both requests. Bodies are limited to 512 KiB for the first request and about 200 KiB for the second: enough for the largest transaction Sui executes (128 KiB) - The treasury pays from its address balance, so a transaction that is prepared and never signed locks nothing
- Without a key, per day: 100 transactions prepared per IP address (IPv6 per /64, and 300 per /48) and 30 sponsored per sender address
- Gas is also limited per day, in SUI, when the treasury signs: 3 SUI per sender address and, without a key, 6 SUI per IP address (IPv6 per /64, and 10 SUI per /48); at most 2 SUI in one transaction. An ordinary transaction costs 0.01–0.1 SUI; almost all of that is storage, so a campaign with a long address list costs more (about 0.0016 SUI per address)
- With an API key: a daily quota per key (1000 transactions and 50 SUI by default) and per sender address within the key (20 by default); a key can be bound to the websites (origins) that may use it from a browser
- Websites on other domains must use an API key, in both requests
- Only calls into the Logbook package are sponsored, after a simulation: a transaction that would fail is refused, not paid for. A campaign transaction holds at most 4 calls, with plain values or shared objects as inputs; group transactions have a check of their own (below)
- Preparing a transaction counts only against the IP address or the API key. The sender's own count and the SUI budgets are charged once it has signed, so nobody can use up the limits of an address they do not control
- A daily cap on all sponsorship together can apply as well; once it is reached,
/api/sponsor/signanswers 503 until 00:00 UTC - A 429 means a daily limit is used up; a 503 means sponsorship is paused or its limits cannot be checked right now. Pay gas yourself meanwhile, except for changes to a campaign created with sponsored gas, which cannot be paid with your own gas
- The creator's changes to a campaign (edits, address list, documents, reports, deletion) must be paid the way it was created, so the storage refund goes back to whoever paid: manage a sponsored campaign with a client that has
sponsor, and one you paid for with a client without it
#Sponsored group transactions
The treasury also pays for transactions of the module groups, through the same two requests, with rules and daily limits of their own:
- Members are sponsored in any group: joining, applying, leaving, a member card, and a response to a campaign for the members of groups (charged like any response)
- Owners and admins are sponsored only on a group that was created with sponsored gas. A group created with its creator's own gas is changed with the sender's own gas. The contract enforces this for the calls that write the group (abort 105 of
groups), and the sponsor applies it to the rest - Creating a group: 2 per sender address per day, 3 per IP address (IPv6 per /64, and 6 per /48), 20 per API key, and 100 for everyone together
- Per group and day: 100 sponsored applications, 60 transactions of its owner and admins, and 4 joins, applications and leaves per address
- Each address has 100 units a day for group transactions (300 per IP address, 600 per /48, 3000 per API key). A unit stands for storage that never comes back: most calls cost 1, a join 2, a newly taken seat 3, a new group 3, the first join into a new part of a group's member list 5, and a campaign 1 plus 2 for each of its groups
- All group sponsorship together is limited to 5 SUI a day, apart from responses
- A group transaction holds calls of the module
groupsonly, at most 280 commands, and results of one call may be arguments of the next
#REST API
Public, read-only, cacheable, CORS-enabled. Base URL: https://logbook.zone/api/v1
| Endpoint | Returns |
|---|---|
GET /api/v1 | API description, package, Config and Registry ids, Seal key servers |
GET /api/v1/openapi.json | OpenAPI 3.1 description of this API |
GET /api/v1/campaigns | Campaigns, newest first, a page at a time: { limit, nextCursor, campaigns }. Query: creator, status (active, ended), limit (1-100), cursor |
GET /api/v1/campaigns/{id} | One campaign with its questions and settings |
GET /api/v1/campaigns/{id}/responses | Who responded and when, a page at a time, in the order of the campaign's responses table: { campaignId, answersEncrypted, total, limit, nextCursor, responses }. Query: limit (1-500), cursor. The answers when they are public |
GET /api/v1/campaigns/{id}/results | Tally of public answers, and matchesOnChainTally: whether it equals the counts the contract keeps |
curl "https://logbook.zone/api/v1/campaigns?status=active&limit=10"
curl "https://logbook.zone/api/v1/campaigns?status=active&limit=10&cursor=$NEXT_CURSOR"
Lists come a page at a time: pass the nextCursor of an answer back as cursor until it is null. A page can hold fewer items than limit, even none, and still have a next one, because deleted campaigns and the status filter are applied to what the page read. There is no total for campaigns and no offset: an offset above 0 is answered with 400 offset_unsupported. On the responses endpoint total counts all responses of the campaign; the pages follow the order of the campaign's on-chain responses table, not the time of the responses, and within a page the oldest come first. Answers are cached at the edge for about 10 seconds, so a new campaign or response can take that long to show up.
Private campaigns appear with encrypted: true: title, questions and options are null, and the results endpoint answers 403 answers_encrypted. That data is encrypted on-chain; reading it takes the SDK and an account the campaign's rules allow (for a campaign with access group: the site, as a member). Errors have the shape { "error": { "code", "message" } }; a 502 upstream_error means Sui could not be read right now, not that the data is missing.
The REST API has no endpoints for groups. GET /api/v1/campaigns/{id} returns a campaign for the members of groups with its audience; the lists do not carry it.
#The Move package
The smart contract is the real API. Any Sui SDK (TypeScript, Rust, Python, Go) can call create_campaign, submit_response and the other functions described on the Smart Contract page, and read Campaign and Response objects and events. The same holds for the module groups: its functions, its Group and member shard objects and its events. Package, Config and Registry ids are returned by GET /api/v1; every function takes the shared Config.
Public campaigns need nothing else. Private campaigns also need the encryption scheme described on the Data Storage page (content key, password wrapping and proof, Seal identities, answer binding and padding); today it is implemented in the TypeScript SDK. Groups need more of the same kind: the format of the card form and of cards, the lists as Merkle trees, the signatures of invitation links and personal links, and the Seal identities of groups. The SDK exports all of them.