# Hubzone API documentation

> Connect your own systems to Hubzone with one API key: read and write contacts, companies, deals, tickets, products and orders, log phone calls, send your app's events, and let AI assistants work with your records.

Base URL: https://hubzone.app/api
OpenAPI: https://hubzone.app/openapi.json

Connect your own systems to Hubzone with one API key: read and write contacts, companies, deals, tickets, products and orders, log phone calls, send your app's events, and let AI assistants work with your records.

Base URL: `https://hubzone.app/api`

- **Records**: List, search, read, create and change records of seven kinds.
- **Phone calls**: Log calls from phones and phone systems; they land on the contact's timeline.
- **Custom events**: Send sign-ups, purchases and logins from your app and see them per person.
- **Scoped keys**: Each key has only the permissions you give it, optional expiry and IP list, and a call log.
- **MCP and AI**: Connect Claude and other assistants; download all the documentation as Markdown.
- **Safe by design**: Keys act inside their own workspace, as the person who made them; limits protect you from loops.


# Getting started

Three steps to your first request.

### 1. Make an API key

Sign in, open Settings > Developers and make a key. Choose only the permissions it needs (for example contacts.read). The key is shown once; keep it somewhere safe. A key works inside its own workspace, as the person who made it, so it can never do more than that person may.

### 2. Send your first request

Put the key in the Authorization header as a Bearer token:

```bash
curl https://hubzone.app/api/open/v1/me \
  -H "Authorization: Bearer hzk_your_key_here"
```

### 3. Read the answer

Every answer is JSON. This one tells you what the key may do and its limits:

```json
{
  "keyId": "3f0c8f0e-2b1a-4c55-9a38-0d6c1c9d7e11",
  "scopes": ["contacts.read", "contacts.write"],
  "limits": { "get": { "label": "Read one record", "perMinute": 600 } },
  "allScopes": 14
}
```

### Then: list and create records

```bash
# the properties (fields) of contacts, with their names and types
curl https://hubzone.app/api/open/v1/properties/contacts -H "Authorization: Bearer $HUBZONE_KEY"

# create a contact
curl -X POST https://hubzone.app/api/open/v1/objects/contacts \
  -H "Authorization: Bearer $HUBZONE_KEY" -H "content-type: application/json" \
  -d '{"properties":{"first_name":"Ada","last_name":"Lovelace","email":"ada@example.com"}}'

# the first 25 contacts that mention "ada"
curl "https://hubzone.app/api/open/v1/objects/contacts?limit=25&q=ada" -H "Authorization: Bearer $HUBZONE_KEY"
```

> The kinds of record are: contacts, companies, deals, tickets, products, orders and calls. Properties are always sent and returned under their property names (see "List properties").

# Authentication

Keys, permissions (scopes) and how to keep a key safe.

Every call carries an API key in the Authorization header. A key cannot be sent in the body or the query string.

```http
Authorization: Bearer hzk_your_key_here
```

### Permissions (scopes)

Give each key only what it needs. A permission is a kind of record and a mode: <type>.read or <type>.write. A call without the right permission answers 403.

| Permission | Allows |
| --- | --- |
| contacts.read | Read contacts: list, find, read one, list properties |
| contacts.write | Create and change contacts |
| companies.read | Read companies: list, find, read one, list properties |
| companies.write | Create and change companies |
| deals.read | Read deals: list, find, read one, list properties |
| deals.write | Create and change deals |
| tickets.read | Read tickets: list, find, read one, list properties |
| tickets.write | Create and change tickets |
| products.read | Read products: list, find, read one, list properties |
| products.write | Create and change products |
| orders.read | Read orders: list, find, read one, list properties |
| orders.write | Create and change orders |
| calls.read | Read calls: list, find, read one, list properties |
| calls.write | Create and change calls |

### Good practice

- Keep keys on your server. Never put one in a web page, a mobile app or a public repository.
- Make a separate key for every system, with the smallest set of permissions. Start with read only.
- Revoke a key at any time under Settings > Developers; the last calls of each key are listed there.
- A key can have an expiry date and a list of allowed IP addresses.
- Wrong keys are limited per address, so guessing does not work.

### Authentication errors

```json
// 401: no key, a wrong key, or a revoked or expired key
{ "statusCode": 401, "message": "..." }

// 403: the key is valid but lacks the permission (or the address is not allowed)
{ "statusCode": 403, "message": "..." }
```

