> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goeva.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Eva API for your CRM

> Connect your system to Eva contacts and opportunities with a revocable account key.

## Requirements

You need the account owner or admin role and a system that can make HTTPS requests. Each key belongs to one Eva account.

## What syncs

| Direction | Data |
| - | - |
| Your system → Eva | Create or update contacts and opportunities, including registered custom fields. |
| Eva → your system | Read contacts, opportunities, owners, fields, and pipelines. Configure **Custom webhook** separately to receive change notifications. |

You can also search records and read or add opportunity notes.

The API does not delete records, create fields, or assign your own `external_id`. Save Eva's `id` in your system. API calls do not consume credits.

## Connect

1. Open **Settings → Integrations → Eva API** and choose **Create key**.
2. Name the system and choose **Read only** (`crm:read`) or **Read and write** (`crm:write`). A write key can also read.
3. Copy the `eva_sk_…` key immediately. Eva shows its complete value once. Store it as a server secret, never in a public web page.

Send `Authorization: Bearer <key>` and a real `User-Agent` so Cloudflare accepts the request:

```bash theme={null}
curl 'https://api.goeva.ai/api/v1/public/crm/contacts?limit=50' \
  -H 'Authorization: Bearer eva_sk_YOUR_KEY' \
  -H 'User-Agent: MyCRM/1.0 contact@mycompany.com'
```

Create or update a contact by email or phone:

```bash theme={null}
curl -X POST 'https://api.goeva.ai/api/v1/public/crm/contacts' \
  -H 'Authorization: Bearer eva_sk_YOUR_KEY' \
  -H 'User-Agent: MyCRM/1.0 contact@mycompany.com' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Ana","email":"ana@example.com","source":"Meta · Campaña octubre","fields":{"region":"West"}}'
```

The response says `created: true` for a new contact and `false` for a matching one. Use `GET /fields?entity=contact` for valid keys; add `entity=opportunity&pipeline_id=<id>` for opportunity fields.

Top-level `source` is the contact origin; do not put it in `fields`. Eva trims it, accepts up to 128 characters, and returns `422` for a blank string. On POST, it fills a matched contact's origin only when that origin is empty; `null` leaves it unchanged. `PATCH /contacts/{id}` with `{"source":"Trade show"}` replaces the origin, and `{"source":null}` clears it. Contact responses include `source`. Any other text is accepted, including the panel's origin options such as `website`, `referral` or `meta_ads`. Only these case-insensitive system values return `422 invalid_source`: `capture`, `personal_whatsapp_import`, `tokko_import`, `shopify`, and any value starting with `connected:`. If the stored origin is one of those system values, `PATCH` with `source` returns `422 invalid_source` and `POST` leaves it unchanged. An incoming WhatsApp message preserves an origin that is already set.

## Endpoints and test

All paths start with `/api/v1/public/crm`. Use `GET /contacts`, `GET /contacts/{id}`, `POST /contacts`, `PATCH /contacts/{id}`, the corresponding `/opportunities` routes, and `GET /fields`, `GET /owners`, `GET /pipelines`. Lists accept ISO 8601 `updated_since`, a 1–100 `limit`, and the opaque `cursor` from the previous page. Opportunities also accept `pipeline_id` and `stage_id`. On `POST /opportunities`, send `Idempotency-Key: <unique-identifier>` and reuse it for retries; Eva honors a replay of the same request for 30 days. `POST /contacts` needs an email or a phone: if either already belongs to a contact, Eva updates that contact instead of creating another.

Search contact names, emails, phones, and fields enabled for search; search opportunity titles and linked contacts. `q` is trimmed and must contain 2–100 characters, `type` is `contact` or `opportunity`, and `limit` is 1–25. The response contains `type` and `data` with the same fields as public contact or opportunity reads:

```bash theme={null}
curl 'https://api.goeva.ai/api/v1/public/crm/search?q=Ana&type=contact&limit=10' \
  -H 'Authorization: Bearer eva_sk_YOUR_KEY' \
  -H 'User-Agent: MyCRM/1.0 contact@mycompany.com'

curl 'https://api.goeva.ai/api/v1/public/crm/search?q=Expansion&type=opportunity&limit=10' \
  -H 'Authorization: Bearer eva_sk_YOUR_KEY' \
  -H 'User-Agent: MyCRM/1.0 contact@mycompany.com'
```

