Conventions

Reference

Base URL and versioning

Every request goes to https://app.envoycrm.com/api/v1/... (a self-hosted install uses its own domain instead). v1 is part of the path from day one, even though there's only one version today - a future breaking change would ship as /api/v2 rather than changing what /api/v1 means underneath you.

Resources

contacts, companies, deals, activities, and notes support GET (list and by id), POST, PATCH, and DELETE. brands is read-only (GET only) - brand management stays an in-app concern; the API only needs brand ids to scope the other resources.

Pagination

List endpoints accept page and limit query parameters (default page=1, limit=50, maximum limit=200) and return:

{ "items": [ ... ], "total": 137 }

Errors

A consistent JSON shape across every endpoint:

{ "error": "A human-readable message", "code": "optional_machine_readable_code" }

Prefer branching on code when it's present, rather than matching against the text of error, which can change. A request that fails Zod's schema validation (a missing required field, a value of the wrong type) returns 400 with code: "FST_ERR_VALIDATION" before your request reaches any application logic at all.

Idempotency

POST requests accept an optional Idempotency-Key header. If a request is retried with the same key within 24 hours, the original response is returned unchanged rather than creating a second resource - useful for retrying a request after a dropped connection without risking a duplicate.

curl -X POST https://app.envoycrm.com/api/v1/contacts \
  -H "Authorization: Bearer envoy_live_..." \
  -H "Idempotency-Key: a-uuid-you-generate-per-logical-request" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Jane" }'

Reusing the same Idempotency-Key for a genuinely different request (a different endpoint, or a different method) returns 422 rather than silently replaying the wrong response - generate a new key per logical operation, the same way you would with Stripe's API.

Rate limits

Limits apply per API key, not per IP address, and reset every minute. The current limit and remaining budget are returned on every response:

x-ratelimit-limit: 300
x-ratelimit-remaining: 297
x-ratelimit-reset: 60

Exceeding the limit returns 429. Limits scale with your plan:

PlanRequests / minute
Starter120
Growth (and trial)300
Agency600

Next