# Rate limits

How many calls a key may make, and how to read where you stand.

Calls are counted per key and per kind of call, per minute, and there is an overall limit per key too. Every answer says where you stand.

| Kind of call | Name | Per minute |
| --- | --- | --- |
| Read one record | get | 600 |
| List records | list | 240 |
| Change a record | update | 240 |
| Search records | search | 120 |
| Create a record | create | 120 |
| Key information | me | 60 |
| List properties | properties | 60 |

### Headers

```http
X-RateLimit-Limit: 240
X-RateLimit-Remaining: 238
X-RateLimit-Reset: 1794313700
```

| Header | Meaning |
| --- | --- |
| X-RateLimit-Limit | Calls allowed in the window for this kind of call. |
| X-RateLimit-Remaining | Calls left in the window (the smaller of the kind's and the key's overall). |
| X-RateLimit-Reset | When the window resets, in seconds. |
| Retry-After | On 429: seconds to wait before trying again. |

### When the limit is passed

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 20

{ "statusCode": 429, "message": "Too many requests: slow down" }
```

Wait for Retry-After, then try again. Back off for longer each time a retry fails. A program that loops over records should read X-RateLimit-Remaining and slow itself down before it hits 429.

# Errors

Standard HTTP status codes and a JSON body that says what went wrong.

```json
{
  "statusCode": 400,
  "message": "properties.email: must be a valid e-mail address",
  "error": "Bad Request"
}
```

| Code | Meaning |
| --- | --- |
| 200 | OK. |
| 201 | Created. |
| 400 | The request is not valid. The message says why (a property name that does not exist, a wrong type, a value out of range). |
| 401 | No key, a wrong key, or a revoked or expired key. |
| 403 | The key lacks the permission, or the address is not allowed. |
| 404 | Not found, or it belongs to another workspace. |
| 409 | Conflict with the current state. |
| 429 | Too many calls. Wait for Retry-After. |
| 500 | Something unexpected on our side. Try again. |

> Records of other workspaces never show up: a record you may not see answers 404, as if it did not exist.

# Custom events

Send things that happen in your own app (a sign-up, a purchase, a login) and see them in Hubzone.

Define an event under Data management > Event management: a name and typed properties (text, number, yes/no, date). Each event has its own key, shown once. Your system sends every occurrence to that key with the e-mail address of the person. The occurrence is tied to the contact with that address and shows up on the Occurrences and Analyze tabs.

```bash
curl -X POST https://hubzone.app/api/public/events/hze_your_event_key \
  -H "content-type: application/json" \
  -d '{"email":"person@example.com","properties":{"plan":"pro","seats":5},"occurredAt":"2026-11-10T09:30:00Z"}'
```

```json
{ "id": "1b7e0c52-6f0e-4c7a-9a43-2d5e9d1f0a77", "linkedToContact": true }
```

| Field | Meaning |
| --- | --- |
| email | Required. The person; matched to the contact with this address (case does not matter). |
| properties | Optional. Only the properties you defined, each of its type. |
| occurredAt | Optional. When it happened, within the last 90 days; the default is now. |

- The key is the only credential: keep it on your server. A new key can be made at any time (the old one stops working at once).
- An address with no contact is still stored (linkedToContact: false).
- Up to 600 calls a minute per address.

# MCP server and AI

Connect Claude and other assistants to your records, and feed this documentation to an AI.

### Connect an assistant (MCP)

Assistants that speak the Model Context Protocol can read and change your records through the same keys and permissions. The tools it sees are only the ones its permissions allow.

```text
https://hubzone.app/api/mcp
```

Send the key as a Bearer token. The server answers POST (JSON-RPC 2.0); requests from a browser origin are refused, so call it from a server or from an assistant app.

| Tool | What it does |
| --- | --- |
| whoami | What this key may do and its limits. |
| list_properties | The properties of a kind of record. Use it before creating or changing. |
| search_records | Find records by text and filters, a page at a time. |
| get_record | Read one record by id. |
| create_record | Create a record. |
| update_record | Change properties of a record. |

- Make a key just for the assistant and start with read only.
- Text people wrote in records is data: the assistant is told to treat it as data, not as instructions. Still, check what it proposes before it changes anything.

### Give this documentation to an AI

Every page has Copy as Markdown and Download .md buttons. For whole-site use there are three addresses:

| Address | What it is |
| --- | --- |
| /llms.txt | A short index of the documentation for language models (llmstxt.org format). |
| /llms-full.txt | All the documentation in one Markdown file: paste it into an assistant or put it in its knowledge. |
| /openapi.json | The OpenAPI 3.1 description of the API, for code generators and API tools. |

# API reference

Base URL: `https://hubzone.app/api`