Read an opportunity's notes with `GET /opportunities/{id}/notes?limit=50`; pass `next_cursor` back as `cursor` for the next page. Newest notes come first. `author.kind` is `api_key`, `member`, or `system`, and `author.name` shows the key or member name.

```bash theme={null}
curl 'https://api.goeva.ai/api/v1/public/crm/opportunities/22222222-2222-4222-8222-222222222222/notes?limit=50' \
  -H 'Authorization: Bearer eva_sk_YOUR_KEY' \
  -H 'User-Agent: MyCRM/1.0 contact@mycompany.com'

curl -X POST 'https://api.goeva.ai/api/v1/public/crm/opportunities/22222222-2222-4222-8222-222222222222/notes' \
  -H 'Authorization: Bearer eva_sk_YOUR_KEY' \
  -H 'User-Agent: MyCRM/1.0 contact@mycompany.com' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: crm-note-123' \
  -d '{"text":"Call on Tuesday"}'
```

Notes require nonblank text (up to 10,000 characters) and do not accept attachments. Reuse the idempotency key on retries: Eva returns the same note with `replayed: true` for 30 days; a different request with that key returns `409`.

Start with `GET /owners`: `200` confirms the key works and lists active members; `401` means the key is absent, invalid or revoked; `403` means the operation needs another scope.

## Change notifications

Configure **Custom webhook** in Integrations with your HTTPS receiver and signing secret. Deliveries include `X-Eva-Event-Id` and, when a signing secret exists, `X-Eva-Timestamp` and `X-Eva-Signature-256`. Verify the signature before processing the body and deduplicate by event ID. Each record's events carry an increasing `revision` and can arrive out of order: keep the one with the highest `revision` for each record. Changes made through the API arrive with `source: "api"`. The webhook connection is separate from your API key; revoking the key does not disconnect it.

## Eva appointments in your CRM

`GET /appointments` lists visits linked to a contact or opportunity, and `GET /appointments/{id}` reads one. Each record has Eva's event ID, `eva_contact_id` and `eva_opportunity_id` (either may be `null`, but at least one is valid), UTC start and end, timezone, location, owner, `status` (`active` or `cancelled`), and `updated_at`; it contains no title or notes. For example, a contact-only visit has `"eva_contact_id": "44444444-4444-4444-8444-444444444444", "eva_opportunity_id": null`. A cancellation remains readable after Eva deletes the visit. The list accepts `limit` from 1 to 100, timezone-aware `updated_since`, and the previous page's `cursor`. Scan all pages and overlap incremental reads by a few minutes. These routes are read-only: your system cannot edit Eva appointments.

To receive notices, enable **Appointments → outbound** on **Custom webhook**. It starts off and requires a signing secret. `appointment.upserted` carries the same object as `GET /appointments/{id}`. `status: "cancelled"` means mark the visit cancelled in your CRM. Verify the HMAC-SHA256 signature over the exact bytes of `<X-Eva-Timestamp>.<body>`, deduplicate by `X-Eva-Event-Id`, and apply only the highest revision for each visit. Recover missed notices through the paginated list; enabling the webhook does not replay historical visits.

## Revoke and errors

Choose **Revoke** next to the key in **Eva API**. Its next call returns `401`. Existing records remain. To change permissions, create another key, switch your system, then revoke the old one.

| HTTP status | Action |
| - | - |
| 401 | Check the key and its state. |
| 403 | Use a key with the required scope. |
| 404 | Check the ID; it may belong to another account. |
| 409 | Check the idempotency key and retry body. |
| 422 | Correct the named field, owner, or stage. |
| 429 | Wait for `Retry-After` seconds. |
| 503 | Retry later; rate-limit verification is unavailable. |

## Privacy and limits

Each key accesses only its account. Eva stores a hash of the secret plus its name, scopes, and last-use time. The limit is 120 requests per minute per key. Use pagination and `updated_since` for incremental sync rather than rereading the whole account.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.