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
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/contactsList. Searches with ?q= across first_name, last_name, business_name, facebook_id, email, phone.
POST /api/v1/contactsCreate. 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

FieldTypeRequiredNotes
first_namestringoptional
last_namestringoptional
business_namestringoptional
emailstringoptionalTrimmed and lowercased on save. One contact per email address per workspace — a duplicate is a 409 naming the existing contact's id.
phonestringoptionalNormalized 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_idstringoptionalAlso unique per workspace; a duplicate is a 409 naming the existing contact's id.
websitestringoptional
titlestringoptionalJob title.
birthdaystringoptional
sourcestringoptionalWhere this contact came from.
source_detailobjectoptionalPer-source specifics — which site, which search, which file.
lifecyclestringoptionalLifecycle stage.
company_iduuidoptional
owner_iduuidoptional
notesstringoptional
addressobjectoptional
customobjectoptionalCustom 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
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/dealsList. ?q= searches title; ?status= filters by open, won, lost or abandoned.
POST /api/v1/dealsCreate. 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

FieldTypeRequiredNotes
titlestringon createCannot be null.
pipeline_iduuidoptionalOptional 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_iduuidoptionalStage changes made over the API are recorded in the Activity Log, attributed to the API.
contact_iduuidoptional
company_iduuidoptional
value_centsintoptionalDeal value in cents.
currencystringoptionalCannot be null.
status"open" | "won" | "lost" | "abandoned"optionalSetting won/lost/abandoned stamps closed_at; setting open clears it.
owner_iduuidoptional
expected_closedateoptionalYYYY-MM-DD.
customobjectoptionalCustom field values.

Reads also return: id, org_id, position, closed_at, created_by, created_at, updated_at.

curl
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/tasksList. ?q= searches title and description; ?status= filters by open, done or canceled.
POST /api/v1/tasksCreate. 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

FieldTypeRequiredNotes
titlestringon createCannot be null.
descriptionstringoptional
status"open" | "done" | "canceled"optionalSetting done stamps completed_at; open/canceled clear it.
priority"low" | "normal" | "high" | "urgent"optional
due_atdatetimeoptionalISO 8601 timestamp.
assignee_iduuidoptional
contact_iduuidoptional
deal_iduuidoptional

Reads also return: id, org_id, completed_at, created_by, created_at, updated_at.

curl
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/webhooksList this workspace's subscriptions (limit/offset), plus available_events — the full taxonomy you can subscribe to.
POST /api/v1/webhooksSubscribe: { "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}/deliveriesDelivery 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
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.