Developer documentation
Kontax API v1
Read and write your contacts programmatically. All API requests are authenticated with a Bearer token created in Settings → Developer.
Introduction
The Kontax REST API allows you to list, create, update, and delete contacts from external scripts, automations, and integrations. API access is available on Pro, Family, and Teams plans.
All requests must include a valid Authorization header. The API returns JSON and uses standard HTTP status codes. All timestamps are ISO 8601 in UTC.
Authentication
Generate an API token in Settings → Developer and include it as a Bearer token on every request:
Authorization: Bearer ktx_live_your-token-hereTokens come in two scopes. Read-only tokens can list and fetch contacts. Read-write tokens can also create, update, and delete contacts. The API returns 403 Forbidden if a read-only token attempts a write operation.
Tokens are shown once on creation and hashed at rest. If you lose a token, revoke it and create a new one — there is no way to retrieve the original value.
Endpoints
All endpoints are under the base URL above. Responses are JSON.
/contactsList contacts. Returns up to 100 per page, ordered by full name.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | — | Search by name, company, or email |
limit | integer | 50 | Results per page (max 100) |
cursor | string | — | Pagination cursor from previous response |
bookId | string | — | Filter to a specific address book |
archived | boolean | false | Return archived contacts instead of active ones |
Example request
curl -H "Authorization: Bearer ktx_live_..." \
"https://api.getkontax.com/v1/contacts?q=acme&limit=10"Example response
{
"contacts": [
{
"id": "clx7a...",
"firstName": "Jane",
"lastName": "Smith",
"fullName": "Jane Smith",
"company": "Acme Corp",
"jobTitle": "Head of Sales",
"notes": null,
"birthday": null,
"emails": [{ "value": "[email protected]", "label": "work", "isPrimary": true }],
"phones": [{ "value": "+1 415 555 0100", "label": "mobile", "isPrimary": true }],
"bookId": null,
"source": "MANUAL",
"createdAt": "2026-06-01T09:00:00.000Z",
"updatedAt": "2026-06-10T14:32:00.000Z"
}
],
"pagination": { "cursor": "clx7a...", "hasMore": true }
}/contactsCreate a contact. Requires a read-write token. At least one of firstName, lastName, fullName, or company is required.
Request body (JSON)
| Field | Type | Description |
|---|---|---|
firstName | string | Given name (max 80 chars) |
lastName | string | Family name (max 80 chars) |
fullName | string | Override the derived full name (max 200 chars) |
company | string | Company or organisation (max 120 chars) |
jobTitle | string | Job title (max 120 chars) |
notes | string | Free-text notes (max 10,000 chars) |
birthday | string | YYYY-MM-DD or --MM-DD (year unknown) |
emails | array | Up to 10 email entries — see entry format below |
phones | array | Up to 10 phone entries — see entry format below |
bookId | string | CUID of an address book to place the contact in |
Email and phone entries use the shape { value: string, label?: string }. The first entry in each array becomes the primary. Omitting label defaults to primary / mobile.
Example request
curl -X POST "https://api.getkontax.com/v1/contacts" \
-H "Authorization: Bearer ktx_live_..." \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Smith",
"company": "Acme Corp",
"emails": [{ "value": "[email protected]", "label": "work" }],
"phones": [{ "value": "+1 415 555 0100", "label": "mobile" }]
}'Returns 201 Created with the created contact object, or 400 / 403 on validation or limit errors.
/contacts/:idFetch a single contact by ID.
curl -H "Authorization: Bearer ktx_live_..." \
"https://api.getkontax.com/v1/contacts/clx7a..."Returns the contact object, or 404 Not Found if the contact does not exist or belongs to a different user.
/contacts/:idUpdate a contact. Requires a read-write token. Only fields included in the request body are updated — omitted fields are left unchanged (PATCH semantics despite the PUT method name).
Note: emails and phones are replaced entirely when included. To add a phone number without losing existing ones, send the complete array.
curl -X PUT "https://api.getkontax.com/v1/contacts/clx7a..." \
-H "Authorization: Bearer ktx_live_..." \
-H "Content-Type: application/json" \
-d '{ "jobTitle": "VP of Sales" }'Returns the updated contact object.
/contacts/:idArchive a contact (soft delete). The contact is hidden from the contacts list but remains in the database. Add ?permanent=true to hard-delete immediately — this cannot be undone.
# Archive (reversible)
curl -X DELETE -H "Authorization: Bearer ktx_live_..." \
"https://api.getkontax.com/v1/contacts/clx7a..."
# Permanent delete
curl -X DELETE -H "Authorization: Bearer ktx_live_..." \
"https://api.getkontax.com/v1/contacts/clx7a...?permanent=true"Returns 204 No Content on success.
Pagination
List responses include a pagination envelope. Pass the returned cursor as a query parameter on the next request to fetch the next page.
{
"contacts": [ /* up to limit items */ ],
"pagination": {
"cursor": "clx7b...", // pass as ?cursor= on the next request
"hasMore": true // false on the last page
}
}Cursor-based pagination is stable: inserting or deleting contacts between pages does not cause duplicates or gaps. The default limit is 50; the maximum is 100.
Errors
All error responses use the same JSON shape:
{ "error": "ERROR_CODE", "message": "Human-readable description." }Validation errors include a details field with field-level messages.
| Status | Error code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body is invalid. See details field. |
| 400 | INVALID_JSON | Request body is not valid JSON. |
| 401 | UNAUTHENTICATED | Authorization header is missing or malformed. |
| 401 | INVALID_TOKEN | Token is invalid, expired, or revoked. |
| 403 | FORBIDDEN | Read-only token attempted a write operation. |
| 403 | LIMIT_REACHED | Contact limit for your plan has been reached. |
| 404 | NOT_FOUND | Contact not found or belongs to another user. |
| 429 | RATE_LIMITED | Too many requests. See X-RateLimit-* headers. |
| 500 | INTERNAL_ERROR | Unexpected server error. |
Field reference
All fields returned by the API. Write endpoints accept a subset.
| Field | Type | Writable | Notes |
|---|---|---|---|
id | string | — | CUID, assigned on creation |
firstName | string | null | ✓ | Max 80 chars |
lastName | string | null | ✓ | Max 80 chars |
fullName | string | ✓ | Derived from parts if omitted; required indirectly |
company | string | null | ✓ | Max 120 chars |
jobTitle | string | null | ✓ | Max 120 chars |
notes | string | null | ✓ | Max 10,000 chars |
birthday | string | null | ✓ | YYYY-MM-DD or --MM-DD |
emails | entry[] | ✓ | Array of { value, label, isPrimary } |
phones | entry[] | ✓ | Array of { value, label, isPrimary } |
labels | string[] | ✓ | Array of label names, e.g. ["VIP", "Newsletter"] |
isFavorite | boolean | ✓ | True if the contact is starred |
isEmergency | boolean | ✓ | True if the contact is marked as an emergency contact |
bookId | string | null | ✓ | Address book CUID |
source | string | — | Origin: MANUAL, API, SYNC_CARDDAV, etc. |
createdAt | ISO 8601 | — | UTC timestamp |
updatedAt | ISO 8601 | — | UTC timestamp, updated on every write |
Rate limits
Rate limits are enforced per token using a sliding 1-hour window.
| Token scope | Requests per hour |
|---|---|
| Read-only | 1,000 |
| Read-write | 200 |
Every response includes rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 2026-06-11T15:00:00.000ZWhen the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header (seconds until the window resets).
Code examples
cURL — list contacts
curl -H "Authorization: Bearer ktx_live_your-token" \
"https://api.getkontax.com/v1/contacts?limit=20"JavaScript (fetch) — create a contact
const response = await fetch("https://api.getkontax.com/v1/contacts", {
method: "POST",
headers: {
"Authorization": "Bearer ktx_live_your-token",
"Content-Type": "application/json",
},
body: JSON.stringify({
firstName: "Jane",
lastName: "Smith",
emails: [{ value: "[email protected]", label: "work" }],
}),
});
const contact = await response.json();
console.log(contact.id);Python (requests) — paginate all contacts
import requests
TOKEN = "ktx_live_your-token"
BASE = "https://api.getkontax.com/v1"
def list_all_contacts():
contacts = []
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
r = requests.get(
f"{BASE}/contacts",
headers={"Authorization": f"Bearer {TOKEN}"},
params=params,
)
r.raise_for_status()
data = r.json()
contacts.extend(data["contacts"])
if not data["pagination"]["hasMore"]:
break
cursor = data["pagination"]["cursor"]
return contacts
all_contacts = list_all_contacts()
print(f"Fetched {len(all_contacts)} contacts")Export format (v1.0)
Kontax exports contacts in an open, documented format so your data is never locked in. One JSON document describes one contact; an archive is packaging around many of them plus their photos. The format is developed in the open — this section is the reference; the canonical spec, JSON Schemas, and a reference validator live in that repository (also mirrored under /format on this site).
The base standard is JSContact (RFC 9553): an exported contact is a JSContact Card. Everything Kontax-specific lives under the getkontax.com: vendor namespace, so a generic JSContact reader that ignores unknown properties still recovers a usable, mostly-complete contact. There is no proprietary file type — documents are plain .json, archives plain .zip; recognition is by content (the getkontax.com:formatVersion property and manifest.json), never the file extension.
| Serialization | File | Contains |
|---|---|---|
| Document | contact.json | One Card. Photo inlined as a data: URI. |
| Archive | contacts.zip | manifest.json + contacts/ + content-addressed media/ + optional vcards/ fallback. |
Document structure
A bare document is a single JSContact Card with two Kontax envelope properties. Native JSContact properties (name, emails, phones, addresses, anniversaries, …) carry the bulk of the data; vendor properties add what JSContact has no slot for. In a bare document the photo is an inline data: URI; in an archive it is a relative reference into media/.
{
"@type": "Card",
"version": "1.0", // JSContact spec version
"uid": "3b1e4c7a-2f90-4d81-9c2a-7e5b6d4f0a11",
"created": "2024-02-11T08:30:00Z",
"updated": "2026-05-19T14:02:00Z",
"getkontax.com:formatVersion": "1.0", // Kontax extension-set version
"getkontax.com:exportedAt": "2026-07-04T14:30:00Z",
"name": {
"full": "Daniel Cho",
"components": [
{ "kind": "given", "value": "Daniel" },
{ "kind": "surname", "value": "Cho" }
]
},
"emails": {
"e1": { "address": "[email protected]", "contexts": { "work": true }, "pref": 1 }
},
"phones": {
"p1": { "number": "+1 (415) 555-0132", "features": { "mobile": true }, "pref": 1 }
},
"anniversaries": {
"d1": { "kind": "birth", "date": { "@type": "PartialDate", "year": 1990, "month": 9, "day": 4 } }
},
"keywords": { "Clients": true },
"getkontax.com:labels": { "l1": { "name": "Clients", "color": "#4158f4" } },
"getkontax.com:customFields": [ { "label": "Client ID", "value": "NW-0042" } ],
"getkontax.com:favorite": true,
"media": {
"m1": {
"kind": "photo",
"uri": "data:image/png;base64, ...", // relative "media/<sha256>.png" inside an archive
"mediaType": "image/png",
"getkontax.com:sha256": "a4dd28db…045ae6"
}
}
}The complete property reference — every field, its class (must / optional / never), and its vendor shape — is in the full spec (§3). The machine-checkable form is the contact JSON Schema. A ready-to-read example is daniel-cho.json.
Archive layout
An archive is a .zip containing a manifest, one JSON document per contact, and their photos. Photos are content-addressed — media/<sha256>.<ext> — so two contacts that share a photo store it once. Contact filenames are ordinals; the document content (its uid) is the identity, not the filename.
contacts.zip
├─ manifest.json envelope + integrity table
├─ contacts/0001.json one Card per contact (photo by relative ref)
├─ contacts/0002.json
├─ media/<sha256>.jpg content-addressed photo bytes (deduplicated)
└─ vcards/contacts.vcf optional vCard 3.0 compatibility copyThe manifest.json carries an integrity table with a sha256 and byte length for every packed entry, so a truncated or tampered archive is detectable before anything is imported:
{
"@type": "getkontax.com:Archive",
"getkontax.com:formatVersion": "1.0",
"getkontax.com:exportedAt": "2026-07-04T14:30:00Z",
"counts": { "contacts": 2, "photos": 1 },
"integrity": {
"algorithm": "sha256",
"entries": [
{ "path": "contacts/0001.json", "sha256": "…", "bytes": 1180 },
{ "path": "media/3fa4c2….png", "sha256": "3fa4c2…", "bytes": 20481 }
]
}
}Every contacts/*.json declares the same formatVersion as the manifest — a mixed-version archive is invalid. Full container rules (streaming, limits, recognition) are in spec §7; the manifest schema is kontax-archive.v1.schema.json.
Versioning policy
Two independent version fields — do not conflate them:
| Field | Meaning | Changes when |
|---|---|---|
version | JSContact spec version | RFC 9553 itself revises (expected to stay 1.0). |
getkontax.com:formatVersion | The Kontax extension-set version, MAJOR.MINOR | Kontax adds or changes a getkontax.com:* property. |
A MINOR bump is additive — a new optional property; older readers must still parse the document (unknown properties are preserved, per RFC 9553 §1.7.4). A MAJOR bump removes, renames, or repurposes a property; a reader must reject a document whose major exceeds what it supports with a clear error, never a silent partial import. One JSON Schema is published per major. The current format version is 1.0.
vCard mapping
Every property maps to a native vCard property where one exists, or an X-KONTAX-* extension otherwise. The archive's optional vcards/contacts.vcf is that projection, for tools that can't read JSContact — lossy by construction (a generic reader drops the X- props); the lossless source is always contacts/. Key rows (full table in spec §6):
| Document property | vCard line |
|---|---|
name.full | FN: |
name.components | N:family;given;given2;title;credential |
emails | EMAIL;TYPE=…[,PREF]: |
phones.number | TEL;TYPE=…[,PREF]: |
anniversaries (birth) | BDAY: |
keywords (labels) | CATEGORIES: |
notes | NOTE: |
updated | REV: |
getkontax.com:favorite | X-KONTAX-FAVORITE:TRUE |
getkontax.com:customFields | X-KONTAX-CUSTOM-FIELD;X-KONTAX-LABEL=…: |
media (photo) | PHOTO;ENCODING=b;TYPE=…: |
Schemas & validator
A developer can implement a reader from the files below alone — no Kontax account needed. The reference validator is zero-dependency (Node.js ≥ 18): it validates a document or an archive against the schemas and verifies the archive integrity checksums.
| File | What it is |
|---|---|
| kontax-contact.v1.schema.json | JSON Schema for one contact document |
| kontax-archive.v1.schema.json | JSON Schema for the archive manifest |
| daniel-cho.json | Example bare document (inline photo, custom field, labels) |
| example-archive.zip | Example archive — two contacts sharing one photo |
| validate.mjs | Zero-dependency reference validator |
| spec.md | The full human-readable specification |
# validate any exported file — a renamed .zip still works (content-based)
node validate.mjs contacts.zip
node validate.mjs contact.jsonSource, issues, and the canonical spec live at github.com/getkontax/contact-format. The page and the repository always state the same formatVersion (1.0).
API questions or issues? [email protected] · Manage your tokens →