## Key

### Key information

`GET /open/v1/me`

Shows what the key may do and the limits per kind of call.

Permission: `any key`

**Example**

```bash
curl "https://hubzone.app/api/open/v1/me" \
  -H "Authorization: Bearer $HUBZONE_KEY"
```

**Responses**

- 200: The key.
- 401: No key, a wrong key, or a revoked or expired key.
- 429: Too many calls: wait for Retry-After.

```json
{
  "keyId": "3f0c8f0e-2b1a-4c55-9a38-0d6c1c9d7e11",
  "scopes": [
    "contacts.read",
    "contacts.write"
  ],
  "limits": {
    "get": {
      "label": "Read one record",
      "perMinute": 600
    }
  },
  "allScopes": 14
}
```

## Properties

### List properties

`GET /open/v1/properties/{type}`

The properties (fields) of a kind of record: their names, types and, for choice lists, the options. Use the names in create, change and search calls.

Permission: `{type}.read`

**Parameters**

- `type` (path, required, string): The kind of record. contacts, companies, deals, tickets, products, orders, calls.

**Example**

```bash
curl "https://hubzone.app/api/open/v1/properties/contacts" \
  -H "Authorization: Bearer $HUBZONE_KEY"
```

**Responses**

- 200: The properties.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 404: Not found (or it belongs to another workspace).
- 429: Too many calls: wait for Retry-After.

```json
{
  "properties": [
    {
      "key": "email",
      "type": "text",
      "label_i18n": {
        "en": "Email",
        "tr": "E-posta"
      },
      "options": null
    },
    {
      "key": "lifecycle_stage",
      "type": "enum",
      "label_i18n": {
        "en": "Lifecycle stage",
        "tr": "Yaşam döngüsü aşaması"
      },
      "options": [
        {
          "value": "lead",
          "label_i18n": {
            "en": "Lead"
          }
        }
      ]
    }
  ]
}
```

## Records

### List or find records

`GET /open/v1/objects/{type}`

Records of one kind, newest activity first, a page at a time. "q" finds records whose text properties contain the text. For the next page send the "after" value from the answer.

Permission: `{type}.read`

**Parameters**

- `type` (path, required, string): The kind of record. contacts, companies, deals, tickets, products, orders, calls.
- `limit` (query, integer 1-100): Records per page (default 25).
- `after` (query, string): The cursor of the next page, from the previous answer.
- `q` (query, string): Text to find.

**Example**

```bash
curl "https://hubzone.app/api/open/v1/objects/contacts?limit=25&q=ada" \
  -H "Authorization: Bearer $HUBZONE_KEY"
```

**Responses**

- 200: A page of records. "paging.next.after" is null on the last page.
- 400: The request is not valid; the message says why.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 429: Too many calls: wait for Retry-After.

```json
{
  "results": [
    {
      "id": "8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12",
      "objectType": "contacts",
      "properties": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "ada@example.com"
      },
      "ownerId": "5d1f0f3e-7e0a-4b8e-9a50-aa14d3c9b2f1",
      "isPrivate": false,
      "createdAt": "2026-11-10T09:30:00.000Z",
      "updatedAt": "2026-11-10T09:30:00.000Z",
      "pipelineId": null,
      "stageId": null,
      "stageChangedAt": null,
      "lastActivityAt": null
    }
  ],
  "paging": {
    "next": {
      "after": "eyJ2IjoxfQ"
    }
  },
  "total": {
    "value": 1,
    "capped": false
  }
}
```

### Search records

`POST /open/v1/objects/{type}/search`

Search with filters, sorting and a text query. A record matches when it satisfies every filter in "filters". Operators: eq, neq, gt, gte, lt, lte, contains, starts_with, in, exists, not_exists.

Permission: `{type}.read`

**Parameters**

- `type` (path, required, string): The kind of record. contacts, companies, deals, tickets, products, orders, calls.

**Request body** (JSON): All fields are optional.

