DocumentationDotallio Public API Keys

Product and integration details for teams building with Dotallio.

Browse documentation

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/v1

Machine-readable contract: /api/v1/openapi.json


Table of contents

  1. Start here
  2. What a public API key is
  3. Security model
  4. Create a key
  5. Choose permissions
  6. Choose board access
  7. Plans and limits
  8. Authenticate requests
  9. Five-minute quickstart
  10. Magician-suite CRM walkthrough
  11. Endpoint reference
  12. Understand board schemas
  13. Field names and addressing
  14. Supported value formats
  15. AI-powered columns
  16. Create complete rows
  17. Batch ingestion
  18. Convert CSV to JSON
  19. Build a board-aware AI agent
  20. Movie-board example
  21. Research-and-expand example
  22. Read and paginate items
  23. Deduplication and retry safety
  24. Responses and rate-limit headers
  25. Error reference
  26. Key lifecycle
  27. Security practices
  28. Incident response
  29. Deployment and Unkey setup
  30. Extension keys are separate
  31. Troubleshooting
  32. Frequently asked questions
  33. 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:

  1. Fetch the board schema before writing. Do not guess field names or option values.
  2. Send complete, grounded rows. If your agent already knows a value, including a value for an AI-powered column, send it.
  3. 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 requestFive-minute quickstart
install a magician CRM and send it a leadMagician-suite CRM walkthrough
import a CSVConvert CSV to JSON
build a research agentBuild a board-aware AI agent
understand every endpointEndpoint reference
respond to a leaked keyIncident response
configure Unkey for a deploymentDeployment 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:

  1. Select the workspace.
  2. Enter a recognizable key name.
  3. Choose an expiry from 1 through 365 days.
  4. Choose a permission preset or explicit permissions.
  5. Choose selected-board access or all-board access.
  6. Review the effective tier limits.
  7. Create the key.
  8. Copy the secret before dismissing the one-time panel.
  9. Store it in a production secret manager.
  10. Make a low-risk GET /api/v1/me request 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

EnvironmentPrefixExample shape
Productiondot_live_dot_live_…
Development/testdot_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.

PermissionAllowsEndpoint
profile.readVerify identity and read the key's workspace profileGET /api/v1/me
boards.listList boards allowed by the key policyGET /api/v1/boards
board.schema.readRead columns, accepted values, and examplesGET /api/v1/boards/{boardId}/schema
board.items.readRead board rowsGET /api/v1/boards/{boardId}/items
board.items.writeCreate complete rowsPOST /api/v1/boards/{boardId}/items

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.

TierRequests per minute per keyWeighted operations per workspace per UTC month
FREEAPI unavailableAPI unavailable
MEMBER6025,000
PLUS120100,000
PRO300500,000
ENTERPRISEAdmin override, otherwise PRO fallbackAdmin override, otherwise PRO fallback

Weighted costs

OperationCost
Any authenticated GET1
Item POSTNumber 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:

PropertyCurrent value
Template nameHebrew Magician/Mentalist Quote Suite
Template IDservice-provider-quote-suite-he-magician-mentalist
CRM boardלקוחות (COMPANIES_BOARD_ID)
Quote/intake boardהצעות מחיר (EVENTS_BOARD_ID)
Default localeHebrew / 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:

  1. App setup questionnaire: the owner answers this once after installation. It configures branding, language, tax, pricing defaults, and generated documents.
  2. 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:

  1. Open the workspace where the suite should live.
  2. Open the main AI chat.
  3. Click the Create App mode chip. Its input changes to What app should I build?
  4. 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.
  1. Send the message.
  2. 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.
  3. Let the AI use the existing template search and install flow. The expected template ID is service-provider-quote-suite-he-magician-mentalist.
  4. 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:

  1. Open the app switcher in the sidebar.
  2. Click Create New App.
  3. In Create a new app, stay on App Templates.
  4. In Search app templates…, search for magician.
  5. Select Hebrew Magician/Mentalist Quote Suite.
  6. Review What this template creates.
  7. Click Install.
  8. 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.

QuestionRequired?Current default or choicesWhat it controls
Feature selectionvariestemplate feature groupsWhich optional packaged capabilities stay enabled
Document languageyesHebrew, English, Hebrew + EnglishQuote PDFs and intake-form language
Your name or stage nameyesemptyTitle printed on every generated quote PDF
PhonenoemptyBooking phone in the quote footer
EmailnoemptyReply/booking email in the quote footer
LogonoPNG or JPEG, up to 5 MBBrand logo in generated material
VAT rateyes17%Tax calculations and document defaults
Validityyes30 daysDefault quote validity, 1–365 days
CurrencyyesILS, USD, or EURMonetary formatting and defaults
Starting price for a 1-hour shownoemptyContext for AI pricing suggestions
Brand voicenoPlayful, Professional, MysticalTone 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:

  1. Upload a logo only if it is ready and approved.
  2. Click Continue.
  3. Wait for Setup complete.
  4. Follow the product tour to identify Your intake form, Your CRM, and Your quotes board.
  5. 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 IDVisible questionRequired
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:

  1. Open the app's intake page.
  2. Find the form titled הצעת מחיר חדשה.
  3. Select Hebrew or English.
  4. Start typing an existing customer in שם חברה / לקוח. Existing CRM matches are suggested and can autofill contact name and phone.
  5. Fill the event, audience, and three pre-VAT prices.
  6. Click יצירת הצעה.
  7. Confirm the quote appears on הצעות מחיר.
  8. 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:

