Dotallio Public API Keys
The complete guide to creating, securing, using, rotating, and operating Dotallio's Unkey-backed public API keys.
Audience: integration developers, AI-agent authors, workspace owners, security administrators, and Dotallio deployment maintainers.
API version:
/api/v1Machine-readable contract:
/api/v1/openapi.json
Table of contents
- Start here
- What a public API key is
- Security model
- Create a key
- Choose permissions
- Choose board access
- Plans and limits
- Authenticate requests
- Five-minute quickstart
- Magician-suite Prospects walkthrough
- Endpoint reference
- Understand board schemas
- Field names and addressing
- Supported value formats
- AI-powered columns
- Create complete rows
- Batch ingestion
- Convert CSV to JSON
- Build a board-aware AI agent
- Movie-board example
- Research-and-expand example
- Read and paginate items
- Deduplication and retry safety
- Responses and rate-limit headers
- Error reference
- Key lifecycle
- Security practices
- Incident response
- Deployment and Unkey setup
- Extension keys are separate
- Troubleshooting
- Frequently asked questions
- Production checklists
Start here
The normal integration flow is:
Create a scoped key
→ GET /api/v1/boards
→ GET /api/v1/boards/{boardId}/schema
→ normalize source data to JSON rows
→ POST /api/v1/boards/{boardId}/items
→ inspect warnings and created IDs
→ GET items when reconciliation is needed
The three rules that prevent most integration problems are:
- Fetch the board schema before writing. Do not guess field names or option values.
- Send complete, grounded rows. If your agent already knows a value, including a value for an AI-powered column, send it.
- Leave unknown AI cells empty. Dotallio can run their configured
autocomplete when the cell is omitted,
null, or empty.
Choose the shortest path that matches your job:
| You want to… | Start with… |
|---|---|
| make one authenticated request | Five-minute quickstart |
| install Magician and POST a Prospects lead | Magician-suite Prospects walkthrough |
| import a CSV | Convert CSV to JSON |
| build a research agent | Build a board-aware AI agent |
| understand every endpoint | Endpoint reference |
| respond to a leaked key | Incident response |
| configure Unkey for a deployment | Deployment and Unkey setup |
The public API is deliberately schema-first. Examples in this guide show the current contract, but an integration must still read the installed board's schema. Template updates, renamed columns, and workspace customizations can change the correct field addresses without changing the endpoint URL.
Minimal quickstart
export DOTALLIO_ORIGIN="https://your-dotallio.example"
export DOTALLIO_API_KEY="dot_live_replace_with_your_key"
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards"
Never put a real API key in source control, a screenshot, a browser bundle, an AI prompt, a support ticket, or a client-side environment variable.
What a public API key is
A public API key is a server-to-server credential that represents one Dotallio user inside one workspace.
It is intended for:
- backend services;
- scripts and command-line tools;
- workflow runners;
- trusted AI agents;
- scheduled data importers;
- serverless functions;
- integration platforms that can store secrets securely.
It is not intended for:
- browser JavaScript;
- mobile binaries where a secret can be extracted;
- public forms;
- URLs or query strings;
- shared spreadsheets;
- frontend
NEXT_PUBLIC_*variables; - an internal Dotallio board-chat tool call.
Public API keys versus internal board chat
Dotallio's authenticated board chat uses server-side tools and the current user session. It does not need, receive, or expose a public API key.
Board chat can already:
- read existing columns and rows;
- learn preferences from current board contents;
- parse CSV and JSON;
- research current facts, links, and images;
- fill known values in AI-powered columns;
- leave unknown AI cells empty for autocomplete;
- deduplicate titles;
- insert large results in bounded batches;
- report partial failures and progress.
External agents use the public REST API. CSV is an input format for the agent, not a second REST dialect: the agent converts CSV to the documented JSON row contract before sending it.
Public API keys versus extension keys
Browser-extension keys use a separate Unkey API, root key, namespaces, identities, and limits. They are not interchangeable with public API keys.
Security model
Authorization is the intersection of three independent boundaries:
Unkey
verifies the secret, expiry, enabled state, and endpoint permission
AND
Dotallio public API policy
restricts workspace, board mode, board allowlist, tier, and quotas
AND
Dotallio product authorization + Supabase RLS
rechecks current membership, app role, hidden/deleted state, and row policy
All three must allow the operation.
This matters because a key is not a frozen copy of access. If the represented user is removed from the workspace, downgraded from editor to viewer, loses app membership, or loses access to a board, the key loses that access immediately on the next request.
Fail-closed behavior
The API does not fall back to local secret verification or unmetered access.
If Unkey, metering, or the local authorization store is unavailable, the API
returns a retryable 503. It does not continue with weaker security.
One-time secrets
Unkey generates the secret. Dotallio shows it once and never stores the plaintext value. Dotallio stores only safe identifiers and authorization metadata such as:
- local key ID;
- Unkey key ID;
- display prefix;
- owner user ID;
- workspace ID;
- permissions;
- board-scope mode;
- selected board IDs;
- expiry;
- provider metadata;
- bounded usage and lifecycle audit records.
If the secret is lost, rotate the key or create a replacement. It cannot be recovered.
Create a key
Open:
Account → Public API
Then:
- Select the workspace.
- Enter a recognizable key name.
- Choose an expiry from 1 through 365 days.
- Choose a permission preset or explicit permissions.
- Choose selected-board access or all-board access.
- Review the effective tier limits.
- Create the key.
- Copy the secret before dismissing the one-time panel.
- Store it in a production secret manager.
- Make a low-risk
GET /api/v1/merequest to verify it.
Key naming
Names are limited to 60 characters. Use names that answer both of these questions:
- Which system uses the key?
- Which environment uses it?
Good names:
Research agent — production
CRM nightly import — staging
Customer portal sync — production
Illusion catalog expander — test
Avoid names such as Key 1, API, or New key.
Key count
One user may have up to 10 active provider-backed public API keys. Revoke keys that are no longer used before creating more.
Environment prefixes
| Environment | Prefix | Example shape |
|---|---|---|
| Production | dot_live_ | dot_live_… |
| Development/test | dot_test_ | dot_test_… |
A production deployment rejects test-prefixed credentials, and a test deployment rejects live-prefixed credentials.
Choose permissions
Every endpoint requires one explicit permission.
| Permission | Allows | Endpoint |
|---|---|---|
profile.read | Verify identity and read the key's workspace profile | GET /api/v1/me |
boards.list | List boards allowed by the key policy | GET /api/v1/boards |
board.schema.read | Read columns, accepted values, and examples | GET /api/v1/boards/{boardId}/schema |
board.items.read | Read board rows | GET /api/v1/boards/{boardId}/items |
board.items.write | Create complete rows | POST /api/v1/boards/{boardId}/items |
Recommended presets
Discovery-only agent
profile.read
boards.list
board.schema.read
Read-only data agent
profile.read
boards.list
board.schema.read
board.items.read
Read-and-write board agent
profile.read
boards.list
board.schema.read
board.items.read
board.items.write
Least privilege
Grant only the permissions the integration actually calls. A write-only
ingestion service may technically need only board.items.write, but agents
usually also need schema and read access to avoid bad field mappings and
duplicates.
Write scope can be created only when the owner has an editor, admin, or owner role on eligible boards. Live role checks still run on every write.
Choose board access
A key belongs to exactly one workspace and uses one of two explicit board policies.
Selected boards: allowlist
This is the recommended default.
The key may access only the selected board IDs. An empty allowlist never means "all boards." Creation and policy update require at least one selected board, and a policy may include at most 200 board IDs.
Use selected-board mode for:
- single-purpose integrations;
- customer-specific sync services;
- agents that should not discover unrelated boards;
- write-enabled credentials;
- third-party systems with a narrow purpose.
All boards: all
The key may access every current and future board in the workspace that the represented user can access through current product permissions.
Use this only when future-board discovery is an intentional requirement.
All-board mode does not bypass:
- app membership;
- app role;
- workspace membership;
- hidden-board rules;
- deleted/inactive state;
- Supabase RLS;
- endpoint permissions.
Local and provider policy intersection
Dotallio stores the board policy locally and also places policy metadata on the Unkey key. The effective policy is the intersection. If synchronization is partial, the result can deny too much, but it cannot authorize too much.
Plans and limits
Public API availability and quotas are workspace-level subscription features.
| Tier | Requests per minute per key | Weighted operations per workspace per UTC month |
|---|---|---|
| FREE | API unavailable | API unavailable |
| MEMBER | 60 | 25,000 |
| PLUS | 120 | 100,000 |
| PRO | 300 | 500,000 |
| ENTERPRISE | Admin override, otherwise PRO fallback | Admin override, otherwise PRO fallback |
Weighted costs
| Operation | Cost |
|---|---|
Any authenticated GET | 1 |
Item POST | Number of fully validated rows, from 1 to 100 |
Examples:
- Listing boards costs 1.
- Reading a schema costs 1.
- Reading a page of 200 items costs 1.
- Creating 1 row costs 1.
- Creating 75 rows costs 75.
- Creating 100 rows costs 100.
The monthly counter uses one shared identifier for the workspace and UTC calendar month. Multiple keys cannot multiply the workspace budget.
What happens after a downgrade
If the workspace becomes FREE, existing keys immediately fail with
API_UNAVAILABLE_FOR_TIER. A previously issued secret is not a promise of
continued plan access.
Authenticate requests
Send exactly one credential header.
Bearer authentication
Authorization: Bearer dot_live_your_secret
API-key header
X-API-Key: dot_live_your_secret
Do not send both headers. Conflicting credential headers return
AMBIGUOUS_API_KEY.
Why CORS is not enabled
Public keys are server credentials, not browser tokens. The API intentionally does not provide wildcard browser CORS.
Use this architecture:
Browser or mobile client
→ your trusted backend
→ Dotallio public API
Do not use this architecture:
Browser bundle containing Dotallio secret
→ Dotallio public API
Five-minute quickstart
1. Verify the key
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/me"
Example response:
{
"ok": true,
"user": {
"id": "user-uuid",
"email": "owner@example.com",
"name": "Board Owner"
},
"workspaces": [
{
"id": "workspace-uuid",
"name": "Research Workspace"
}
]
}
Only the key's immutable workspace is returned.
2. List boards
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards"
Example response:
{
"ok": true,
"boards": [
{
"id": "board-uuid",
"name": "Movie Research",
"workspaceId": "workspace-uuid",
"workspaceName": "Research Workspace",
"appId": "app-uuid",
"appName": "Media Catalog",
"canInsert": true,
"schemaUrl": "/api/v1/boards/board-uuid/schema"
}
]
}
3. Read the schema
export BOARD_ID="board-uuid"
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$BOARD_ID/schema"
4. Create a row
curl -X POST \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Arrival",
"release_year": 2016,
"watched": true
}' \
"$DOTALLIO_ORIGIN/api/v1/boards/$BOARD_ID/items"
5. Read it back
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$BOARD_ID/items?limit=50&offset=0"
Magician-suite Prospects walkthrough
This chapter is the Magician Add Contact worked example: install a current
Hebrew or English magician/mentalist suite, create a least-privilege public API
key allowlisted to the installed Prospects board, and POST a lead row
there.
Prospects, not Companies. Website / server Add Contact for Magician suites writes to
PROSPECTS_BOARD_ID— titled לידים (Hebrew) or Leads (English). Do not allowlist or POST to לקוחות / Companies for this path. Companies remains the quote-CRM customer board; quote intake may still match/create customers there. Add Contact does not.
This matches the in-app App Settings → Add Contact / API intake panel (T5),
which resolves PROSPECTS_BOARD_ID and emits the same curl shape and field
titles used below.
Current packaged Magician templates:
| Property | Hebrew suite | English suite |
|---|---|---|
| Template name | Hebrew Magician/Mentalist Quote Suite | English Magician/Mentalist Quote Suite |
| Template ID | service-provider-quote-suite-he-magician-mentalist | service-provider-quote-suite-en-magician-mentalist |
| Prospects board | לידים (PROSPECTS_BOARD_ID) | Leads (PROSPECTS_BOARD_ID) |
| Fragment | fragments/prospect-leads-board.he.v1 | fragments/prospect-leads-board.en.v1 |
| Companies (quotes) | לקוחות (COMPANIES_BOARD_ID) — not Add Contact | same token; Magician Add Contact still uses Prospects |
| Quote/intake board | הצעות מחיר (EVENTS_BOARD_ID) | הצעות מחיר (EVENTS_BOARD_ID) |
| Default locale | Hebrew / RTL | English |
Template IDs and board tokens are installation-time identifiers. External API
calls use the real board UUID from App Settings or GET /api/v1/boards.
Understand the three input paths
- App setup questionnaire: the owner answers this once after installation. It configures branding, language, tax, pricing defaults, and generated documents.
- Quote intake questionnaire: the performer or customer answers this for each inquiry. It runs the suite-specific intake workflow, creates a quote, and may match or create a Companies customer by phone.
- Public API Add Contact: a trusted server posts complete rows to the
Prospects / Leads board. It does not impersonate quote intake and
does not invoke
template.intake.submit.
App setup questionnaire
→ configures the installed suite
Quote intake questionnaire
→ quote workflow (+ Companies match/create by phone)
POST /api/v1/boards/{prospectsBoardId}/items
→ creates Prospects / Leads row(s) directly
Use the intake form when you want a quote and its suite workflow. Use Add Contact / the public API when a website form or trusted agent already has a lead to land on לידים / Leads.
Step 1: install the suite with AI chat
This is the preferred natural-language path:
- Open the workspace where the suite should live.
- Open the main AI chat.
- Click the Create App mode chip. Its input changes to What app should I build?
- Paste a request for the locale you need:
Install a Hebrew magician/mentalist business suite with quote intake, a
Prospects/Leads board, quote PDFs, follow-up, and the existing magician-specific
package. Prefer the existing Hebrew Magician/Mentalist Quote Suite template
instead of creating a duplicate. Keep the לידים (Prospects), לקוחות
(Companies), and הצעות מחיר boards and the public quote-intake form.
For English, ask for English Magician/Mentalist Quote Suite
(service-provider-quote-suite-en-magician-mentalist) and keep Leads
(Prospects).
- Send the message.
- Read the AI's proposed template/persona, language, feature groups, and branding plan. If it proposes removing Prospects/Leads, reply before the install runs.
- Let the AI use the existing template search and install flow.
- When the installed app opens, continue to the setup questionnaire.
The AI chat installs the packaged app through Dotallio's authenticated internal tooling. Do not give the chat a public API key. The key is only for the external server or agent that will send Prospects rows later.
Alternative: install it from the template picker
If you prefer the manual UI:
- Open the app switcher in the sidebar.
- Click Create New App.
- In Create a new app, stay on App Templates.
- In Search app templates…, search for
magician. - Select Hebrew Magician/Mentalist Quote Suite or English Magician/Mentalist Quote Suite.
- Review What this template creates and confirm a לידים / Leads board is listed.
- Click Install.
- Wait for the app to open; do not click Install twice while it is working.
Do not use the deprecated magician-apps/ tree. Live Magician suites ship from
app-templates.
Step 2: answer the app setup questionnaire
The first-run dialog is titled Finish app setup and contains Set up your quote app. Complete it before wiring the external integration.
| Question | Required? | Current default or choices | What it controls |
|---|---|---|---|
| Feature selection | varies | template feature groups | Which optional packaged capabilities stay enabled |
| Document language | yes | Hebrew, English, Hebrew + English | Quote PDFs and intake-form language |
| Your name or stage name | yes | empty | Title printed on every generated quote PDF |
| Phone | no | empty | Booking phone in the quote footer |
| no | empty | Reply/booking email in the quote footer | |
| Logo | no | PNG or JPEG, up to 5 MB | Brand logo in generated material |
| VAT rate | yes | 17% | Tax calculations and document defaults |
| Validity | yes | 30 days | Default quote validity, 1–365 days |
| Currency | yes | ILS, USD, or EUR | Monetary formatting and defaults |
| Starting price for a 1-hour show | no | empty | Context for AI pricing suggestions |
| Brand voice | no | Playful, Professional, Mystical | Tone used when AI personalizes quote copy |
Recommended magician example:
Document language: Hebrew + English
Your name or stage name: Maya Lev — Mentalist
Phone: +972 50 555 0142
Email: bookings@example.com
VAT rate: 17
Validity: 30
Currency: ILS
Starting price for a 1-hour show: 3500
Brand voice: Mystical
Then:
- Upload a logo only if it is ready and approved.
- Click Continue.
- Wait for Setup complete.
- Follow the product tour. Identify Your intake form, Prospects / Leads, Companies, and quotes.
- Complete or skip the optional photo wizard according to your content policy.
The stage/business name is the one field you should not leave as a template placeholder. It becomes customer-facing document copy.
To edit these answers later, open the installed app's Pack Settings, choose the setup-answer control, edit the same questionnaire, and click Save.
Step 3: know when quote intake still uses Companies
The installed הצעת מחיר חדשה form still drives the quote workflow and may match or create a לקוחות customer by phone. That is correct for quotes and is not the Add Contact API path.
To test the native suite flow:
- Open the app's intake page.
- Find the form titled הצעת מחיר חדשה.
- Fill the event, audience, and pricing fields.
- Click יצירת הצעה.
- Confirm the quote appears on הצעות מחיר.
- Open לקוחות only to confirm quote-side customer match/create.
Website lead capture and the public API examples below target לידים / Leads instead.
Step 4: preferred path — App Settings Add Contact panel
After install, open the Magician app's App Settings. The Add Contact / API intake card:
- Resolves the live
PROSPECTS_BOARD_IDUUID and shows the board label (לידים (Prospects)orLeads (Prospects)). - Mints a public API key named like
Add Contact — Prospects, allowlisted to that UUID only, with:board.schema.readboard.items.readboard.items.write
- Shows a copy-paste curl that fetches schema, then
POSTs a Prospects row using the packaged HE or EN field titles,Intake source/מקור פנייה=website, and optional sandboxedtalkingPoints.
Use that panel when you can. The rest of this chapter documents the same contract manually so an integration developer can reproduce it from Public API settings or a script.
Step 5: inspect Prospects columns before writing
Open לידים (Hebrew) or Leads (English). Packaged writable intake columns currently include:
| HE title | EN title | Type | Notes |
|---|---|---|---|
| שם מלא | Full name | text | primary lead name |
| טלפון | Phone | phone | primary contact phone |
| אימייל | text | contact email | |
| מקצוע | Profession | text | used in the settings-panel example |
| חברה | Company | text | optional organization |
| סוג אירוע מתעניין | Interested event type | label | use live options |
| יום הולדת | Birthday | date | YYYY-MM-DD |
| ילדים וימי הולדת | Kids and birthdays | longText | free text |
| הרגע הקסום | The magic moment | longText | show-side opener context |
| נפגשנו באירוע | Met at event | text | where you met |
| תאריך היכרות | Date met | date | YYYY-MM-DD |
| פולו אפ הבא | Next follow-up | date | YYYY-MM-DD |
| סטטוס | Status | label | use live options |
| מקור פנייה | Intake source | label | allowlisted presets (Add Contact uses website; see below) |
| נקודות לשיחה | Talking points | longText | prefer reserved talkingPoints / talking_points enrichment (sandboxed plain text) |
| התנדבות | Volunteer | checkbox | boolean |
| מועד האירוע | Event date/time | datetime | ISO-8601 when writable |
| פוטנציאל | Potential | label | use live options |
| הערות | Notes | longText | free text; also fallback target if Talking points column is absent |
Intake source / מקור פנייה on Magician Prospects boards accepts the packaged
preset values whatsapp, facebook, manual, website, and other (send the
option value, or its label). Website / server Add Contact examples use
website.
talkingPoints (or talking_points) is a locale-independent reserved
enrichment key on POST …/items. Send a plain string or string[] only —
values are HTML/control-stripped and size-bounded, then mapped onto נקודות
לשיחה / Talking points (Notes fallback). Objects and executable payloads
are rejected. Do not invent board columns named talkingPoints.
Action/button columns (Call / Email / WhatsApp) and AI-assisted columns such as פתיח שיחה / Call opener are not for raw POST in the first test. Omit them, or fill only when the live schema marks them writable and you have a grounded value.
The installed board is editable. A workspace owner may rename or add columns, so this table is orientation—not an API contract. The schema endpoint is the contract.
Step 6: create a Prospects-only public API key (manual UI)
Create the key only after לידים / Leads exists, because allowlist scope needs a real board UUID.
- Open the user menu → Public API (or use App Settings → Add Contact).
- Under Create a scoped key, select the suite's workspace.
- Enter a name such as
Add Contact — Prospects. - Set Expires (days) to the shortest practical rotation period.
- Under Permissions, select at least:
- Read board schemas;
- Read board items; and
- Create full board rows.
- Add List boards only if this integration must call
GET /api/v1/boardsfor discovery. The settings panel already exposes the UUID, so many website relays skipboards.list. - Under Board access, keep Selected boards only (recommended).
- Select לידים or Leads — the Prospects board. Do not select לקוחות / Companies for Magician Add Contact.
- Do not add הצעות מחיר unless the same key truly needs quote-board access.
- Review per-minute and monthly limits → Create key.
- Copy the one-time secret into the backend secret manager immediately.
Do not choose All current and future boards merely to skip discovery.
The older API Keys page is for browser-extension credentials. It cannot be used here.
Step 7: configure the trusted sender
DOTALLIO_ORIGIN=https://your-dotallio.example
DOTALLIO_API_KEY=dot_live_the_one_time_secret
DOTALLIO_PROSPECTS_BOARD_ID=the-real-prospects-board-uuid
Prefer copying DOTALLIO_PROSPECTS_BOARD_ID from the Add Contact panel. For
development, use the deployment's dot_test_… key. A live deployment rejects a
test prefix, and a test deployment rejects a live prefix.
Never put DOTALLIO_API_KEY in NEXT_PUBLIC_*, browser JavaScript, public form
definitions, source control, AI prompts, analytics, request logs, screenshots,
or support tickets.
Step 8: discover the Prospects board ID (if not using the panel)
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards"
Find the entry whose name is לידים or Leads and whose appName is the
installed magician suite. A representative Hebrew response is:
{
"ok": true,
"boards": [
{
"id": "4bff7e69-1111-4444-8888-703a8f987654",
"name": "לידים",
"workspaceId": "workspace-uuid",
"workspaceName": "Magic Studio",
"appId": "app-uuid",
"appName": "Hebrew Magician/Mentalist Quote Suite",
"canInsert": true,
"schemaUrl": "/api/v1/boards/4bff7e69-1111-4444-8888-703a8f987654/schema"
}
]
}
Save the returned id as DOTALLIO_PROSPECTS_BOARD_ID. The UUID above is
illustrative.
If Prospects is absent, check workspace, allowlist, app membership, active app,
and that the key has boards.list when using this discovery call. If
canInsert is false, the represented user also needs owner/admin/editor
access to that app.
Step 9: fetch the live Prospects schema
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_PROSPECTS_BOARD_ID/schema"
Copy advertised key values for production. Exact titles are accepted and match
the settings-panel first test; stable integrations should map to schema keys.
Before sending any field, enforce:
writable === true
value conforms to format
option is one of fields[].options[].value or .label
Step 10: create a Prospects row (parity with the settings panel)
The App Settings snippet, /api/v1/openapi.json createItems examples
(magicianHebrewProspectsAddContact / magicianEnglishProspectsAddContact),
and this guide share the same title-based payloads from
public-api-add-contact.utils.ts.
Use a key allowlisted to Prospects / Leads only. OpenAPI also keeps a
Companies / לקוחות example (companiesQuoteCrmContrast) for non-Magician
quote CRM — do not use that payload for Magician Add Contact.
Hebrew Magician (service-provider-quote-suite-he-magician-mentalist):
curl -H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_PROSPECTS_BOARD_ID/schema"
curl -X POST \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
-H "Content-Type: application/json" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_PROSPECTS_BOARD_ID/items" \
-d '{
"items": [
{
"שם מלא": "דנה כהן",
"טלפון": "+972501234567",
"אימייל": "dana@example.com",
"מקצוע": "מפיקת אירועים",
"מקור פנייה": "website",
"talkingPoints": [
"נפגשנו באירוע חברה — התלהבה מהקסם עם הקלפים",
"מפיקה אירועים ארגוניים; מחפשת מופע של ~90 דק׳"
]
}
]
}'
English Magician (service-provider-quote-suite-en-magician-mentalist):
curl -X POST \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
-H "Content-Type: application/json" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_PROSPECTS_BOARD_ID/items" \
-d '{
"items": [
{
"Full name": "Dana Cohen",
"Phone": "+15551234567",
"Email": "dana@example.com",
"Profession": "Event producer",
"Intake source": "website",
"talkingPoints": [
"Met at a corporate offsite — loved the card bit",
"Produces company events; asking about a ~90 min show"
]
}
]
}'
Title-address form is fine for a first manual test. For production, replace
board titles with the live schema key values. Keep the reserved
talkingPoints / talking_points key as documented (it is not a board title).
Step 11: send a fuller Prospects row
{
"items": [
{
"שם מלא": "דנה כהן",
"טלפון": "+972501234567",
"אימייל": "dana@example.com",
"מקצוע": "מפיקת אירועים",
"חברה": "Orion Events",
"מקור פנייה": "website",
"הרגע הקסום": "Chose the three of hearts during the table hop",
"פולו אפ הבא": "2026-10-05",
"talkingPoints": [
"Website contact form — birthday lead",
"Chose the three of hearts during the table hop"
],
"הערות": "Optional free-text notes separate from sandboxed talking points"
}
]
}
For English installs, use the EN titles from Step 5 (Full name, Phone,
Email, Profession, Company, Intake source, The magic moment,
Next follow-up, Talking points via talkingPoints, Notes, …). Label
fields must use a live option value or label (Intake source →
whatsapp|facebook|manual|website|other).
Step 12: PowerShell (UTF-8 Hebrew titles)
$headers = @{
Authorization = "Bearer $env:DOTALLIO_API_KEY"
"Content-Type" = "application/json"
}
$uri = "$env:DOTALLIO_ORIGIN/api/v1/boards/$env:DOTALLIO_PROSPECTS_BOARD_ID/items"
$body = Get-Content -LiteralPath ".\prospect-lead.json" -Raw -Encoding utf8
Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $body
Use one credential header only. Do not add X-API-Key when Authorization is
already present.
Step 13: understand the success response
{
"ok": true,
"createdIds": ["new-item-uuid"],
"boardId": "the-real-prospects-board-uuid",
"warnings": []
}
Treat non-empty warnings as integration defects. Unknown fields are ignored with warnings, so HTTP 200 alone does not prove every requested cell was stored.
Each validated row costs one weighted workspace operation.
Step 14: read the lead back
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_PROSPECTS_BOARD_ID/items?limit=20&offset=0"
Verify:
- the returned item ID matches
createdIds; - name, phone, email, and profession are correct;
- warnings were empty; and
- the row is visible in the לידים / Leads board UI — not only in Companies.
Step 15: batch Prospects rows
One request may contain 1–100 rows. Validation is batch-atomic: one invalid row
yields 422 and inserts none of the validated rows. A later 503 can be
partially applied — reconcile before retry.
Step 16: prevent duplicate leads
The generic public create endpoint does not promise Magician-specific upsert.
Before importing:
- normalize phones to one international representation;
- read existing Prospects rows;
- index non-empty normalized phones (then email);
- send only new records; and
- optionally add an immutable
source_lead_idcolumn, refetch schema, and deduplicate on that value.
Do not use display name alone as the stable identity.
Step 17: safe agent operating instruction
Keep the key in the runtime, then give the agent a contract like this:
You are adding leads to the installed magician suite's Prospects board
(לידים / Leads). Never write Magician website contacts to Companies/לקוחות.
1. Prefer the board UUID from App Settings Add Contact (PROSPECTS_BOARD_ID).
If discovering via API, GET /api/v1/boards and select name "לידים" or
"Leads" in the magician-suite app. Never guess its UUID.
2. Call that board's schema endpoint before writing.
3. Use fields[].key as the production field address. Respect writable, format,
and options exactly.
4. Deduplicate primarily by normalized phone, then email or external source ID.
5. Fill grounded lead fields (name, phone, email, profession, notes, …).
6. Omit action/button columns and uncertain AI cells unless schema-writable and
grounded.
7. Send no more than 100 rows per request; inspect warnings and createdIds.
8. A direct Prospects POST creates lead rows only. It does not create a quote
or run template intake.
9. Never print, log, return, or place the Dotallio key in model context.
Step 18: form vs API
Use the native quote intake form when the desired result includes quote defaults, event capture, Companies match/create, quote artifacts, or intake workflow side effects.
Use Prospects Add Contact / public API when the desired result is:
- website contact → לידים / Leads;
- synchronizing researched prospects as leads;
- enriching Prospects columns the external agent already knows; or
- creating lead rows for later human follow-up.
Step 19: Magician Prospects troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
לידים / Leads missing from /boards | wrong workspace, allowlist, or app membership | Reopen Public API / Add Contact panel; verify workspace and selected boards |
Key allowlisted to לקוחות | Magician Add Contact pointed at Companies | Retire that key; mint Prospects-scoped key (PROSPECTS_BOARD_ID) |
canInsert is false | user is a viewer | Grant editor/admin/owner or create the key for an authorized owner |
403 INSUFFICIENT_SCOPE | missing endpoint permission | Add only required permissions (board.schema.read, board.items.*) |
404 BOARD_NOT_FOUND | wrong UUID or board outside policy | Copy UUID from Add Contact panel or rediscover with /boards |
422 on a label field | invalid option | Use a value/label from the live field options |
| Row created but a field is absent | unknown field ignored | Inspect warnings, fetch schema, use advertised key |
| Lead missing from Companies | expected — Add Contact writes Prospects | Open לידים / Leads; Companies is for quote CRM |
| Duplicate lead | create-only endpoint | Reconcile by phone/email/source ID before POST |
429 after a batch | minute or shared monthly limit | Honor rate-limit headers; each row is one weighted operation |
Step 20: production handoff checklist
- Correct Magician HE or EN template is installed once (app-templates, not deprecated
magician-apps/). - Setup answers contain the real stage/business name and approved branding.
- Prospects board (
לידים/Leads, tokenPROSPECTS_BOARD_ID) is present. - Key is on Public API (or minted from Add Contact panel), not the extension-key page.
- Key is scoped to the Prospects board only — not Companies /
לקוחות. - Permissions include schema read + items read + items write (and
boards.listonly if needed). - One-time secret is in a backend secret manager.
- Sender uses the Prospects UUID and fetches schema at job start.
- Curl/snippet field titles match the locale (HE: שם מלא/טלפון/אימייל/מקצוע/מקור פנייה; EN: Full name/Phone/Email/Profession/Intake source).
- Example includes
Intake source/מקור פנייה=website(or another allowlisted preset) and optionaltalkingPointsplain text / string[]. - Production mappings use advertised field keys (except reserved
talkingPoints/talking_points). - Phone/email/source-ID deduplication runs before create.
- Warnings and created IDs are recorded without logging the credential.
- Usage, expiry, audit, rotation, and revocation have named owners.
Endpoint reference
GET /api/v1/me
Required permission: profile.read
Weighted cost: 1
Use it to:
- verify a newly created secret;
- identify the represented user;
- confirm the key's workspace;
- detect a removed user or unavailable workspace.
GET /api/v1/boards
Required permission: boards.list
Weighted cost: 1
Returns only boards that satisfy all of these conditions:
- inside the key's workspace;
- included by the key's board policy;
- visible to the represented user;
- inside an active, non-deleted app and workspace;
- not hidden from the represented role.
canInsert tells the caller whether the current app role can write.
GET /api/v1/boards/{boardId}/schema
Required permission: board.schema.read
Weighted cost: 1
Returns:
- board ID and name;
- every active column;
- preferred field key;
- immutable column ID;
- title and type;
- whether the field is writable;
- primary-field marker;
- option values and labels;
- accepted format;
- read-only reason;
- a ready-to-send example.
GET /api/v1/boards/{boardId}/items
Required permission: board.items.read
Weighted cost: 1
Query parameters:
| Parameter | Default | Range | Behavior |
|---|---|---|---|
limit | 50 | 1–200 | Out-of-range values are clamped |
offset | 0 | 0 or greater | Invalid/negative values become 0 |
Rows are returned newest first. Empty fields are omitted.
POST /api/v1/boards/{boardId}/items
Required permission: board.items.write
Weighted cost: validated row count
Limits:
- 1 through 100 rows per request;
- 1 MB maximum request body;
- JSON only;
- one object per row.
Accepts either one row object or a batch envelope.
Machine-readable examples in /api/v1/openapi.json lead with Magician Add
Contact → Prospects (HE then EN), keep a Companies / לקוחות quote-CRM contrast
example, and document optional reserved talkingPoints / talking_points
(plain string or string[], sandboxed onto Talking points / Notes). Magician
Prospects boards also accept Intake source / מקור פנייה with preset
values whatsapp|facebook|manual|website|other.
When Idempotency-Key is a UUID, create also returns boardId, version,
updatedAt, and etag for every created item. An identical retry returns the
stored response without another row, activity, workflow, or realtime event.
Reusing the key with a different board or canonical payload returns
409 idempotency_key_reused. Requests without the header retain the legacy
response contract.
GET /api/v1/boards/{boardId}/items/{itemId}
Required permission: board.items.read
Returns one item with its current numeric version, exact-title cells, and a
strong ETag such as "item:<itemId>:v7" in both the item and response header.
PATCH /api/v1/boards/{boardId}/items/{itemId}
Required permission: board.items.write
Weighted cost: 1
Every request requires a UUID Idempotency-Key and the exact current ETag in
If-Match. Send exactly one non-empty cells object with 1–50 exact,
case-sensitive column titles:
{
"cells": {
"Small Talk Details": "Preferred reply: email",
"Email": "buyer@example.com"
}
}
Omitted cells remain unchanged. null explicitly clears a nullable cell;
empty string retains that column's normal value semantics. Unknown, duplicate,
read-only, formula, system, computed, or invalid columns fail the whole request
with 422 validation_failed and bounded repair issues. A stale ETag changes
nothing and returns 412 version_conflict with only currentVersion and
currentEtag. A successful non-no-op increments the version exactly once.
An identical retry returns the stored body and ETag with replayed: true and
does not create a second activity or downstream dispatch. Use a fresh UUID for
each new intended update.
Understand board schemas
Never design an agent around a screenshot of a board. Read the schema endpoint at the start of a job and refresh it when a validation response indicates that the board changed.
Example field descriptor:
{
"key": "status",
"id": "column-uuid",
"title": "Status",
"type": "status",
"writable": true,
"options": [
{ "value": "researching", "label": "Researching" },
{ "value": "ready", "label": "Ready" }
],
"format": "one of the listed options (label or value, case-insensitive)"
}
Example read-only formula descriptor:
{
"key": "display_score",
"id": "column-uuid",
"title": "Display Score",
"type": "number",
"writable": false,
"reason": "computed by a formula"
}
Schema-driven agent rule
For each field:
If writable is false:
never send it
If writable is true and the agent has grounded information:
send the normalized value
If writable is true, AI-powered, and the agent lacks grounded information:
omit it or send an empty value
Field names and addressing
A payload may address a column by:
- the advertised schema
key— preferred; - the immutable column UUID — always unambiguous;
- the column's exact title, case-insensitive.
Preferred keys
When a column has no explicit key, Dotallio derives one from its title:
"Release Year" → "release_year"
"IMDb.URL" → "imdb_url"
If multiple columns derive the same key, later keys receive suffixes such as
_2 and _3. Use exactly what the schema endpoint advertises.
Duplicate addresses in one row
Do not address the same column twice:
{
"title": "Arrival",
"Title": "Duplicate address for the same column"
}
The API rejects ambiguous or duplicate addressing with a repair hint.
Unknown fields
Unknown fields are ignored and returned as warnings. This allows integrations to evolve without turning one extra source field into a failed import.
However, if no field in a row matches a board column, the row fails validation.
Treat warnings as actionable telemetry. They often indicate:
- a renamed column;
- a stale schema cache;
- a typo;
- a source-field mapping that no longer belongs in the payload.
Supported value formats
Text family
Column types include:
text, longText, email, phone, url, link, markdown,
code, richText, color, location
Accepted values:
- strings;
- numbers and booleans, converted to strings.
Objects and arrays are rejected for text fields.
{
"title": "Arrival",
"imdb_url": "https://www.imdb.com/title/tt2543164/",
"notes": "First-contact science fiction"
}
Number family
Column types include:
number, rating, percentage, progress, slider, duration
Accepted values:
- finite JSON numbers;
- numeric strings.
{
"release_year": 2016,
"rating": "8.0"
}
NaN, infinity, and non-numeric strings are rejected.
Boolean family
Column types:
checkbox, toggle
Accepted true values:
true, "true", "yes", 1, "1"
Accepted false values:
false, "false", "no", 0, "0"
Date family
Column types:
date, datetime
Send ISO 8601 strings:
{
"release_date": "2016-11-11",
"researched_at": "2026-07-16T12:30:00Z"
}
Ambiguous locale values such as 07/06/2026 are rejected.
Single-option family
Column types include:
select, label, radio, status, priority
Send one option value or label. Matching is case-insensitive, and Dotallio stores the stable option value.
{
"status": "ready"
}
Always prefer the schema's value over the display label.
Multi-option family
Column types:
multiSelect, multiLabel
Send an array of option values or labels:
{
"genres": ["science-fiction", "drama"]
}
A bare string is accepted as a one-element array, but explicit arrays are clearer and more portable.
Conditional options
If a child option list depends on a parent field, send the parent and child in the same row. The child value must be allowed by the supplied parent value.
JSON family
Send an object or array:
{
"source_metadata": {
"source": "official-site",
"confidence": 0.94,
"retrievedAt": "2026-07-16T12:30:00Z"
}
}
Strings are also accepted for JSON-backed fields, but structured JSON is recommended when the source is structured.
Formula and unsupported fields
Formula columns are always read-only. Other column types that the API cannot
write are returned with writable: false and a reason.
Never guess how to serialize a read-only field. Remove it from the payload.
AI-powered columns
AI-powered does not mean API-read-only.
If the agent already has a grounded value, it may write that value directly. The autocomplete trigger does not need to repeat work for an already populated cell.
Fill the cell directly
Use this when the information is already known or was deliberately researched:
{
"title": "Arrival",
"imdb_url": "https://www.imdb.com/title/tt2543164/",
"summary": "A linguist is recruited to communicate with extraterrestrial visitors."
}
Leave the cell for autocomplete
Use omission when the agent does not know the value:
{
"title": "Arrival"
}
The following also remain effectively unset at insertion:
{
"title": "Arrival",
"imdb_url": null,
"summary": ""
}
Decision policy for agents
Grounded and current?
Yes → fill the AI-powered cell.
No → leave it empty and allow configured autocomplete.
Formula field?
Never send it.
Uncertain but plausible?
Do not fabricate. Leave it empty or research it first.
Links and images
When a board asks for links or images:
- use canonical or primary sources where possible;
- verify the URL resolves;
- avoid search-result redirect URLs;
- do not invent an image URL;
- respect licensing and source terms;
- include source metadata when the board has appropriate columns;
- leave the field empty when the agent cannot establish a reliable value.
Create complete rows
Single-row body
{
"title": "Arrival",
"release_year": 2016,
"genres": ["science-fiction", "drama"],
"watched": true,
"rating": 8.0,
"imdb_url": "https://www.imdb.com/title/tt2543164/"
}
Batch body
{
"items": [
{
"title": "Arrival",
"release_year": 2016,
"watched": true
},
{
"title": "The Prestige",
"release_year": 2006,
"watched": false
}
]
}
A board field whose key is items
At the top level, items is reserved for the batch envelope. If the board
itself has a writable field whose advertised key is items, send the row
inside a batch envelope so the two meanings are unambiguous:
{
"items": [
{
"title": "Inventory record",
"items": "The value for the board column named items"
}
]
}
Do not send a single top-level row with a scalar items property; it will be
interpreted as a malformed batch envelope.
Successful response
{
"ok": true,
"created": [
{
"id": "created-item-uuid",
"url": "https://your-dotallio.example/app/board/board-uuid"
}
],
"warnings": []
}
Validation atomicity
The entire request is validated before insertion begins. If validation returns
422, no row from that request was inserted.
Runtime partial failure
Rows are inserted in payload order. A database or downstream failure after
insertion begins can leave earlier rows created and later rows absent. A 500
or 503 message tells the caller when preceding rows may already exist.
Before retrying a runtime failure:
- read the board;
- reconcile by a stable source identifier or normalized title;
- remove rows that already exist from the retry payload;
- retry only the missing rows.
Batch ingestion
One request accepts at most 100 rows. Split larger jobs into bounded batches.
Recommended batch sizes:
| Job type | Suggested batch size |
|---|---|
| Simple text/number rows | 50–100 |
| Rows with many fields | 25–50 |
| Research-heavy agent output | 10–25 after research completes |
| Reconciliation-sensitive imports | 10–25 |
The API itself accepts 100; smaller batches reduce the reconciliation surface if a runtime failure occurs.
JavaScript batch helper
function chunk(values, size) {
const chunks = []
for (let index = 0; index < values.length; index += size) {
chunks.push(values.slice(index, index + size))
}
return chunks
}
async function insertRows({ origin, key, boardId, rows }) {
const results = []
for (const batch of chunk(rows, 50)) {
const response = await fetch(
`${origin}/api/v1/boards/${boardId}/items`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ items: batch }),
},
)
const payload = await response.json()
if (!response.ok) {
throw new Error(JSON.stringify(payload))
}
results.push(payload)
}
return results
}
For production, add the status-aware retry and reconciliation rules described later in this guide.
Convert CSV to JSON
The HTTP API does not accept text/csv. Parse the file in your trusted service
or agent, map headers to schema field keys, normalize types, then POST JSON.
Example CSV:
Title,Release Year,Watched,Genres,IMDb URL
Arrival,2016,true,"science-fiction|drama",https://www.imdb.com/title/tt2543164/
The Prestige,2006,false,"drama|mystery",https://www.imdb.com/title/tt0482571/
JavaScript conversion
import { parse } from 'csv-parse/sync'
const records = parse(csvText, {
columns: true,
skip_empty_lines: true,
trim: true,
})
const rows = records.map((record) => ({
title: record['Title'],
release_year: Number(record['Release Year']),
watched: record['Watched'].toLowerCase() === 'true',
genres: record['Genres'].split('|').filter(Boolean),
imdb_url: record['IMDb URL'] || undefined,
}))
Python conversion
import csv
import io
reader = csv.DictReader(io.StringIO(csv_text))
rows = []
for record in reader:
rows.append({
"title": record["Title"],
"release_year": int(record["Release Year"]),
"watched": record["Watched"].lower() == "true",
"genres": [value for value in record["Genres"].split("|") if value],
"imdb_url": record["IMDb URL"] or None,
})
CSV mapping procedure
- Fetch the current board schema.
- Build a mapping from source header to schema key.
- Reject or quarantine required source columns that are absent.
- Normalize each source value using the schema's
type,format, andoptions. - Omit source columns with no intended board mapping.
- Deduplicate rows.
- Split into batches.
- POST JSON.
- Persist created IDs or source-to-Dotallio mapping in your own integration.
Do not map by column position. Board columns can be reordered without changing their IDs or advertised keys.
Build a board-aware AI agent
A reliable agent separates reasoning/research from deterministic API enforcement.
Agent workflow
1. Authenticate and resolve workspace identity.
2. List boards allowed by the key.
3. Select the intended board by ID, not a fuzzy title alone.
4. Read the schema.
5. Read current rows.
6. Infer preferences and exclusions from current data.
7. Plan additions and stable deduplication keys.
8. Research current facts, links, and images with bounded workers.
9. Normalize research output to exact schema keys and value formats.
10. Fill known AI-powered cells; omit unknown AI-powered cells.
11. Validate locally against schema metadata.
12. Send bounded batches.
13. Record created IDs and warnings.
14. Re-read/reconcile after any uncertain runtime failure.
15. Produce a user-facing completion and partial-failure report.
Suggested agent instruction
You are writing structured rows to a Dotallio board.
Before proposing or writing rows:
- list allowed boards;
- read the target board schema;
- read existing rows;
- use the schema's field keys and option values;
- never write fields where writable is false;
- deduplicate against existing data;
- research current facts and canonical URLs when needed;
- fill an AI-powered cell only when its value is grounded;
- omit unknown AI-powered cells so Dotallio autocomplete may fill them;
- never invent an image or source URL;
- write no more than 100 rows per request;
- after an uncertain write failure, reconcile before retrying.
Schema cache policy
Schema data can be cached for one job, but refresh it when:
- a validation error includes
expectedSchema; - the user says the board changed;
- an option no longer validates;
- a field becomes read-only;
- a previously valid key becomes unknown;
- a long-running job crosses an administrative edit window.
Live authorization changes
Research may take minutes. The user's role can change between research and
write. Treat a final 403 as authoritative. Do not attempt to work around it
with another board or credential.
Movie-board example
Suppose the board schema exposes:
title text, primary, writable
release_year number, writable
genres multiSelect, writable
director text, AI-powered, writable
imdb_url url, AI-powered, writable
poster_url url, AI-powered, writable
personal_note longText, writable
display_label formula, read-only
If the agent has researched the data, send it:
{
"items": [
{
"title": "Arrival",
"release_year": 2016,
"genres": ["science-fiction", "drama"],
"director": "Denis Villeneuve",
"imdb_url": "https://www.imdb.com/title/tt2543164/",
"poster_url": "https://trusted-image-host.example/arrival.jpg",
"personal_note": "Strong fit for cerebral first-contact stories."
}
]
}
Do not send display_label; it is a formula.
If the agent knows only title and year:
{
"title": "Arrival",
"release_year": 2016
}
The configured AI columns remain empty and may autocomplete.
Movie deduplication
Title alone is often insufficient. Normalize and compare a composite:
lower(trim(title)) + "|" + release_year
When an IMDb URL exists, its title identifier is a stronger external key.
Do not send a duplicate simply because punctuation, casing, or localized title differs.
Research-and-expand example
Goal: a board contains 25 preferred visual illusions, and an agent should add 75 more with complete details, sources, and images where available.
Step 1: inspect existing preferences
Read enough current rows to understand:
- repeated categories;
- complexity level;
- whether the collection favors physical, optical, interactive, or digital illusions;
- excluded or overrepresented topics;
- title style;
- link and image expectations;
- how options are encoded.
Step 2: generate candidates
Generate more than 75 candidates, then remove:
- exact duplicates;
- normalized-title duplicates;
- near-identical concepts with a different name;
- items outside observed preferences;
- candidates without enough grounded information for required fields.
Step 3: research bounded groups
Use bounded research concurrency. Each worker should return structured data, not prose:
{
"title": "Ames Room",
"category": "forced-perspective",
"description": "A distorted room that appears rectangular from one viewpoint.",
"source_url": "https://reliable-source.example/ames-room",
"image_url": "https://reliable-image-source.example/ames-room.jpg",
"confidence": 0.97
}
Step 4: map to schema
Use only current schema keys and allowed option values. Drop internal research
properties such as confidence unless the board has a matching field.
Step 5: insert in batches
Three batches of 25 are easier to reconcile than one batch of 75, while all remain below the 100-row limit.
Step 6: report completion
The agent's final report should include:
- requested count;
- researched count;
- duplicate count removed;
- successfully created count;
- batches attempted;
- warnings;
- rows that still need attention;
- whether any AI-powered fields were intentionally left empty.
Read and paginate items
Example:
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$BOARD_ID/items?limit=100&offset=0"
Response:
{
"ok": true,
"items": [
{
"id": "item-uuid",
"createdAt": "2026-07-16T12:30:00Z",
"fields": {
"title": "Arrival",
"release_year": 2016,
"watched": true,
"genres": ["science-fiction", "drama"]
}
}
],
"page": {
"limit": 100,
"offset": 0,
"returned": 1,
"hasMore": false
}
}
Pagination loop:
async function readAllItems({ origin, key, boardId }) {
const all = []
let offset = 0
while (true) {
const response = await fetch(
`${origin}/api/v1/boards/${boardId}/items?limit=200&offset=${offset}`,
{ headers: { Authorization: `Bearer ${key}` } },
)
if (!response.ok) {
throw new Error(`Read failed with ${response.status}`)
}
const page = await response.json()
all.push(...page.items)
if (!page.page.hasMore) break
offset += page.page.returned
}
return all
}
The loop terminates on hasMore: false; it does not guess from a full page.
Deduplication and retry safety
The create endpoint accepts an optional UUID Idempotency-Key; exact-item
PATCH requires one. Generate one UUID for an atomic operation and reuse that
same value only when retrying the identical request. Reordered JSON object keys
are equivalent. A changed board, item, value, or If-Match with the same key
returns 409 idempotency_key_reused. Receipts are retained for at least 24
hours. If no key was supplied to legacy create, read the board and reconcile an
uncertain outcome before submitting another create request.
Stable source identifier
When possible, add a board column such as:
source_id
external_id
canonical_url
catalog_key
Populate it from the external system and deduplicate before writing.
Title normalization
For idea/catalog boards, normalize titles by:
- Unicode normalization;
- trimming;
- case folding;
- collapsing whitespace;
- optionally removing punctuation that is not semantically meaningful.
Use semantic judgment for near-duplicates. A regex alone cannot determine whether two differently named ideas are the same concept.
Retry categories
| Outcome | Retry? | Action |
|---|---|---|
400 malformed request | No automatic retry | Fix request construction |
401 invalid/expired | No automatic retry | Replace or rotate credential |
403 permission/tier | No automatic retry | Correct policy, role, or plan |
404 board | No automatic retry | Refresh board list and policy |
409 idempotency mismatch | No automatic retry | Generate a new key only for a genuinely new write |
412 item version conflict | Reconcile first | GET the item; do not overwrite team-entered values |
413 size/count | Retry after change | Split payload |
422 validation | Retry after change | Apply issues and resend full batch |
429 rate limit | Yes | Wait for Retry-After, then retry |
503 unavailable | Yes, bounded | Wait for Retry-After; use backoff and jitter |
| uncertain connection loss during POST | Reconcile first | Read board, remove created rows, retry missing rows |
Bounded retry example
async function requestWithRetry(url, init, maxAttempts = 4) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const response = await fetch(url, init)
if (response.status !== 429 && response.status !== 503) {
return response
}
if (attempt === maxAttempts) return response
const retryAfter = Number(response.headers.get('retry-after') ?? '1')
const jitter = Math.floor(Math.random() * 250)
await new Promise((resolve) =>
setTimeout(resolve, retryAfter * 1000 + jitter),
)
}
throw new Error('unreachable')
}
Never retry forever.
Responses and rate-limit headers
Every authenticated response includes:
| Header | Meaning |
|---|---|
X-Request-Id | Dotallio request correlation ID |
Cache-Control: private, no-store | Do not cache credential-bound content |
Vary: Authorization, X-API-Key | Credential-sensitive response variance |
RateLimit-Limit | Limit for the reported window |
RateLimit-Remaining | Remaining operations in the reported window |
RateLimit-Reset | Unix time in seconds when that window resets |
Rate denials also include:
Retry-After: 12
Retryable 503 responses include a short Retry-After hint.
On a 429, the rate-limit headers may describe either:
- the per-key one-minute window; or
- the shared workspace monthly window.
Use the error message and reset time to distinguish them.
Store X-Request-Id with your own integration log entry. Do not log the
Authorization header.
Error reference
All non-success responses use this envelope:
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable explanation"
}
}
Error codes
| Code | Typical status | Meaning | Recovery |
|---|---|---|---|
INVALID_JSON | 400 | Body is not valid JSON | Fix serialization and content type |
INVALID_REQUEST | 400 | Request shape or parameter is invalid | Follow endpoint contract |
MISSING_API_KEY | 401 | No usable credential was sent | Send exactly one credential header |
INVALID_API_KEY | 401 | Key is invalid, disabled, retired, or revoked | Create/rotate a key |
EXPIRED_API_KEY | 401 | Key expiry passed | Rotate or replace it |
AMBIGUOUS_API_KEY | 400 | Both credential headers were sent | Keep only one header |
INSUFFICIENT_SCOPE | 403 | Endpoint permission is missing | Update key permissions or use another key |
FORBIDDEN | 403 | Current product role/state denies access | Restore legitimate access; do not bypass |
API_UNAVAILABLE_FOR_TIER | 403 | Workspace plan does not include API access | Upgrade or change plan |
BOARD_NOT_FOUND | 404 | Board is absent or intentionally hidden | Refresh allowed boards and check policy |
TOO_MANY_ITEMS | 413 | Batch exceeds 100 rows | Split it |
PAYLOAD_TOO_LARGE | 413 | Body exceeds 1 MB | Split or reduce fields |
VALIDATION_ERROR | 422 | One or more rows failed schema validation | Apply issues and resend |
RATE_LIMITED | 429 | Per-key or monthly budget exhausted | Honor headers and retry when allowed |
API_NOT_PROVISIONED | 503 | Deployment schema is not ready | Maintainer runs schema rollout |
PROVIDER_UNAVAILABLE | 503 | Unkey/metering is unavailable | Bounded retry |
INTERNAL_ERROR | 503 | Board data operation could not complete | Reconcile uncertain writes, then retry |
Validation repair response
A 422 may include:
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "1 of 2 item(s) failed validation. Nothing was inserted.",
"issues": [
{
"itemIndex": 1,
"field": "status",
"received": "Finished",
"expected": "one of: researching, ready",
"hint": "Send an option value or its label (case-insensitive)."
}
],
"expectedSchema": [],
"example": {
"items": [{ "title": "Jane Doe" }]
},
"schemaUrl": "/api/v1/boards/board-uuid/schema"
}
}
itemIndex is zero-based within the submitted items array.
Do not expose provider details to callers
Your integration should show users a safe recovery message. Keep raw network or SDK errors in protected operational logs, and ensure those logs never contain the key.
Key lifecycle
Policy updates
The key owner may change:
- permissions;
- selected/all-board mode;
- selected board IDs.
Workspace and expiry are immutable after creation. Create a replacement if those need to change.
When broadening policy, review both permission scope and board scope. Granting write plus all-board access is substantially more powerful than either change alone.
Rotation
Rotation uses Unkey reroll with zero overlap:
old key expires immediately
new key is returned once
Only the key owner may rotate it. Workspace admins may revoke another member's key but do not rotate it and assume that member's identity.
Safe rotation runbook:
- Open Account → Public API.
- Select Rotate.
- Confirm immediate invalidation.
- Copy the new secret.
- Update the secret manager.
- restart/redeploy only the consuming service if needed;
- verify with
/me; - verify one read operation;
- verify one low-risk write when the integration writes;
- confirm the old secret returns
401.
Because overlap is zero, coordinate the consuming service before clicking Rotate.
Revocation
Revocation disables/deletes the provider key first, then hard-deletes local key metadata and board scopes. Usage and immutable audit snapshots remain.
Repeated revocation is idempotent: the desired state is still "no active key."
Expiry
Expiry is selected at creation from 1 through 365 days. Treat expiry as a scheduled security event:
- alert before expiry in your own operations process;
- rotate during a maintenance window;
- do not wait for production
401errors; - remove the old secret from every secret store after rotation.
Legacy local keys
Keys without an Unkey key ID are retired and do not authenticate. Replace and revoke them. There is no local-hash compatibility fallback.
Usage and audit
The account page exposes:
- bounded daily request aggregates;
- weighted operation totals;
- key creation events;
- policy updates;
- rotations;
- revocations;
- immutable key-name and prefix snapshots;
- actor IDs and timestamps.
Use audit history to answer who changed a key and when. It does not recover the secret.
Security practices
Do
- create one key per integration and environment;
- use selected-board mode by default;
- use read-only permissions unless writing is required;
- choose the shortest practical expiry;
- store secrets in a managed secret store;
- inject secrets at runtime;
- redact Authorization and
X-API-Keyheaders; - record request IDs, status codes, endpoint, and timing;
- rotate after personnel or vendor changes;
- revoke unused keys immediately;
- reconcile privileges regularly;
- keep production and test deployments separate.
Do not
- commit keys to Git;
- paste keys into chat or issue trackers;
- send keys to an AI model as prompt content;
- store keys in board cells or documents;
- include keys in URLs;
- expose keys in frontend code;
- reuse one key across unrelated integrations;
- share public API and extension keyspaces;
- implement a local verification fallback;
- continue unmetered when Unkey is unavailable;
- log complete provider error objects when they may contain request data.
Secret-manager examples
Use platform-supported encrypted environment variables or a dedicated secret manager. The application should receive only the secret value it needs at runtime.
DOTALLIO_API_KEY → secret value
DOTALLIO_ORIGIN → non-secret deployment URL
DOTALLIO_BOARD_ID → non-secret board UUID
Key exposure boundaries
Prefixes and provider key IDs are safe identifiers. The full secret is not.
Safe to log:
local key ID
provider key ID
display prefix
request ID
operation
workspace ID, when operational policy permits
status code
weighted cost
Never log:
Authorization header
X-API-Key header
full request URL if it incorrectly contains a secret
full provider request object
environment dump
one-time key response
Incident response
Suspected key exposure
- Revoke the key immediately.
- Do not wait to confirm exploitation.
- Identify every system that stored or used it.
- Review usage and lifecycle audit history.
- Review application logs by safe prefix, local key ID, and request ID.
- Confirm board scope and write permission exposure.
- Create a replacement with narrower policy if possible.
- update the consuming secret store;
- remove the exposed value from logs, tickets, and build artifacts where feasible;
- verify the revoked value returns
401.
Unexpected high usage
- Identify whether the denial is per-key minute or shared monthly budget.
- Review recent deploys for retry loops.
- Check whether a 429 handler is retrying without waiting.
- Check whether pagination restarts from offset zero.
- Check whether failed batches are being resent without reconciliation.
- Revoke the key if usage appears malicious.
- Narrow permissions and board scope on replacement keys.
Provider outage
The API fails closed with 503.
The client should:
- stop writes after a bounded number of retries;
- preserve the source job state outside Dotallio;
- show an actionable retry-later message;
- avoid switching to another secret automatically;
- reconcile any POST whose connection outcome is uncertain.
Deployment and Unkey setup
This section is for Dotallio deployment maintainers.
Database rollout
Apply the Drizzle schema using the repository's maintained workflow:
npm run db:push
The schema includes:
api_keys— provider/local authorization mirror;api_key_board_scopes— explicit board allowlist;public_api_usage_daily— bounded UTC daily aggregates;public_api_key_audit— immutable lifecycle snapshots;public_api_limit_overrides— Enterprise platform-admin overrides.
RLS behavior:
- key owners manage their credentials;
- workspace owners/admins may view metadata, usage, and audit;
- workspace owners/admins may revoke workspace keys;
- usage and audit writes are server-only;
- board mutations still run through the represented user's RLS context.
Public API environment variables
Generate a copy-ready 256-bit idempotency HMAC configuration from the repository root:
node scripts/generate-secret-key.js --public-api-idempotency
The command prints the two PUBLIC_API_IDEMPOTENCY_HMAC_* assignments once.
Store them only in the Dotallio server environment; never commit or share the
printed secret.
UNKEY_PUBLIC_API_ROOT_KEY="..."
UNKEY_PUBLIC_API_ID="..."
UNKEY_PUBLIC_API_BURST_NAMESPACE="..."
UNKEY_PUBLIC_API_WORKSPACE_NAMESPACE="..."
PUBLIC_API_IDEMPOTENCY_HMAC_ACTIVE_VERSION="1"
PUBLIC_API_IDEMPOTENCY_HMAC_KEYS='{"1":"at-least-32-secret-bytes"}'
PUBLIC_API_ITEM_PATCH_ROLLOUT_MODE="disabled"
# allowlist mode only: comma-separated local UUIDs
PUBLIC_API_ITEM_PATCH_ROLLOUT_KEY_IDS=""
PUBLIC_API_ITEM_PATCH_ROLLOUT_BOARD_IDS=""
Never prefix these with NEXT_PUBLIC_.
The active idempotency HMAC key signs new request receipts. Keep previous key versions in the JSON map for at least the receipt-retention window so retries created before rotation can still be verified. Never log these keys or the canonical payload.
PATCH rollout fails closed. Use disabled while applying schema, then
allowlist with both a local API-key UUID and board UUID for staging, and only
set enabled after contract verification. Targets outside the rollout boundary
receive the same non-enumerating item_not_found response.
Required Unkey permissions
Provision these public permissions in the public API keyspace:
profile.read
boards.list
board.schema.read
board.items.read
board.items.write
The Dotallio operation registry maps exactly one permission to each endpoint.
Namespace design
Create two public API rate-limit namespaces:
- per-key burst namespace;
- shared workspace-month namespace.
The burst identifier is the stable local key ID.
The monthly identifier follows this shape:
workspaceId:YYYY-MM
The month is UTC.
Root-key least privilege
The public root key should be authorized only for:
- creating keys in the public API;
- verifying keys in the public API;
- updating public key permissions/metadata;
- rerolling public keys;
- deleting public keys;
- applying limits in the two public namespaces.
Do not grant cross-API or cross-namespace wildcards when narrower Unkey root-key permissions are available.
Key configuration
Public keys are created with:
- 32 random bytes;
- environment-specific prefix;
recoverable: false;- explicit expiry;
- explicit permissions;
- local key ID and authorization metadata;
- enabled state.
Provider and local persistence compensation
Creation order:
create stable local UUID
→ issue Unkey key
→ persist local metadata and audit
→ if persistence fails, delete provider key
→ return plaintext once
Revocation order:
delete/disable at Unkey
→ write immutable audit snapshot
→ hard-delete local key and cascading board scopes
Rollout validation
After schema and environment configuration:
- create a test key in a non-production deployment;
- confirm the
dot_test_prefix; - call
/me; - list selected boards;
- verify an excluded board returns
404; - verify a missing permission returns
403; - create one row with a known AI field;
- create one row with the AI field omitted;
- confirm the first remains unchanged and the second can autocomplete;
- rotate and confirm the old key fails immediately;
- revoke and confirm repeated revoke is safe;
- confirm usage and audit records remain.
Extension keys are separate
Extension-key configuration uses separate variables:
UNKEY_EXTENSION_ROOT_KEY="..."
UNKEY_EXTENSION_API_ID="..."
UNKEY_EXTENSION_BURST_NAMESPACE="..."
UNKEY_EXTENSION_WORKSPACE_NAMESPACE="..."
Public API and extension systems may share the generic lazy Unkey v2 SDK adapter, but must not share:
- root keys;
- API IDs;
- user-facing secrets;
- permission names;
- identities;
- namespaces;
- quotas;
- metadata assumptions;
- verification routes.
If an extension credential is sent to /api/v1, verification must fail.
Troubleshooting
401 INVALID_API_KEY
Check:
- correct secret value;
- no surrounding quotes or whitespace;
- correct environment prefix;
- key has not been revoked or retired;
- secret manager deployed the new version;
- request sends exactly one credential.
401 EXPIRED_API_KEY
Rotate or replace the key. Expiry is immutable.
400 AMBIGUOUS_API_KEY
Your HTTP library, gateway, or proxy sent both Authorization and
X-API-Key. Remove one.
403 INSUFFICIENT_SCOPE
Compare the endpoint to the permission table. Update the key policy only if the integration legitimately needs the permission.
403 FORBIDDEN
Check the represented user's current:
- workspace membership;
- app membership;
- app role;
- workspace/app active state;
- write eligibility.
403 API_UNAVAILABLE_FOR_TIER
The workspace is FREE or otherwise lacks API availability. The key itself may still exist, but requests remain denied.
404 BOARD_NOT_FOUND
The API intentionally avoids revealing inaccessible boards. Refresh
GET /boards and verify:
- key workspace;
- selected-board allowlist;
- board deletion/hidden state;
- current app access.
413 TOO_MANY_ITEMS
Split the job into batches of at most 100.
413 PAYLOAD_TOO_LARGE
The body exceeds 1 MB. Split the batch, remove unused source fields, or reduce large text/JSON values.
422 VALIDATION_ERROR
Read every issue. Then:
- refresh
schemaUrl; - update option values;
- remove read-only fields;
- correct field addresses;
- fix value formats;
- resend the entire corrected batch.
Nothing was inserted for a 422.
429 RATE_LIMITED
Honor Retry-After and RateLimit-Reset. Do not hammer the endpoint. If the
monthly workspace budget is exhausted, another key will share the same denial.
503 API_NOT_PROVISIONED
The deployment maintainer must apply the Drizzle schema.
503 PROVIDER_UNAVAILABLE
Unkey or metering is temporarily unavailable. Use bounded retries with jitter.
Created rows are missing fields
Check whether:
- the source sent
nullor empty values; - an unknown field produced a warning;
- the field key changed;
- the field is formula/read-only;
- the AI autocomplete is configured and completed;
- the source value failed before the request was sent.
Duplicate rows after retry
The previous POST may have partially succeeded before a runtime failure. Reconcile against source IDs, canonical URLs, or normalized title composites before resending.
Frequently asked questions
Can I recover a key after closing the dialog?
No. Public keys are non-recoverable. Rotate or create a replacement.
Can I use a public key in React?
Not in browser-delivered React code. Call Dotallio from your backend.
Can one key access multiple workspaces?
No. Workspace is immutable and fixed at creation.
Can a key access future boards?
Only when its board mode is all, and only when the represented user can
currently access those boards.
Can I create a key with an empty board allowlist?
No. Selected-board mode requires at least one board. An empty relation never means unrestricted access.
Can an admin rotate another user's key?
No. An admin may revoke it. Rotation is restricted to the owner because the key represents that user's identity.
Does a write key bypass RLS?
No. Board work executes through the represented user's authorization context, and product access is rechecked before the operation.
Does a known AI value get overwritten?
The public API inserts the supplied value. Configured autocomplete is intended for cells that remain empty.
What should I do when an AI value is uncertain?
Research it, or leave it empty. Do not fabricate a plausible value.
Can I upload CSV directly?
Not to /api/v1. Convert it to the JSON row contract first. Authenticated board
chat/upload flows may accept CSV and perform this normalization internally.
Are batch writes transactional?
Validation is atomic: a 422 inserts nothing. Runtime insertion is ordered and
can partially succeed if infrastructure fails after earlier rows were created.
Are unknown fields errors?
They are warnings and are ignored. A row with no recognized fields is an error.
Can I send column titles instead of keys?
Yes, exact titles are matched case-insensitively. Advertised keys or column IDs are safer.
Why did a valid key suddenly stop writing?
Check expiry, plan, permissions, board allowlist, workspace membership, app role, hidden/deleted state, and quota.
Why does another key also get a monthly 429?
The monthly weighted budget is shared by the workspace.
Is the OpenAPI document authenticated?
No. It is the public machine-readable API description. It contains no secret or tenant data.
Should an agent send 100 rows every time?
Not necessarily. One hundred is the maximum. Use smaller batches when reconciliation cost matters.
Production checklists
New integration checklist
- Create a dedicated key for this integration and environment.
- Choose selected-board scope unless all-board is required.
- Grant only required permissions.
- Set a deliberate expiry.
- Copy the secret once into a managed secret store.
- Verify
/me. - List allowed boards.
- Read the target schema.
- Implement schema-key mapping.
- Implement type and option normalization.
- Implement source-level deduplication.
- Split writes into bounded batches.
- Handle warnings.
- Handle
422repair details. - Honor 429 and 503 retry headers.
- Reconcile uncertain POST failures.
- Redact credential headers from logs.
- Store
X-Request-Idfor troubleshooting. - Document rotation ownership.
- Document emergency revocation.
AI-agent checklist
- Read boards before selecting a target.
- Read schema before composing output.
- Read current rows before proposing additions.
- Learn preferences without overfitting to one row.
- Deduplicate exact and semantic duplicates.
- Use bounded research workers.
- Verify links and images.
- Use schema option values.
- Never send formula/read-only fields.
- Fill grounded AI cells directly.
- Leave unknown AI cells empty.
- Never fabricate missing facts.
- Stop on live authorization loss.
- Batch at no more than 100 rows.
- Report progress and partial failures.
- Reconcile before retrying an uncertain write.
Rotation checklist
- Identify every consumer.
- Schedule zero-overlap rotation.
- Rotate as the key owner.
- Copy the new secret immediately.
- Update the secret manager.
- Reload/redeploy consumers as needed.
- Verify the new key.
- Confirm the old key fails.
- Remove old local secret copies.
- Review the audit event.
Revocation checklist
- Revoke immediately when unused or exposed.
- Confirm integration traffic stops.
- Confirm repeated revoke is harmless.
- Remove the secret from consuming systems.
- Preserve and review usage/audit evidence.
- Create a narrower replacement only when needed.
Deployment-maintainer checklist
- Apply the Drizzle schema with
npm run db:push. - Create separate Unkey public and extension APIs.
- Create separate burst and workspace namespaces.
- Provision the five public permissions.
- Create least-privilege root keys.
- Configure all public API environment variables.
- Configure all separate extension variables.
- Confirm secrets are server-only.
- Validate live/test prefixes.
- Validate FREE denial and paid-tier limits.
- Validate board allowlists and cross-workspace denial.
- Validate role downgrade behavior.
- Validate 100-row weighted accounting.
- Validate rotation with immediate predecessor failure.
- Validate idempotent revocation and audit retention.
- Replace retired local keys.
Related documentation and contracts
- Machine contract:
/api/v1/openapi.json - Boards
- Cells and columns
- Authentication
- Multi-tenancy
- External APIs
- Browser extension
- Webhooks
When this guide and the live OpenAPI document appear to disagree about an HTTP
shape, treat the deployed /api/v1/openapi.json document and actual response as
the runtime contract, then report the documentation drift.
Last updated: 2026-07-16
Credential provider: Unkey v2
Dotallio public API version: v1