```json
{
  "query": "renewal",
  "filters": [
    {
      "property": "amount",
      "op": "gte",
      "value": 1000
    }
  ],
  "sort": {
    "property": "amount",
    "direction": "desc"
  },
  "limit": 25
}
```

**Example**

```bash
curl -X POST "https://hubzone.app/api/open/v1/objects/contacts/search" \
  -H "Authorization: Bearer $HUBZONE_KEY" \
  -H "content-type: application/json" \
  -d '{"query":"renewal","filters":[{"property":"amount","op":"gte","value":1000}],"sort":{"property":"amount","direction":"desc"},"limit":25}'
```

**Responses**

- 200: A page of records, as in the list call.
- 400: The request is not valid; the message says why.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 429: Too many calls: wait for Retry-After.

```json
{
  "results": [
    {
      "id": "8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12",
      "objectType": "contacts",
      "properties": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "ada@example.com"
      },
      "ownerId": "5d1f0f3e-7e0a-4b8e-9a50-aa14d3c9b2f1",
      "isPrivate": false,
      "createdAt": "2026-11-10T09:30:00.000Z",
      "updatedAt": "2026-11-10T09:30:00.000Z",
      "pipelineId": null,
      "stageId": null,
      "stageChangedAt": null,
      "lastActivityAt": null
    }
  ],
  "paging": null,
  "total": {
    "value": 1,
    "capped": false
  }
}
```

### Read one record

`GET /open/v1/objects/{type}/{id}`

One record with all its properties.

Permission: `{type}.read`

**Parameters**

- `type` (path, required, string): The kind of record. contacts, companies, deals, tickets, products, orders, calls.
- `id` (path, required, string (uuid)): The id of the record.

**Example**

```bash
curl "https://hubzone.app/api/open/v1/objects/contacts/8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12" \
  -H "Authorization: Bearer $HUBZONE_KEY"
```

**Responses**

- 200: The record.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 404: Not found (or it belongs to another workspace).
- 429: Too many calls: wait for Retry-After.

```json
{
  "id": "8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12",
  "objectType": "contacts",
  "properties": {
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com"
  },
  "ownerId": "5d1f0f3e-7e0a-4b8e-9a50-aa14d3c9b2f1",
  "isPrivate": false,
  "createdAt": "2026-11-10T09:30:00.000Z",
  "updatedAt": "2026-11-10T09:30:00.000Z",
  "pipelineId": null,
  "stageId": null,
  "stageChangedAt": null,
  "lastActivityAt": null
}
```

### Create a record

`POST /open/v1/objects/{type}`

Creates a record. Send properties under the names from "List properties". Only properties, ownerId, stageId and pipelineId are accepted. The key's person owns the record unless ownerId says otherwise. To log a phone call use the calls endpoint instead.

Permission: `{type}.write`

**Parameters**

- `type` (path, required, string): The kind of record. contacts, companies, deals, tickets, products, orders, calls.

**Request body** (JSON): "properties" is required.

```json
{
  "properties": {
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com"
  }
}
```

**Example**

```bash
curl -X POST "https://hubzone.app/api/open/v1/objects/contacts" \
  -H "Authorization: Bearer $HUBZONE_KEY" \
  -H "content-type: application/json" \
  -d '{"properties":{"first_name":"Ada","last_name":"Lovelace","email":"ada@example.com"}}'
```

**Responses**

- 201: The new record.
- 400: The request is not valid; the message says why.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 429: Too many calls: wait for Retry-After.

```json
{
  "id": "8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12",
  "objectType": "contacts",
  "properties": {
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com"
  },
  "ownerId": "5d1f0f3e-7e0a-4b8e-9a50-aa14d3c9b2f1",
  "isPrivate": false,
  "createdAt": "2026-11-10T09:30:00.000Z",
  "updatedAt": "2026-11-10T09:30:00.000Z",
  "pipelineId": null,
  "stageId": null,
  "stageChangedAt": null,
  "lastActivityAt": null
}
```

### Change a record

`PATCH /open/v1/objects/{type}/{id}`

Changes the properties you send; the others stay as they are. Send a property as null to clear it. A deal or ticket moves to another stage with stageId.

Permission: `{type}.write`

**Parameters**

- `type` (path, required, string): The kind of record. contacts, companies, deals, tickets, products, orders, calls.
- `id` (path, required, string (uuid)): The id of the record.

**Request body** (JSON): Send only what changes.

```json
{
  "properties": {
    "job_title": "Mathematician"
  }
}
```

