Developer documentation

Kontax API v1

Read and write your contacts programmatically. All API requests are authenticated with a Bearer token created in Settings → Developer.

Base URLhttps://api.getkontax.com/v1

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-here

Tokens 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.

GET/contacts

List contacts. Returns up to 100 per page, ordered by full name.

Query parameters

ParameterTypeDefaultDescription
qstringSearch by name, company, or email
limitinteger50Results per page (max 100)
cursorstringPagination cursor from previous response
bookIdstringFilter to a specific address book
archivedbooleanfalseReturn 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 }
}
POST/contacts

Create a contact. Requires a read-write token. At least one of firstName, lastName, fullName, or company is required.

Request body (JSON)

FieldTypeDescription
firstNamestringGiven name (max 80 chars)
lastNamestringFamily name (max 80 chars)
fullNamestringOverride the derived full name (max 200 chars)
companystringCompany or organisation (max 120 chars)
jobTitlestringJob title (max 120 chars)
notesstringFree-text notes (max 10,000 chars)
birthdaystringYYYY-MM-DD or --MM-DD (year unknown)
emailsarrayUp to 10 email entries — see entry format below
phonesarrayUp to 10 phone entries — see entry format below
bookIdstringCUID 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.

GET/contacts/:id

Fetch 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.

PUT/contacts/:id

Update 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.

DELETE/contacts/:id

Archive 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.

StatusError codeMeaning
400VALIDATION_ERRORRequest body is invalid. See details field.
400INVALID_JSONRequest body is not valid JSON.
401UNAUTHENTICATEDAuthorization header is missing or malformed.
401INVALID_TOKENToken is invalid, expired, or revoked.
403FORBIDDENRead-only token attempted a write operation.
403LIMIT_REACHEDContact limit for your plan has been reached.
404NOT_FOUNDContact not found or belongs to another user.
429RATE_LIMITEDToo many requests. See X-RateLimit-* headers.
500INTERNAL_ERRORUnexpected server error.

Field reference

All fields returned by the API. Write endpoints accept a subset.

FieldTypeWritableNotes
idstringCUID, assigned on creation
firstNamestring | nullMax 80 chars
lastNamestring | nullMax 80 chars
fullNamestringDerived from parts if omitted; required indirectly
companystring | nullMax 120 chars
jobTitlestring | nullMax 120 chars
notesstring | nullMax 10,000 chars
birthdaystring | nullYYYY-MM-DD or --MM-DD
emailsentry[]Array of { value, label, isPrimary }
phonesentry[]Array of { value, label, isPrimary }
labelsstring[]Array of label names, e.g. ["VIP", "Newsletter"]
isFavoritebooleanTrue if the contact is starred
isEmergencybooleanTrue if the contact is marked as an emergency contact
bookIdstring | nullAddress book CUID
sourcestringOrigin: MANUAL, API, SYNC_CARDDAV, etc.
createdAtISO 8601UTC timestamp
updatedAtISO 8601UTC timestamp, updated on every write

Rate limits

Rate limits are enforced per token using a sliding 1-hour window.

Token scopeRequests per hour
Read-only1,000
Read-write200

Every response includes rate limit headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 2026-06-11T15:00:00.000Z

When 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.

SerializationFileContains
Documentcontact.jsonOne Card. Photo inlined as a data: URI.
Archivecontacts.zipmanifest.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 copy

The 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:

FieldMeaningChanges when
versionJSContact spec versionRFC 9553 itself revises (expected to stay 1.0).
getkontax.com:formatVersionThe Kontax extension-set version, MAJOR.MINORKontax 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 propertyvCard line
name.fullFN:
name.componentsN:family;given;given2;title;credential
emailsEMAIL;TYPE=…[,PREF]:
phones.numberTEL;TYPE=…[,PREF]:
anniversaries (birth)BDAY:
keywords (labels)CATEGORIES:
notesNOTE:
updatedREV:
getkontax.com:favoriteX-KONTAX-FAVORITE:TRUE
getkontax.com:customFieldsX-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.

FileWhat it is
kontax-contact.v1.schema.jsonJSON Schema for one contact document
kontax-archive.v1.schema.jsonJSON Schema for the archive manifest
daniel-cho.jsonExample bare document (inline photo, custom field, labels)
example-archive.zipExample archive — two contacts sharing one photo
validate.mjsZero-dependency reference validator
spec.mdThe full human-readable specification
# validate any exported file — a renamed .zip still works (content-based)
node validate.mjs contacts.zip
node validate.mjs contact.json

Source, 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 →