ColumnTypeNotes
שם חברה / לקוחtextrequired primary customer/company name
איש קשרtextrequired main contact
טלפוןphonecontact phone
אימיילtextcontact email
כתובתlocationJSON address/coordinates when advertised writable
סוג לקוחlabelcompany, private, or business
ערוץ קשר מועדףlabelWHATSAPP, PHONE, or EMAIL
חבילה אחרונהtextlast purchased/performed package
חלון ריבוקdaterecommended rebooking date
תובנות מהרשתrich text + AIonline research summary
סיכום AImultimodal JSONstructured text/files context
התנגדות אחרונהmultimodal JSONlatest 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.

  1. Open the user menu.
  2. Click Public API.
  3. Under Create a scoped key, select the suite's workspace.
  4. Enter a name such as Magician CRM intake — production.
  5. Set Expires (days). Use the shortest operationally practical period; 90 or 365 days are common depending on your rotation process.
  6. Under Permissions, select:
    • List boards;
    • Read board schemas;
    • Read board items; and
    • Create full board rows.
  7. Read profile is optional for this integration. Add it only if your service calls /api/v1/me for a health check.
  8. Under Board access, keep Selected boards only (recommended).
  9. Select לקוחות.
  10. Select הצעות מחיר only if this same integration genuinely needs to read or write the quote board. CRM-only ingestion does not need it.
  11. Review the displayed per-minute and monthly workspace limits.
  12. Click Create key.
  13. In the one-time secret panel, click Copy and put the secret directly in the backend's secret manager.
  14. 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 key and immutable column id;
  • the visible title and storage type;
  • writable, primary, format, and read-only reason;
  • 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 of company, private, or business;
  • ערוץ קשר מועדף uses one of WHATSAPP, PHONE, or EMAIL;
  • חבילה אחרונה 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:

  1. the returned item ID matches createdIds;
  2. the company, contact, phone, email, options, and date are correct;
  3. warnings were empty;
  4. the online-insights cell contains the supplied research, or is queued/filled by the configured autocomplete when omitted; and
  5. 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:

  1. normalize phone numbers to one canonical international representation;
  2. read existing CRM rows;
  3. index non-empty normalized phone numbers;
  4. fall back to normalized email when phone is absent;
  5. decide how to handle a same-name/different-phone record; and
  6. 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

SymptomLikely causeFix
לקוחות is missing from /boardswrong workspace, allowlist, or app membershipReopen Public API, verify workspace and selected boards; check app membership
canInsert is falseuser is a viewerGrant the represented user editor/admin access or create a key for an authorized owner
403 INSUFFICIENT_SCOPEmissing endpoint permissionAdd only the required permission and apply the key policy
404 BOARD_NOT_FOUNDwrong UUID or board outside policyRediscover with /boards; do not reuse an ID from another install
422 on סוג לקוחinvalid label/valueUse a value or label from the live field options
422 on כתובתwrong location shapeCopy the live field format; normally send an address/lat/lng object
Row created but a field is absentunknown field was ignoredInspect warnings, fetch schema, and use its advertised key
Online insights did not runcell was filled, dependencies missing, or workflow unavailableDecide whether the external agent owns the value; otherwise omit it and provide company/contact context
Duplicate customerdirect endpoint is create-onlyReconcile by normalized phone/email or external source ID before POST
Customer exists but no quoteCRM POST does not run quote intakeSubmit the native quote form for quote-workflow behavior
429 after a batchminute or shared monthly limitHonor rate-limit headers; remember each row consumes one weighted operation
Old key stopped after rotationrotation has zero overlapDeploy 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:

ParameterDefaultRangeBehavior
limit501–200Out-of-range values are clamped
offset00 or greaterInvalid/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:

  1. the advertised schema key — preferred;
  2. the immutable column UUID — always unambiguous;
  3. 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.

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:

  1. read the board;
  2. reconcile by a stable source identifier or normalized title;
  3. remove rows that already exist from the retry payload;
  4. retry only the missing rows.

Batch ingestion

One request accepts at most 100 rows. Split larger jobs into bounded batches.

Recommended batch sizes:

