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 CRM 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 a magician CRM and send it a lead | Magician-suite CRM 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 CRM walkthrough
This chapter is a complete worked example: install the current magician/mentalist business suite, answer its setup questionnaire, create a least-privilege key, and send a customer into its CRM with full cell data.
The current packaged template is:
| Property | Current value |
|---|---|
| Template name | Hebrew Magician/Mentalist Quote Suite |
| Template ID | service-provider-quote-suite-he-magician-mentalist |
| CRM board | לקוחות (COMPANIES_BOARD_ID) |
| Quote/intake board | הצעות מחיר (EVENTS_BOARD_ID) |
| Default locale | Hebrew / RTL |
| CRM AI-research column | תובנות מהרשת |
Template IDs and board tokens are installation-time identifiers. External API
calls use the real board UUID returned by GET /api/v1/boards.
Understand the two questionnaires
The suite has two different question flows:
- 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 matches or creates a CRM customer by phone.
The public API is a third input path. It creates complete board rows directly.
Posting a row to the CRM does not impersonate the quote intake questionnaire
and does not invoke its template.intake.submit artifact workflow.
App setup questionnaire
→ configures the installed suite
Quote intake questionnaire
→ creates/matches customer + creates quote workflow artifacts
POST /api/v1/boards/{customersBoardId}/items
→ creates CRM row(s) directly
Use the intake form when you want a quote and its suite workflow. Use the API when another trusted system or agent already has CRM data to import.
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 this request:
Install a Hebrew magician/mentalist business suite with quote intake, a
customer CRM, 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 Customers and Quotes boards and the
public quote-intake form.
- Send the message.
- Read the AI's proposed template/persona, language, feature groups, and branding plan. If it proposes removing something you need, reply before the install runs.
- Let the AI use the existing template search and install flow. The expected
template ID is
service-provider-quote-suite-he-magician-mentalist. - 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 records 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.
- Review What this template creates.
- Click Install.
- Wait for the app to open; do not click Install twice while it is working.
The separate Templates marketplace can also install catalog templates with Use template, but the app switcher's preview makes it easier to confirm the exact packaged boards before installing.
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 to identify Your intake form, Your CRM, and Your quotes board.
- 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. A later save changes future generated documents; it does not rerun first-install side effects.
Step 3: understand the quote intake questionnaire
The installed הצעת מחיר חדשה form currently asks:
| Field ID | Visible question | Required |
|---|---|---|
documentLanguage | עברית או אנגלית? | yes |
subjectName | שם חברה / לקוח | workflow-required |
contactName | שם איש הקשר | yes |
activityDate | תאריך האירוע | yes |
activityTime | שעת האירוע | yes |
contactPhone | טלפון | yes |
activityLocation | מיקום האירוע | yes |
participantCount | מספר משתתפים משוער | yes |
includesChildren | האם יש ילדים? | yes |
pricingPrimaryExTax | מחיר קלוז אפ | yes |
pricingSecondaryExTax | מחיר במה | yes |
pricingCombinedExTax | מחיר משולב | yes |
To test the native suite flow:
- Open the app's intake page.
- Find the form titled הצעת מחיר חדשה.
- Select Hebrew or English.
- Start typing an existing customer in שם חברה / לקוח. Existing CRM matches are suggested and can autofill contact name and phone.
- Fill the event, audience, and three pre-VAT prices.
- Click יצירת הצעה.
- Confirm the quote appears on הצעות מחיר.
- Open לקוחות and confirm the customer was matched or created by phone.
This test proves the template workflow works. The API example below is for systems that need to add CRM customers without submitting a quote.
Step 4: inspect the CRM before creating a key
Open לקוחות and confirm the current columns. The packaged CRM currently contains:
| Column | Type | Notes |
|---|---|---|
| שם חברה / לקוח | text | required primary customer/company name |
| איש קשר | text | required main contact |
| טלפון | phone | contact phone |
| אימייל | text | contact email |
| כתובת | location | JSON address/coordinates when advertised writable |
| סוג לקוח | label | company, private, or business |
| ערוץ קשר מועדף | label | WHATSAPP, PHONE, or EMAIL |
| חבילה אחרונה | text | last purchased/performed package |
| חלון ריבוק | date | recommended rebooking date |
| תובנות מהרשת | rich text + AI | online research summary |
| סיכום AI | multimodal JSON | structured text/files context |
| התנגדות אחרונה | multimodal JSON | latest objection/context |
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 5: create a CRM-only public API key
The key should be created only after לקוחות exists, because selected-board scope requires a real board ID.
- Open the user menu.
- Click Public API.
- Under Create a scoped key, select the suite's workspace.
- Enter a name such as
Magician CRM intake — production. - Set Expires (days). Use the shortest operationally practical period; 90 or 365 days are common depending on your rotation process.
- Under Permissions, select:
- List boards;
- Read board schemas;
- Read board items; and
- Create full board rows.
- Read profile is optional for this integration. Add it only if your
service calls
/api/v1/mefor a health check. - Under Board access, keep Selected boards only (recommended).
- Select לקוחות.
- Select הצעות מחיר only if this same integration genuinely needs to read or write the quote board. CRM-only ingestion does not need it.
- Review the displayed per-minute and monthly workspace limits.
- Click Create key.
- In the one-time secret panel, click Copy and put the secret directly in the backend's secret manager.
- Dismiss the panel only after the secret is stored and verified.
Do not select All current and future boards in this workspace merely to avoid board discovery. That choice grants the key's policy layer access to boards created later as well.
The account's older API Keys page is for browser-extension credentials. It is separate from Public API and its keys cannot be used here.
Step 6: configure the trusted sender
Configure these as server-side secrets:
DOTALLIO_ORIGIN=https://your-dotallio.example
DOTALLIO_API_KEY=dot_live_the_one_time_secret
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;
- a public form definition;
- source control;
- an AI system or user prompt;
- analytics, request logs, screenshots, or support tickets.
If an AI agent performs the import, its trusted server runtime should read the secret and make the HTTP request. The model should receive schema and business data, not the raw credential.
Step 7: discover the installed Customers board ID
Do not copy a UUID from template source or guess it from the board title. Every installation gets real database IDs.
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards"
Find the entry whose name is לקוחות and whose appName is the installed
magician suite. A representative 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"
}
]
}
The UUID above is illustrative. Save the returned id as
DOTALLIO_CUSTOMERS_BOARD_ID:
export DOTALLIO_CUSTOMERS_BOARD_ID="the-real-board-uuid"
If לקוחות is absent, check all of these before broadening the key:
- the key was created for the correct workspace;
- the key allowlist includes
לקוחות; - the key owner still belongs to the workspace and app;
- the app and workspace are active;
- the represented user can see the board; and
- the key has
boards.list.
If canInsert is false, changing key permissions is not enough. The
represented user also needs owner, admin, or editor access to that app.
Step 8: fetch the live CRM schema
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_CUSTOMERS_BOARD_ID/schema"
The response contains:
- the board name and ID;
fields[]in board order;- each preferred
keyand immutable columnid; - the visible
titleand storagetype; writable,primary,format, and read-onlyreason;- valid option labels/values; and
- an
example.items[0]payload generated from that live schema.
A shortened representative response looks like this:
{
"ok": true,
"board": {
"id": "the-real-board-uuid",
"name": "לקוחות"
},
"fields": [
{
"key": "advertised-company-name-key",
"id": "column-uuid-1",
"title": "שם חברה / לקוח",
"type": "text",
"writable": true,
"primary": true,
"format": "a string"
},
{
"key": "advertised-client-type-key",
"id": "column-uuid-2",
"title": "סוג לקוח",
"type": "label",
"writable": true,
"options": [
{ "value": "company", "label": "חברה" },
{ "value": "private", "label": "פרטי" },
{ "value": "business", "label": "עסק" }
]
}
],
"accepts": "Address fields by key (preferred), column id, or exact title (case-insensitive). Unknown fields are ignored with a warning.",
"example": {
"items": [
{
"advertised-company-name-key": "Sample text"
}
]
}
}
The placeholder keys above intentionally are not guessed. Copy the real keys from your response. Exact titles are also accepted and make the first manual example easier to read, but stable integrations should map to advertised keys.
Before sending any field, enforce:
writable === true
value conforms to format
option is one of fields[].options[].value or .label
Step 9: create the smallest valid CRM customer
The API accepts either a single row object or { "items": [...] }. Using exact
current column titles, a minimal customer is:
{
"שם חברה / לקוח": "Orion Events",
"איש קשר": "Dana Cohen"
}
The title-address form is convenient for a human-run test. For production,
replace those titles with the real schema key values.
Save the JSON as crm-customer.json, then send it:
curl -X POST \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @crm-customer.json \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_CUSTOMERS_BOARD_ID/items"
Step 10: send a complete CRM row
This is a fuller payload using current exact titles:
{
"items": [
{
"שם חברה / לקוח": "Orion Events Ltd.",
"איש קשר": "Dana Cohen",
"טלפון": "+972505550142",
"אימייל": "dana@example.com",
"כתובת": {
"address": "12 HaArba'a Street, Tel Aviv, Israel",
"lat": null,
"lng": null
},
"סוג לקוח": "company",
"ערוץ קשר מועדף": "WHATSAPP",
"חבילה אחרונה": "MAG_STAGE_SHOW",
"חלון ריבוק": "2027-03-15"
}
]
}
Important value rules in that example:
- phone and email are strings;
כתובתis a JSON object because the live schema advertises a JSON location format;- date-only values use
YYYY-MM-DD; סוג לקוחuses one ofcompany,private, orbusiness;ערוץ קשר מועדףuses one ofWHATSAPP,PHONE, orEMAIL;חבילה אחרונהis text on the CRM board, so the suite's package token can be stored as text when useful to the integration.
If your live schema advertises a different format or option set, the live schema wins.
Step 11: choose who fills the AI research cell
The CRM's תובנות מהרשת column has an online-search autocomplete configured.
There are two correct strategies.
Strategy A: the external agent already researched the customer
Send the grounded result directly:
{
"items": [
{
"שם חברה / לקוח": "Orion Events Ltd.",
"איש קשר": "Dana Cohen",
"טלפון": "+972505550142",
"אימייל": "dana@example.com",
"סוג לקוח": "company",
"ערוץ קשר מועדף": "EMAIL",
"תובנות מהרשת": "Orion Events produces corporate launch events in central Israel. Public material reviewed on 2026-07-16 highlights technology and employee-experience events. Suggested opening: ask about audience participation goals and whether a close-up reception segment should complement the stage show. Sources were checked by the importing agent before submission."
}
]
}
Because the AI-powered cell is already populated, its empty-cell autocomplete does not need to repeat the research.
Only send research that is current, relevant, and supported. Do not infer sensitive traits, copy private data, or fabricate a source. Keep the source URLs in a dedicated URL/source column if the board has one; do not overload the summary unless that is the workspace's chosen convention.
Strategy B: let Dotallio research the customer
Omit תובנות מהרשת:
{
"items": [
{
"שם חברה / לקוח": "Orion Events Ltd.",
"איש קשר": "Dana Cohen",
"טלפון": "+972505550142",
"אימייל": "dana@example.com",
"סוג לקוח": "company"
}
]
}
Omitted, null, and empty AI values remain unset. The configured autocomplete
can use the company and contact context after row creation.
Do not send plausible-sounding filler merely to avoid an AI run. A blank cell is a valid instruction: “the configured board automation owns this value.”
Step 12: send the request from PowerShell
On Windows PowerShell, keep the JSON in a UTF-8 file so Hebrew field titles are preserved:
$headers = @{
Authorization = "Bearer $env:DOTALLIO_API_KEY"
"Content-Type" = "application/json"
}
$uri = "$env:DOTALLIO_ORIGIN/api/v1/boards/$env:DOTALLIO_CUSTOMERS_BOARD_ID/items"
$body = Get-Content -LiteralPath ".\crm-customer.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
A successful write returns created IDs, the board ID, and warnings:
{
"ok": true,
"createdIds": ["new-item-uuid"],
"boardId": "the-real-board-uuid",
"warnings": []
}
Treat non-empty warnings as integration defects to inspect. Unknown fields are ignored with warnings, so HTTP 200 alone does not prove every requested cell was stored.
Each validated CRM row costs one weighted workspace operation. A 25-customer batch costs 25 operations even though it uses one HTTP request.
Step 14: read the customer back
curl \
-H "Authorization: Bearer $DOTALLIO_API_KEY" \
"$DOTALLIO_ORIGIN/api/v1/boards/$DOTALLIO_CUSTOMERS_BOARD_ID/items?limit=20&offset=0"
The newest rows are first. Returned fields use the same preferred schema keys
as POST, not necessarily the Hebrew title strings used in a manual test.
Verify:
- the returned item ID matches
createdIds; - the company, contact, phone, email, options, and date are correct;
- warnings were empty;
- the online-insights cell contains the supplied research, or is queued/filled by the configured autocomplete when omitted; and
- the row is visible in the לקוחות board UI.
Step 15: import several CRM customers
One request may contain 1–100 rows:
{
"items": [
{
"שם חברה / לקוח": "Orion Events Ltd.",
"איש קשר": "Dana Cohen",
"טלפון": "+972505550142",
"אימייל": "dana@example.com",
"סוג לקוח": "company",
"ערוץ קשר מועדף": "EMAIL"
},
{
"שם חברה / לקוח": "Ariel Levi",
"איש קשר": "Ariel Levi",
"טלפון": "+972525550143",
"סוג לקוח": "private",
"ערוץ קשר מועדף": "WHATSAPP"
},
{
"שם חברה / לקוח": "North Star Productions",
"איש קשר": "Lior Bar",
"טלפון": "+972545550144",
"אימייל": "lior@example.com",
"סוג לקוח": "business",
"ערוץ קשר מועדף": "PHONE",
"חלון ריבוק": "2027-01-20"
}
]
}
Validation is batch-atomic: if one row has an invalid option or format, the API
returns 422 and inserts none of the validated rows. Fix the reported
itemIndex values and resend the complete batch.
A later runtime/database failure can be partially applied. If a 503 says
that earlier rows were created, read the board and reconcile before retrying.
Step 16: prevent duplicate CRM customers
The native quote workflow matches customers by phone. The generic public row endpoint does not promise a template-specific upsert.
Before importing:
- normalize phone numbers to one canonical international representation;
- read existing CRM rows;
- index non-empty normalized phone numbers;
- fall back to normalized email when phone is absent;
- decide how to handle a same-name/different-phone record; and
- send only new records.
For a recurring integration, add a dedicated immutable source ID column to the
CRM—such as source_customer_id—then fetch the refreshed schema and deduplicate
on that value. Do not use a company name alone as the stable identity.
Step 17: give an external AI agent a safe operating instruction
Keep the key in the runtime, then give the agent a contract like this:
You are importing customers into the installed magician suite's Customers CRM.
1. Call GET /api/v1/boards and select the board named "לקוחות" in the intended
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. Read existing items and deduplicate primarily by normalized phone, then
normalized email or a configured external source ID.
5. Fill all grounded customer fields. If you have current, source-supported
company research, write it to the advertised key for "תובנות מהרשת".
6. If research is missing or uncertain, omit that field so Dotallio's configured
autocomplete can own it. Never fabricate research.
7. Send no more than 100 rows per request and inspect warnings and createdIds.
8. A direct CRM POST creates CRM rows only. Do not claim it created a quote or
ran the template intake workflow.
9. Never print, log, return, or place the Dotallio key in model context.
Step 18: know when to use the form instead of the API
Use the native quote intake form when the desired result includes:
- quote-number and quote-status defaults;
- event date, time, venue, audience, and pricing capture;
- customer match/create behavior tied to phone;
- customer–quote relationship setup;
- the magician/mentalist template persona;
- generated quote artifacts or PDFs; or
- the template's intake workflow side effects.
Use a direct CRM API POST when the desired result is:
- importing a contact list;
- synchronizing customers from another CRM;
- adding researched prospects as customer records;
- enriching CRM columns with data the external agent already knows; or
- creating CRM rows for later human qualification.
If a server needs a first-class “submit quote questionnaire” API in the future, that should be a separately documented template-workflow endpoint. Do not simulate it by guessing all quote-board cells and claiming equivalent behavior.
Step 19: magician CRM troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
לקוחות is missing from /boards | wrong workspace, allowlist, or app membership | Reopen Public API, verify workspace and selected boards; check app membership |
canInsert is false | user is a viewer | Grant the represented user editor/admin access or create a key for an authorized owner |
403 INSUFFICIENT_SCOPE | missing endpoint permission | Add only the required permission and apply the key policy |
404 BOARD_NOT_FOUND | wrong UUID or board outside policy | Rediscover with /boards; do not reuse an ID from another install |
422 on סוג לקוח | invalid label/value | Use a value or label from the live field options |
422 on כתובת | wrong location shape | Copy the live field format; normally send an address/lat/lng object |
| Row created but a field is absent | unknown field was ignored | Inspect warnings, fetch schema, and use its advertised key |
| Online insights did not run | cell was filled, dependencies missing, or workflow unavailable | Decide whether the external agent owns the value; otherwise omit it and provide company/contact context |
| Duplicate customer | direct endpoint is create-only | Reconcile by normalized phone/email or external source ID before POST |
| Customer exists but no quote | CRM POST does not run quote intake | Submit the native quote form for quote-workflow behavior |
429 after a batch | minute or shared monthly limit | Honor rate-limit headers; remember each row consumes one weighted operation |
| Old key stopped after rotation | rotation has zero overlap | Deploy the replacement secret immediately; the predecessor is already invalid |
Step 20: production handoff checklist
- The correct magician template is installed once, not duplicated.
- Setup answers contain the real stage/business name and approved branding.
- The native quote intake questionnaire was tested end to end.
- The key is on Public API, not the extension-key page.
- The key is scoped to the correct workspace and לקוחות board.
- Only the required read/write permissions are enabled.
- The one-time secret is in a backend secret manager.
- The sender discovers the board UUID and fetches schema at job start.
- Production mappings use advertised field keys.
- Option values and location/date formats come from the live schema.
- Research is grounded, or the AI column is omitted for autocomplete.
- Phone/email/source-ID deduplication runs before create.
- Warnings and created IDs are recorded without logging the credential.
- Runtime partial failures reconcile before retry.
- 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.
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 does not expose a client idempotency-key header. The client must reconcile writes that have an uncertain outcome.
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 |
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
UNKEY_PUBLIC_API_ROOT_KEY="..."
UNKEY_PUBLIC_API_ID="..."
UNKEY_PUBLIC_API_BURST_NAMESPACE="..."
UNKEY_PUBLIC_API_WORKSPACE_NAMESPACE="..."
Never prefix these with NEXT_PUBLIC_.
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