REST resources
Conventional CRUD endpoints for the records most integrations sync — contacts, deals and tasks — plus webhook subscriptions and an authenticated whoami. Base URL https://app.chirply.io; every request carries Authorization: Bearer <token>.
Typed routes or actions?
Use these REST resources when you want stable list/get/create/update/delete semantics over the core CRM records — they behave like every CRUD API you’ve integrated. Everything else the platform does (sending, automations, invoices, calendars, AI — thousands of operations) lives in the actions catalog, one POST /api/v1/actions/<name> per operation. The two share authentication, envelope and rate limits, and read the same records.
Envelope & pagination
Success is { "data": … }; a list adds { "pagination": { "limit", "offset", "total", "has_more" } }. Failure is { "error": { "code", "message" } } with the category in the HTTP status.
limit— page size, 1–100, default 50. Values outside the range are clamped, not rejected.offset— rows to skip, ≥ 0, default 0.q— free-text search (case-insensitive substring) over the columns listed per resource below.status— exact-match filter, on resources that have a status field (deals and tasks; contacts have no status).
GET requires the credential’s read scope; POST, PATCH and DELETE require write — a missing scope is a 403 forbidden. Unknown keys in a request body are ignored; a wrongly-typed known field is a 400 invalid_field naming the field. Responses carry rate-limit headers — see rate limits.
Lists sort newest-first by created_at. PATCH is partial: send only the fields you’re changing. DELETE returns { "data": { "id", "deleted": true } }, and a miss on any item route is a 404 not_found.
GET /api/v1 — whoami
The cheapest way to verify a credential: it returns the workspace the token acts for, its scopes, and where everything else lives.
curl https://app.chirply.io/api/v1 -H "Authorization: Bearer chp_live_…"
→ 200
{
"data": {
"api": "chirply",
"version": "v1",
"org_id": "…", // the workspace this credential acts for
"scopes": ["read", "write"],
"resources": ["contacts", "deals", "tasks"],
"actions": {
"total": 2000, // live count of actions this key can run
"domains": ["contacts", "deals", "…"],
"catalog_url": "/api/v1/actions",
"run_url": "/api/v1/actions/{name}"
},
"support": { "file_bug": "…", "request_feature": "…" },
"webhooks": { "list_url": "/api/v1/webhooks", "…": "…" },
"mcp_url": "/api/mcp"
}
}Contacts
| GET /api/v1/contacts | List. Searches with ?q= across first_name, last_name, business_name, facebook_id, email, phone. |
| POST /api/v1/contacts | Create. No field is required, but email, phone and facebook_id are each unique per workspace (409 on a duplicate). Creating over the API fires contact_created automations. |
| GET /api/v1/contacts/{id} | Fetch one. |
| PATCH /api/v1/contacts/{id} | Partial update. The same uniqueness rules apply. |
| DELETE /api/v1/contacts/{id} | Delete. |
Writable fields
| Field | Type | Required | Notes |
|---|---|---|---|
| first_name | string | optional | |
| last_name | string | optional | |
| business_name | string | optional | |
| string | optional | Trimmed and lowercased on save. One contact per email address per workspace — a duplicate is a 409 naming the existing contact's id. | |
| phone | string | optional | Normalized to E.164 whatever spelling you post. One contact per phone number per workspace — a duplicate is a 409 naming the existing contact's id. |
| facebook_id | string | optional | Also unique per workspace; a duplicate is a 409 naming the existing contact's id. |
| website | string | optional | |
| title | string | optional | Job title. |
| birthday | string | optional | |
| source | string | optional | Where this contact came from. |
| source_detail | object | optional | Per-source specifics — which site, which search, which file. |
| lifecycle | string | optional | Lifecycle stage. |
| company_id | uuid | optional | |
| owner_id | uuid | optional | |
| notes | string | optional | |
| address | object | optional | |
| custom | object | optional | Custom field values. |
A duplicate is a 409 duplicate on purpose, never a silent merge — the message names the existing contact’s id so you can PATCH it deliberately. Reads also return, among others: id, org_id, is_customer, customer_since, email_validation_status, created_at, updated_at.
curl -X POST https://app.chirply.io/api/v1/contacts \
-H "Authorization: Bearer chp_live_…" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+15551234567"
}'
→ 201 { "data": { "id": "…", "first_name": "Ada", … } }
→ 409 { "error": { "code": "duplicate", "message": "Contact <id> in this org already has the phone number +15551234567." } }Deals
| GET /api/v1/deals | List. ?q= searches title; ?status= filters by open, won, lost or abandoned. |
| POST /api/v1/deals | Create. title is required; pipeline_id defaults to the workspace's default pipeline. |
| GET /api/v1/deals/{id} | Fetch one. |
| PATCH /api/v1/deals/{id} | Partial update. Moving stage_id is recorded in the Activity Log. |
| DELETE /api/v1/deals/{id} | Delete. |
Writable fields
| Field | Type | Required | Notes |
|---|---|---|---|
| title | string | on create | Cannot be null. |
| pipeline_id | uuid | optional | Optional on create: defaults to the workspace's default (or first) pipeline. If the workspace has no pipeline at all, create returns 400 no_pipeline. Cannot be null. |
| stage_id | uuid | optional | Stage changes made over the API are recorded in the Activity Log, attributed to the API. |
| contact_id | uuid | optional | |
| company_id | uuid | optional | |
| value_cents | int | optional | Deal value in cents. |
| currency | string | optional | Cannot be null. |
| status | "open" | "won" | "lost" | "abandoned" | optional | Setting won/lost/abandoned stamps closed_at; setting open clears it. |
| owner_id | uuid | optional | |
| expected_close | date | optional | YYYY-MM-DD. |
| custom | object | optional | Custom field values. |
Reads also return: id, org_id, position, closed_at, created_by, created_at, updated_at.
curl -X POST https://app.chirply.io/api/v1/deals \
-H "Authorization: Bearer chp_live_…" \
-H "Content-Type: application/json" \
-d '{
"title": "Website redesign",
"value_cents": 450000,
"contact_id": "2f6a1c1e-6c3b-4c62-9f6e-8a2d4b7c9e11"
}'
→ 201 { "data": { "id": "…", "pipeline_id": "…", "status": "open", … } }
→ 400 { "error": { "code": "no_pipeline", "message": "This org has no pipeline yet. Create one in the app, or pass pipeline_id." } }Tasks
| GET /api/v1/tasks | List. ?q= searches title and description; ?status= filters by open, done or canceled. |
| POST /api/v1/tasks | Create. title is required. |
| GET /api/v1/tasks/{id} | Fetch one. |
| PATCH /api/v1/tasks/{id} | Partial update. Setting status to done stamps completed_at. |
| DELETE /api/v1/tasks/{id} | Delete. |
Writable fields
| Field | Type | Required | Notes |
|---|---|---|---|
| title | string | on create | Cannot be null. |
| description | string | optional | |
| status | "open" | "done" | "canceled" | optional | Setting done stamps completed_at; open/canceled clear it. |
| priority | "low" | "normal" | "high" | "urgent" | optional | |
| due_at | datetime | optional | ISO 8601 timestamp. |
| assignee_id | uuid | optional | |
| contact_id | uuid | optional | |
| deal_id | uuid | optional |
Reads also return: id, org_id, completed_at, created_by, created_at, updated_at.
curl -X PATCH https://app.chirply.io/api/v1/tasks/2f6a1c1e-6c3b-4c62-9f6e-8a2d4b7c9e11 \
-H "Authorization: Bearer chp_live_…" \
-H "Content-Type: application/json" \
-d '{ "status": "done" }'
→ 200 { "data": { "id": "…", "status": "done", "completed_at": "2026-09-17T15:00:00.000Z", … } }Webhooks
Outbound webhooks: register an HTTPS endpoint and Chirply POSTs each occurrence to it, HMAC-signed, with retries and a delivery log. API keys only on this surface.
| GET /api/v1/webhooks | List this workspace's subscriptions (limit/offset), plus available_events — the full taxonomy you can subscribe to. |
| POST /api/v1/webhooks | Subscribe: { "url": "https://…", "events": ["contact_created"] }. Returns the signing secret ONCE — it can never be read back. |
| GET /api/v1/webhooks/{id} | One subscription: event, url, active, created. |
| DELETE /api/v1/webhooks/{id} | Unsubscribe. Deliveries stop immediately and the delivery log goes with it. |
| GET /api/v1/webhooks/{id}/deliveries | Delivery log, newest first (limit/offset): status (pending → delivered, or failed/dead after retries run out), attempt count, last error. Payload bodies are not echoed here. |
| GET /api/v1/webhooks/samples?event=… | Up to 3 of the workspace's most recent REAL payloads for that event — byte-for-byte what the hook delivered, for field mapping. An event that never fired returns an empty list. |
curl -X POST https://app.chirply.io/api/v1/webhooks \
-H "Authorization: Bearer chp_live_…" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/chirply", "events": ["contact_created"] }'
→ 201
{
"data": {
"subscriptions": [ … ],
"secret": "whsec_…", // shown ONCE — store it now
"signature": "X-Chirply-Signature: t=<unix>,v1=HMAC-SHA256(secret, `${t}.${rawBody}`) hex",
"deliveries_url": "/api/v1/webhooks/{id}/deliveries"
}
}Each delivery carries X-Chirply-Event, X-Chirply-Delivery and X-Chirply-Signature; verify the signature before trusting a payload. The payload body, delivery guarantees and the full event table are documented on the webhooks page.

Chirply