Job typeSuggested batch size
Simple text/number rows50–100
Rows with many fields25–50
Research-heavy agent output10–25 after research completes
Reconciliation-sensitive imports10–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

  1. Fetch the current board schema.
  2. Build a mapping from source header to schema key.
  3. Reject or quarantine required source columns that are absent.
  4. Normalize each source value using the schema's type, format, and options.
  5. Omit source columns with no intended board mapping.
  6. Deduplicate rows.
  7. Split into batches.
  8. POST JSON.
  9. 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

OutcomeRetry?Action
400 malformed requestNo automatic retryFix request construction
401 invalid/expiredNo automatic retryReplace or rotate credential
403 permission/tierNo automatic retryCorrect policy, role, or plan
404 boardNo automatic retryRefresh board list and policy
413 size/countRetry after changeSplit payload
422 validationRetry after changeApply issues and resend full batch
429 rate limitYesWait for Retry-After, then retry
503 unavailableYes, boundedWait for Retry-After; use backoff and jitter
uncertain connection loss during POSTReconcile firstRead 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:

HeaderMeaning
X-Request-IdDotallio request correlation ID
Cache-Control: private, no-storeDo not cache credential-bound content
Vary: Authorization, X-API-KeyCredential-sensitive response variance
RateLimit-LimitLimit for the reported window
RateLimit-RemainingRemaining operations in the reported window
RateLimit-ResetUnix 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

CodeTypical statusMeaningRecovery
INVALID_JSON400Body is not valid JSONFix serialization and content type
INVALID_REQUEST400Request shape or parameter is invalidFollow endpoint contract
MISSING_API_KEY401No usable credential was sentSend exactly one credential header
INVALID_API_KEY401Key is invalid, disabled, retired, or revokedCreate/rotate a key
EXPIRED_API_KEY401Key expiry passedRotate or replace it
AMBIGUOUS_API_KEY400Both credential headers were sentKeep only one header
INSUFFICIENT_SCOPE403Endpoint permission is missingUpdate key permissions or use another key
FORBIDDEN403Current product role/state denies accessRestore legitimate access; do not bypass
API_UNAVAILABLE_FOR_TIER403Workspace plan does not include API accessUpgrade or change plan
BOARD_NOT_FOUND404Board is absent or intentionally hiddenRefresh allowed boards and check policy
TOO_MANY_ITEMS413Batch exceeds 100 rowsSplit it
PAYLOAD_TOO_LARGE413Body exceeds 1 MBSplit or reduce fields
VALIDATION_ERROR422One or more rows failed schema validationApply issues and resend
RATE_LIMITED429Per-key or monthly budget exhaustedHonor headers and retry when allowed
API_NOT_PROVISIONED503Deployment schema is not readyMaintainer runs schema rollout
PROVIDER_UNAVAILABLE503Unkey/metering is unavailableBounded retry
INTERNAL_ERROR503Board data operation could not completeReconcile 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:

  1. Open Account → Public API.
  2. Select Rotate.
  3. Confirm immediate invalidation.
  4. Copy the new secret.
  5. Update the secret manager.
  6. restart/redeploy only the consuming service if needed;
  7. verify with /me;
  8. verify one read operation;
  9. verify one low-risk write when the integration writes;
  10. 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 401 errors;
  • 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-Key headers;
  • 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

  1. Revoke the key immediately.
  2. Do not wait to confirm exploitation.
  3. Identify every system that stored or used it.
  4. Review usage and lifecycle audit history.
  5. Review application logs by safe prefix, local key ID, and request ID.
  6. Confirm board scope and write permission exposure.
  7. Create a replacement with narrower policy if possible.
  8. update the consuming secret store;
  9. remove the exposed value from logs, tickets, and build artifacts where feasible;
  10. verify the revoked value returns 401.

Unexpected high usage

  1. Identify whether the denial is per-key minute or shared monthly budget.
  2. Review recent deploys for retry loops.
  3. Check whether a 429 handler is retrying without waiting.
  4. Check whether pagination restarts from offset zero.
  5. Check whether failed batches are being resent without reconciliation.
  6. Revoke the key if usage appears malicious.
  7. 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:

  1. per-key burst namespace;
  2. 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:

  1. create a test key in a non-production deployment;
  2. confirm the dot_test_ prefix;
  3. call /me;
  4. list selected boards;
  5. verify an excluded board returns 404;
  6. verify a missing permission returns 403;
  7. create one row with a known AI field;
  8. create one row with the AI field omitted;
  9. confirm the first remains unchanged and the second can autocomplete;
  10. rotate and confirm the old key fails immediately;
  11. revoke and confirm repeated revoke is safe;
  12. 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:

  1. refresh schemaUrl;
  2. update option values;
  3. remove read-only fields;
  4. correct field addresses;
  5. fix value formats;
  6. 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 null or 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 422 repair details.
  • Honor 429 and 503 retry headers.
  • Reconcile uncertain POST failures.
  • Redact credential headers from logs.
  • Store X-Request-Id for 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.

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