API reference
One endpoint takes leads from every website. Agents manage websites, forms and leads through the agent API or the MCP server.
Endpoints and credentials
Every website posts to the same URL. The key tells 1DB which website it is, and formSlug which form.
| What | URL | Credential |
|---|---|---|
| Send a lead | POST https://gallant-goshawk-513.convex.site/v1/leads | Ingest key: X-API-Key: 1db_… |
| Agent API | https://gallant-goshawk-513.convex.site/v1/agent/… | Agent token: Authorization: Bearer 1db_at_… |
| MCP server | https://www.1-db.com/api/mcp | Agent token, same header |
The API is not served from www.1-db.com; only the MCP server is. In code, use ${DB_API_ENDPOINT}/v1/leads with these variables:
DB_API_ENDPOINT=https://gallant-goshawk-513.convex.site
DB_API_KEY=1db_... # 1db_ followed by 32 hex characters
DB_FORM_SLUG=acme-contact # the real slug, from the dashboard, get_website or 1db init- Ingest key (
1db_+ 32 hex characters): one per website. It can only create leads, so it is safe in browser code. A server route is still better: it can pass on the visitor's IP and browser and retry on failure. If the browser posts directly, the public prefix is fine:NEXT_PUBLIC_DB_API_KEY,VITE_DB_API_KEY. - Agent token (
1db_at_…): manages an organization. Only a person can create one, in Settings → API keys → Agent tokens or by approvingnpx @1db/cli setupin the browser. It never goes in a project.
For AI agents
If you don't have an agent token, ask your user for one. Don't try to sign up or sign in to 1DB yourself.
Form slugs are unique across all of 1DB and look like acme-contact (website slug, then the form). Always use the real slug, never a guess like contact.
Send a lead
curl -X POST https://gallant-goshawk-513.convex.site/v1/leads \
-H "Content-Type: application/json" \
-H "X-API-Key: $DB_API_KEY" \
-d '{"formSlug":"acme-contact","email":"jane@example.com","name":"Jane Doe","message":"Hello"}'The body is formSlug plus the form's fields, flat. A stored lead answers:
200 {"success":true,"leadId":"…","duplicate":false}duplicate: true means this key already sent this submission_id (v2 only): nothing new was stored or sent on.
Errors from /v1/leads
Every error has a readable error string and a stable code, and a hint saying what to change. Show error to visitors; branch on code.
{"error":"Check the highlighted fields","code":"INVALID_FIELDS",
"fields":{"email":"Enter a valid email address"},"field":"email","details":"Enter a valid email address"}| Code | Status | Meaning |
|---|---|---|
MISSING_API_KEY | 401 | Send the website's ingest key in the X-API-Key header. |
INVALID_API_KEY | 401 | The key is wrong or was revoked. Get the live one from the dashboard, get_website or 1db keys. |
INVALID_JSON | 400 | The body isn't valid JSON. |
INVALID_FIELDS | 400 | One or more fields are wrong. fields names each one with a message you can show the visitor. |
FORM_NOT_FOUND | 404 | No form has that formSlug. Use the slug from the dashboard, get_website or 1db init. |
KEY_NOT_FOR_FORM | 403 | The form belongs to another organization than the key. |
KEY_NOT_FOR_WEBSITE | 403 | The key is for another website than the form. Use that website's key. |
RATE_LIMIT_API_KEY | 429 | Too many leads on this key. Wait retryAfter seconds. |
RATE_LIMIT_IP | 429 | Too many leads from one visitor IP. Wait retryAfter seconds. |
INTERNAL | 500 | Something broke on 1DB's side. Retry with backoff and the same submission_id. |
fields maps each bad field to a message; field and details repeat the first one. 429s also carry retryAfter (seconds) and a Retry-After header.
Lead contract
v1 (the default) stores the body as sent. v2 checks every field and names each bad one. Opt in per request with X-1DB-Contract: 2.
Headers
| Header | Meaning |
|---|---|
X-API-Key | The website's ingest key. Required. |
X-1DB-Contract: 2 | Check the body against v2 and answer with every bad field. |
X-1DB-Client-IP | The visitor's IP, when a server route sends the lead: cf-connecting-ip on Cloudflare, the first x-forwarded-for hop on Vercel. |
X-1DB-Client-UA | The visitor's browser user-agent. |
The two client headers work with either contract. Without them, 1DB uses the connection's own IP and user agent, so every visitor of a server route shares one rate-limit bucket.
v2 fields
Use these names; common aliases such as zip or firstName still work. Fields not listed here are stored as sent.
| Field | Rule |
|---|---|
submission_id | Required. 8–128 of A-Z a-z 0-9 . _ : -. A UUID made when the form loads. externalId is accepted for it. |
email, phone | One of the two is required. Email must be well formed; phone must parse to E.164. |
first_name, last_name | Up to 80 characters each. |
company | Up to 120. |
lead_type | buy, sell, recycle, waste or inquiry. |
service_label, product_label | The site's own wording for those two, up to 200. Kept as metadata. |
product_type | A product type slug the website's organization uses (see list_product_types). |
quantity | A whole number from 1 to 1,000,000. A digit string is accepted and stored as a number. |
address_line_1, address_line_2 | Up to 200. |
city / state | Up to 80 / 40. |
postal_code | US ZIP or ZIP+4, or a Canadian postal code; up to 12 characters when country is another country. |
timeframe | asap, this_week, within_2_weeks, this_month, within_a_month, flexible, just_pricing. Labels like “Within 2 weeks” are accepted. |
needed_by | YYYY-MM-DD. |
sms_consent | yes or no (or a boolean). yes needs sms_consent_text (the exact checkbox wording) and a phone. |
message | Up to 2,000. |
_1db_utm_*, _1db_referrer, _1db_landing_page, _1db_current_page | Attribution. |
_1db_gclid, _1db_gbraid, _1db_wbraid, _1db_fbclid, _1db_msclkid, _1db_ttclid, _1db_ga_client_id | Click and analytics IDs. |
Retries
On a 429 or 500, retry with the same submission_id. If the first attempt was stored, the retry answers duplicate: true and nothing is sent twice. The same id on another key is a different lead. v1 never dedupes.
Rate limits
/v1/leads: 100 leads a minute per ingest key (bursts to 200), and 50 a minute per visitor IP./v1/agent/*: 60 requests a minute per agent token (bursts to 120).
Agent API
Send the agent token as Authorization: Bearer 1db_at_…. GET /v1/agent lists every route. The routes, MCP tools and CLI commands are in AI agents, MCP and CLI.
curl https://gallant-goshawk-513.convex.site/v1/agent/websites/health?website=acme.com \
-H "Authorization: Bearer $ONEDB_TOKEN"Errors are an object under error:
{"error":{"code":"forbidden","message":"This token is missing the routing scope","hint":"…"}}| Code | Status | Meaning |
|---|---|---|
invalid_argument | 400 | A parameter is missing or wrong; message says which. |
unauthorized | 401 | No token, or it is invalid or revoked. Ask your user for a new one. |
forbidden | 403 | The token lacks the scope this route needs; message names it. |
not_found | 404 | No such website, form or rule in the token's organization. |
conflict | 409 | The request clashes with current state, e.g. a test lead for a site with no live key. |
rate_limited | 429 | Over 60 requests a minute. Wait retryAfter seconds. |
upstream | 502 | A step that calls out failed, e.g. the test lead's post to /v1/leads. |
internal | 500 | Something broke on 1DB's side. Retry later. |
Embed script
For a site where you can't write code, one script tag renders a form built from the form's fields in 1DB and posts it to /v1/leads:
<script src="https://gallant-goshawk-513.convex.site/v1/embed.js?key=1db_...&form=acme-contact" async></script>
<button data-1db>Contact us</button> <!-- opens the popup -->
<div data-1db-inline></div> <!-- or renders it here, in inline mode -->The mode (popup, slide-in or inline), title, colors and success message come from the form's embed settings in the dashboard. The key is in the page, which is fine for an ingest key.