**Example**

```bash
curl -X PATCH "https://hubzone.app/api/open/v1/objects/contacts/8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12" \
  -H "Authorization: Bearer $HUBZONE_KEY" \
  -H "content-type: application/json" \
  -d '{"properties":{"job_title":"Mathematician"}}'
```

**Responses**

- 200: The changed record.
- 400: The request is not valid; the message says why.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 404: Not found (or it belongs to another workspace).
- 429: Too many calls: wait for Retry-After.

```json
{
  "id": "8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12",
  "objectType": "contacts",
  "properties": {
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com",
    "job_title": "Mathematician"
  },
  "ownerId": "5d1f0f3e-7e0a-4b8e-9a50-aa14d3c9b2f1",
  "isPrivate": false,
  "createdAt": "2026-11-10T09:30:00.000Z",
  "updatedAt": "2026-11-10T09:30:00.000Z",
  "pipelineId": null,
  "stageId": null,
  "stageChangedAt": null,
  "lastActivityAt": null
}
```

## Calls

### Log a phone call

`POST /open/v1/calls`

For phones and phone systems: the call is tied to the contact with that phone number and shows on their timeline. Audio is never sent. With externalId a call you send twice is stored once.

Permission: `calls.write`

**Request body** (JSON): phoneNumber is required. direction: inbound or outbound. outcome: busy, connected, left_live_message, left_voicemail, meeting_booked, no_answer or wrong_number.

```json
{
  "phoneNumber": "+90 532 000 00 00",
  "direction": "outbound",
  "startedAt": "2026-11-10T09:30:00Z",
  "durationSeconds": 184,
  "outcome": "connected",
  "note": "Asked for a quote",
  "externalId": "pbx-7781"
}
```

**Example**

```bash
curl -X POST "https://hubzone.app/api/open/v1/calls" \
  -H "Authorization: Bearer $HUBZONE_KEY" \
  -H "content-type: application/json" \
  -d '{"phoneNumber":"+90 532 000 00 00","direction":"outbound","startedAt":"2026-11-10T09:30:00Z","durationSeconds":184,"outcome":"connected","note":"Asked for a quote","externalId":"pbx-7781"}'
```

**Responses**

- 201: The call and the contact it was tied to (linked is false when no contact has that number).
- 400: The request is not valid; the message says why.
- 401: No key, a wrong key, or a revoked or expired key.
- 403: The key lacks the permission for this call.
- 429: Too many calls: wait for Retry-After.

```json
{
  "id": "c1d2e3f4-0000-4000-8000-000000000001",
  "activityId": "a1b2c3d4-0000-4000-8000-000000000002",
  "contactId": "8a3b2f64-1c9d-4e05-b7a1-3f2c9d0e4a12",
  "linked": true,
  "duplicate": false
}
```

## Events

### Send a custom event

`POST /public/events/{eventKey}`

Records one occurrence of a custom event you defined. No Authorization header: the event key in the address is the credential. See the Custom events guide.

Permission: `event key` (no Authorization header)

**Parameters**

- `eventKey` (path, required, string): The key of the event (starts with hze_), shown once when you make the event.

**Request body** (JSON): email is required.

```json
{
  "email": "person@example.com",
  "properties": {
    "plan": "pro",
    "seats": 5
  },
  "occurredAt": "2026-11-10T09:30:00Z"
}
```

**Example**

```bash
curl -X POST "https://hubzone.app/api/public/events/hze_your_event_key" \
  -H "content-type: application/json" \
  -d '{"email":"person@example.com","properties":{"plan":"pro","seats":5},"occurredAt":"2026-11-10T09:30:00Z"}'
```

**Responses**

- 201: Stored. linkedToContact says whether a contact has that e-mail address.
- 400: The request is not valid; the message says why.
- 404: Unknown event key.
- 429: Too many calls: wait for Retry-After.

```json
{
  "id": "1b7e0c52-6f0e-4c7a-9a43-2d5e9d1f0a77",
  "linkedToContact": true
}
```

# Changelog

## v1.0.0 (2026-11-10): The Hubzone API is live

Records of contacts, companies, deals, tickets, products and orders can be listed, searched, read, created and changed with scoped keys. Phone calls can be logged. Custom events can be sent with an event key. The MCP server lets assistants such as Claude use the same keys. Limits are per key and per kind of call, with X-RateLimit headers.
