API Reference

Read and write your CRM data programmatically. Create a key in Settings → API Keys.

Machine-readable contract

Download OpenAPI 3.1.1 JSON · Contract version 2026-10-12.3. Contact cleanup, contact and task reads, client briefs, explicit local activities, and note reads/appends/edits/previews/deletions have generated schemas and complete synthetic examples. Other supported endpoints are listed as legacy in the specification until their schema migration is complete.

Contact timestamps are ISO instants, or null for invalid/missing provider timestamps. Page and limit reads are not a snapshot. Stop on an empty or short page, or a reconciled total; changing totals require reconciliation. Contact detail returns at most 50 notes without a note cursor.

Activities and notes cover Relay-local records only. Follow nextCursor until null; the cursor belongs to the account/contact and bounds creation time. Activity append and note append/edit/delete require Idempotency-Key. Retry the same key and normalized request after an uncertain response; a changed request returns 409. After 7 days, an expired saved response returns 410 with original operation/result IDs and never executes again. Other legacy writes retain their existing behavior.

Contact cleanup is read-only. It returns missing details and possible duplicate groups with reasons, checked-record counts and complete/partial read status. Shared details are not proof of identity. Follow its cursor with the same kind and limit; a changed fingerprint returns 409 and requires restarting. Group membership can be truncated explicitly. Refresh with fresh=1 to bypass cached contact data; computedAt is evaluation time.

Task lists support contactId, dealId, assignedUserId and status (open includes pending and in_progress). dueFrom is inclusive and dueBefore exclusive; use timestamps with Z or an explicit offset. They compare stored dueDate instants and do not infer a calendar day. Supply limit to page through stable ordering and matching totals; omitting limit retains the complete matching Relay-local collection. Concurrent changes may move tasks between pages. Partner-minted keys retain their restricted task response.

To permanently delete a local note, first request preview-delete and review its summary. Both preview and deletion require contacts.read and contacts.write. The approval token expires after 10 minutes and binds the actor and exact note content/revision; a changed note requires a fresh preview. Keep the token private. Retry an uncertain deletion using its original key and token even after expiry; do not replace them with a new request.

Authentication

Send your key as a Bearer token on every request. Keys are shown once at creation and cannot be retrieved again.

Authorization: Bearer relay_sk_live_xxxxxxxx

Base URL:

https://www.hellorelay.io/api/v1

Scopes

Each key carries one or more scopes. A .write scope does not grant read access — request both if you need to read and write.

New keys in Settings start with read-only permissions and a 30-day expiry. Choose Contacts only, Tasks only, or Custom to select other permissions and expiry. Scopes apply across the account; they do not restrict access to selected records. Existing keys keep their saved scopes and expiry. Expired or revoked keys cannot authenticate.

contacts.readRead contacts, their notes and explicit local activity logs
contacts.writeCreate, update, and permanently delete contacts; append/edit local notes and append explicit local activity logs. Preview/deletion of local notes also requires contacts.read
tasks.readRead tasks
tasks.writeCreate, update, complete, and delete tasks
pipeline.readRead pipelines, stages, and opportunities
pipeline.writeCreate, update, and delete opportunities
transactions.readRead transactions (deals with property, deadlines, and status)
transactions.writeCreate, update, and delete transactions
marketing.readRead social posts and connected accounts
marketing.writeCreate and schedule social posts
inbox.readRead conversations and message threads
inbox.writeSend messages and start conversations
webhooks.readList registered webhook endpoints
webhooks.writeRegister and remove webhook endpoints
users.readRead team members (id, email, CRM user id) for cross-system identity mapping

Rate limits & errors

Requests are limited to 120 per minute per key. A Relay rate-limit response includes Retry-After: 60 (seconds). Every supported API operation adds a server-generated X-Request-Id for troubleshooting. Errors keep the same code/message shape; schema validation adds details.issues with field paths, codes and messages.

{ "error": { "code": "forbidden", "message": "Missing required scope: contacts.write" } }
{ "error": { "code": "invalid_request", "message": "Request validation failed", "details": { "issues": [{ "path": ["title"], "code": "too_small", "message": "Title is required" }] } } }

Malformed JSON returns 400 invalid_body. A validation failure does not execute the write. A lost response to a write is not proof it failed. Registered local writes supply the replay guarantee described above; legacy writes keep their existing behavior. Unavailable, expired, used or changed note previews return 409 action_preview_invalid; missing notes return 404.

400Request validation failed
401Missing, invalid, expired, or revoked key
403Valid key lacking the required scope
404Resource not found in your account
409Conflict (e.g. duplicate contact)
410Original local action succeeded; replay response expired. Retained IDs remain; do not execute it again
429Rate limit exceeded (120 requests/minute per key)
500Unexpected internal failure (include X-Request-Id when reporting it)
502Upstream error (the upstream status may also be forwarded)

Endpoints

GET/api/v1/contactsrequired scopes: contacts.read

List a page of contacts. Optional query, limit (1–100, default 20) and page (from 1, default 1). Prefer POST search for email or phone terms.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/contacts" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "contacts": [
    {
      "id": "synthetic-c1",
      "name": "Jane Example",
      "firstName": "Jane",
      "lastName": "Example",
      "email": "jane@example.invalid",
      "phone": "+15555550101",
      "address1": "123 Example Street",
      "city": "Example City",
      "state": "UT",
      "postalCode": "84000",
      "country": "US",
      "tags": [
        "buyer"
      ],
      "source": "Synthetic open house",
      "assignedTo": "synthetic-agent1",
      "createdAt": "2026-10-01T12:00:00.000Z",
      "updatedAt": "2026-10-09T12:00:00.000Z",
      "lastActivity": "2026-10-09T12:00:00.000Z"
    }
  ],
  "total": 1
}
POST/api/v1/contacts/searchrequired scopes: contacts.read

Search contacts using a JSON body. Query is trimmed and must contain a non-whitespace character. Limit and page must be JSON numbers. This operation reads data.

Generated OpenAPI contract; complete synthetic response example.

curl -X POST "https://www.hellorelay.io/api/v1/contacts/search" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{"query":"jane@example.invalid","limit":20,"page":1}'

Response

{
  "contacts": [
    {
      "id": "synthetic-c1",
      "name": "Jane Example",
      "firstName": "Jane",
      "lastName": "Example",
      "email": "jane@example.invalid",
      "phone": "+15555550101",
      "address1": "123 Example Street",
      "city": "Example City",
      "state": "UT",
      "postalCode": "84000",
      "country": "US",
      "tags": [
        "buyer"
      ],
      "source": "Synthetic open house",
      "assignedTo": "synthetic-agent1",
      "createdAt": "2026-10-01T12:00:00.000Z",
      "updatedAt": "2026-10-09T12:00:00.000Z",
      "lastActivity": "2026-10-09T12:00:00.000Z"
    }
  ],
  "total": 1
}
GET/api/v1/contacts/:idrequired scopes: contacts.read

Read one contact and its latest 50 Relay notes, newest first. Notes are a bounded subset; this endpoint has no note cursor.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/contacts/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "id": "synthetic-c1",
  "name": "Jane Example",
  "firstName": "Jane",
  "lastName": "Example",
  "email": "jane@example.invalid",
  "phone": "+15555550101",
  "address1": "123 Example Street",
  "city": "Example City",
  "state": "UT",
  "postalCode": "84000",
  "country": "US",
  "tags": [
    "buyer"
  ],
  "source": "Synthetic open house",
  "assignedTo": "synthetic-agent1",
  "createdAt": "2026-10-01T12:00:00.000Z",
  "updatedAt": "2026-10-09T12:00:00.000Z",
  "lastActivity": "2026-10-09T12:00:00.000Z",
  "notes": [
    {
      "id": "synthetic-n1",
      "body": "Synthetic introduction note",
      "authorName": "Alex Example",
      "createdAt": "2026-10-09T12:00:00.000Z"
    }
  ]
}
GET/api/v1/contacts/:id/activitiesrequired scopes: contacts.read

List explicit Relay-local activity, newest occurred-at first. Limit 1–100 (default 20); follow nextCursor until null. The cursor bounds creation time and belongs to this account/contact. Local-only coverage is not evidence that no upstream conversation occurred. Cancelled meetings affect neither communication nor unsuccessful-attempt recency.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/contacts/:id/activities" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "activities": [
    {
      "id": "synthetic-activity1",
      "contactId": "synthetic-c1",
      "channel": "call",
      "outcome": "no_answer",
      "occurredAt": "2026-10-09T12:00:00.000Z",
      "note": "Synthetic call attempt",
      "actorName": "Alex Example",
      "source": "relay_api",
      "createdAt": "2026-10-09T12:05:00.000Z",
      "isCommunication": false
    }
  ],
  "nextCursor": null,
  "asOf": "2026-10-09T12:10:00.000Z",
  "coverage": "relay_local_only",
  "lastCommunicationAt": null,
  "lastUnsuccessfulAttemptAt": "2026-10-09T12:00:00.000Z"
}
POST/api/v1/contacts/:id/activitiesrequired scopes: contacts.write

Explicitly log an already occurred call, meeting, email or SMS. Required Idempotency-Key: 8–200 ASCII letters/digits or . _ : -. Same actor/account/key and normalized request replay one atomic local append; changed input returns 409. Future times are rejected. Actor/source are server assigned; no outreach is sent and nothing is mirrored. Response retained 7 days, then 410 includes original operation/result IDs and never executes again. Recheck authorization/contact ownership on every replay.

Generated OpenAPI contract; complete synthetic response example.

curl -X POST "https://www.hellorelay.io/api/v1/contacts/:id/activities" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Idempotency-Key: synthetic-operation-request-1" \
  -H "Content-Type: application/json" \
  -d '{"channel":"call","outcome":"no_answer","occurredAt":"2026-10-09T12:00:00.000Z","note":"Synthetic call attempt"}'

Response

{
  "activity": {
    "id": "synthetic-activity1",
    "contactId": "synthetic-c1",
    "channel": "call",
    "outcome": "no_answer",
    "occurredAt": "2026-10-09T12:00:00.000Z",
    "note": "Synthetic call attempt",
    "actorName": "Alex Example",
    "source": "relay_api",
    "createdAt": "2026-10-09T12:05:00.000Z",
    "isCommunication": false
  },
  "operation": {
    "id": "synthetic-op1",
    "status": "succeeded",
    "replayed": false,
    "resultIds": [
      "synthetic-activity1"
    ],
    "responseExpired": false,
    "replayUntil": "2026-10-16T12:05:00.000Z"
  }
}
GET/api/v1/contacts/:id/notesrequired scopes: contacts.read

List Relay-local notes, created-at descending then ID descending. Limit 1–100 (default 20); follow nextCursor until null. Cursor belongs to this account/contact and fences creation time. Contents can change between pages; this is not a snapshot. Original author/source and current revision are returned; legacy attribution may be unknown.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/contacts/:id/notes" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "notes": [
    {
      "id": "synthetic-note1",
      "body": "Synthetic open house introduction",
      "authorName": "API key: Example agent",
      "createdAt": "2026-10-10T00:00:00.000Z",
      "updatedAt": "2026-10-10T00:00:00.000Z",
      "revision": 1,
      "source": "relay_api"
    }
  ],
  "nextCursor": null,
  "asOf": "2026-10-10T00:05:00.000Z",
  "coverage": "relay_local_only"
}
POST/api/v1/contacts/:id/notesrequired scopes: contacts.write

Append a nonempty trimmed note of at most 10,000 characters. Author/source are assigned by the server. Required Idempotency-Key: 8–200 ASCII letters/digits or . _ : -. Reuse the same actor/account/key and normalized request after an uncertain save; changed input returns 409. Local write and receipt commit atomically. Receipt body retained 7 days; then 410 retains operation/result IDs and never executes again. Current scope/contact ownership is checked before every replay. No outreach, CRM mirror or activity log is created.

Generated OpenAPI contract; complete synthetic response example.

curl -X POST "https://www.hellorelay.io/api/v1/contacts/:id/notes" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Idempotency-Key: synthetic-operation-request-1" \
  -H "Content-Type: application/json" \
  -d '{"body":"Synthetic open house introduction"}'

Response

{
  "note": {
    "id": "synthetic-note1",
    "body": "Synthetic open house introduction",
    "authorName": "API key: Example agent",
    "createdAt": "2026-10-10T00:00:00.000Z",
    "updatedAt": "2026-10-10T00:00:00.000Z",
    "revision": 1,
    "source": "relay_api"
  },
  "operation": {
    "id": "synthetic-operation1",
    "status": "succeeded",
    "replayed": false,
    "resultIds": [
      "synthetic-note1"
    ],
    "responseExpired": false,
    "replayUntil": "2026-10-17T00:00:00.000Z"
  }
}
PATCH/api/v1/contacts/:id/notes/:noteIdrequired scopes: contacts.write

Edit a note using its last-read integer revision. Stale revision returns 409 note_revision_conflict; re-read and resolve the draft before making a new request. Every successful edit increments revision and preserves original author/source. Replay returns the original edit result even if the note later changes. Missing/wrong-parent notes return 404. Required Idempotency-Key: 8–200 ASCII letters/digits or . _ : -. Reuse the same actor/account/key and normalized request after an uncertain save; changed input returns 409. Local write and receipt commit atomically. Receipt body retained 7 days; then 410 retains operation/result IDs and never executes again. Current scope/contact ownership is checked before every replay. No outreach, CRM mirror or activity log is created.

Generated OpenAPI contract; complete synthetic response example.

curl -X PATCH "https://www.hellorelay.io/api/v1/contacts/:id/notes/:noteId" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Idempotency-Key: synthetic-operation-request-1" \
  -H "Content-Type: application/json" \
  -d '{"body":"Synthetic updated context","revision":1}'

Response

{
  "note": {
    "id": "synthetic-note1",
    "body": "Synthetic updated context",
    "authorName": "API key: Example agent",
    "createdAt": "2026-10-10T00:00:00.000Z",
    "updatedAt": "2026-10-10T00:00:00.000Z",
    "revision": 2,
    "source": "relay_api"
  },
  "operation": {
    "id": "synthetic-operation1",
    "status": "succeeded",
    "replayed": false,
    "resultIds": [
      "synthetic-note1"
    ],
    "responseExpired": false,
    "replayUntil": "2026-10-17T00:00:00.000Z"
  }
}
POST/api/v1/contacts/:id/notes/:noteId/preview-deleterequired scopes: contacts.write + contacts.read

Preview permanent deletion of one Relay-local note. Requires BOTH contacts.write and contacts.read. Returns its original author, revision and body (first 10,000 characters for oversized legacy notes, with bodyLength/bodyTruncated disclosure). No note is changed. The opaque approval token binds this account/actor/contact/note and full note content/revision, expires in 10 minutes and can execute once. Store it privately. Requesting another preview is safe and issues a distinct token.

Generated OpenAPI contract; complete synthetic response example.

curl -X POST "https://www.hellorelay.io/api/v1/contacts/:id/notes/:noteId/preview-delete" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{}'

Response

{
  "preview": {
    "token": "00000000-0000-4000-8000-000000000000.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    "expiresAt": "2026-10-10T00:10:00.000Z",
    "summary": {
      "action": "delete_note",
      "permanent": true,
      "coverage": "relay_local_only",
      "note": {
        "id": "synthetic-note1",
        "body": "Synthetic open house introduction",
        "authorName": "API key: Example agent",
        "createdAt": "2026-10-10T00:00:00.000Z",
        "updatedAt": "2026-10-10T00:00:00.000Z",
        "revision": 1,
        "source": "relay_api",
        "bodyLength": 33,
        "bodyTruncated": false
      }
    }
  }
}
DELETE/api/v1/contacts/:id/notes/:noteIdrequired scopes: contacts.write + contacts.read

Permanently delete the note reviewed in preview-delete. Requires BOTH contacts.write and contacts.read, its previewToken, and Idempotency-Key. A changed note or unavailable/expired/used approval returns 409 action_preview_invalid; missing/wrong-parent note returns 404. Approval consumption, note deletion and receipt commit together. Retry the EXACT same key/token/request after an uncertain response, even if the token expired or note is already gone. Do not substitute a fresh preview/key for an uncertain deletion. Saved response retained 7 days, then 410 retains original operation/deleted-note IDs and never executes again. Fresh scopes/contact ownership checked on every request/replay. No GHL mutation, message, activity log or queue is created.

Generated OpenAPI contract; complete synthetic response example.

curl -X DELETE "https://www.hellorelay.io/api/v1/contacts/:id/notes/:noteId" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Idempotency-Key: synthetic-operation-request-1" \
  -H "Content-Type: application/json" \
  -d '{"previewToken":"00000000-0000-4000-8000-000000000000.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}'

Response

{
  "deletedNoteId": "synthetic-note1",
  "operation": {
    "id": "synthetic-operation1",
    "status": "succeeded",
    "replayed": false,
    "resultIds": [
      "synthetic-note1"
    ],
    "responseExpired": false,
    "replayUntil": "2026-10-17T00:00:00.000Z"
  }
}
GET/api/v1/contacts/:id/profilerequired scopes: contacts.read

Read the optional structured client brief saved in Relay, independent of upstream identity/address fields. A successfully read absent brief is profile:null; failure is an error, never absence. Role, timeframe, budget range, currency, areas and channel are preferences; channel does not grant communication consent. Current raw contact ownership is verified before the local read.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/contacts/:id/profile" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "profile": {
    "id": "synthetic-profile1",
    "contactId": "synthetic-c1",
    "role": "buyer",
    "timeframe": "Within three months",
    "budgetMin": 350000,
    "budgetMax": 500000,
    "currency": "USD",
    "areas": [
      "Example City",
      "Example District"
    ],
    "preferredChannel": "sms",
    "revision": 1,
    "createdAt": "2026-10-12T00:00:00.000Z",
    "updatedAt": "2026-10-12T00:00:00.000Z"
  },
  "coverage": "relay_local_only"
}
PATCH/api/v1/contacts/:id/profilerequired scopes: contacts.write + contacts.read

Requires BOTH contacts.write and contacts.read because the response contains the full effective brief. Send original revision (0 only when no brief exists) and at least one changed field. Omission preserves, null clears nullable fields, [] clears areas. Budgets are nonnegative JSON numbers up to 1 trillion with at most two decimals; the effective minimum cannot exceed maximum, including one-sided updates. Currency is an uppercase supported current ISO currency code, default USD on first creation; changing it relabels amounts without conversion. Areas are trimmed, at most 20 names of 100 characters; duplicate names ignoring case are invalid. A stale revision returns 409; review current values before a new request. Required Idempotency-Key is 8–200 ASCII letters/digits or . _ : -. Reuse the exact actor/account/key/request after an interrupted save. Update and receipt commit atomically; replay returns the original result even after later changes. Response body retained 7 days, then 410 retains operation/profile IDs and never executes again. Fresh scope/contact access checked before every replay. No upstream mutation, message, task, activity or queue is created.

Generated OpenAPI contract; complete synthetic response example.

curl -X PATCH "https://www.hellorelay.io/api/v1/contacts/:id/profile" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Idempotency-Key: synthetic-operation-request-1" \
  -H "Content-Type: application/json" \
  -d '{"revision":0,"role":"buyer","budgetMin":350000,"budgetMax":500000,"currency":"USD"}'

Response

{
  "profile": {
    "id": "synthetic-profile1",
    "contactId": "synthetic-c1",
    "role": "buyer",
    "timeframe": "Within three months",
    "budgetMin": 350000,
    "budgetMax": 500000,
    "currency": "USD",
    "areas": [
      "Example City",
      "Example District"
    ],
    "preferredChannel": "sms",
    "revision": 1,
    "createdAt": "2026-10-12T00:00:00.000Z",
    "updatedAt": "2026-10-12T00:00:00.000Z"
  },
  "operation": {
    "id": "synthetic-operation1",
    "status": "succeeded",
    "replayed": false,
    "resultIds": [
      "synthetic-profile1"
    ],
    "responseExpired": false,
    "replayUntil": "2026-10-19T00:00:00.000Z"
  }
}
GET/api/v1/tasksrequired scopes: tasks.read

List Relay-local tasks. Filter status (including open = pending/in_progress), contactId, dealId, assignedUserId; compatible ghlContactId/ghlOpportunityId aliases must agree. Contact and deal filters combine with AND; no parent metadata is read. dueFrom is inclusive and dueBefore exclusive, using explicit-offset timestamps against legacy dueDate instants; null dates are excluded from a range. No calendar-day meaning is inferred. Supply each recognized parameter once. Unknown parameters are ignored. Limit 1–100 enables database paging; omitting limit returns all matching tasks and ignores page for compatibility. Stable order: dueDate ascending (nulls last), priority descending, id ascending. Count and rows agree within a response; pages across requests are not a snapshot. asOf is read-completion time. Partner-minted keys retain their restricted vendor-neutral projection. Read coverage excludes remote-only tasks.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/tasks" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "tasks": [
    {
      "id": "synthetic-task-1",
      "tenantId": "synthetic-account",
      "assignedUserId": "synthetic-user",
      "title": "Call Jane",
      "description": "Synthetic follow-up",
      "status": "pending",
      "priority": "high",
      "dueDate": "2026-10-16T16:00:00.000Z",
      "ghlContactId": "synthetic-contact",
      "ghlOpportunityId": null,
      "ghlTaskId": null,
      "completedAt": null,
      "completionNotes": null,
      "taskPlanId": null,
      "taskPlanItemId": null,
      "taskInstanceId": null,
      "createdAt": "2026-10-12T12:00:00.000Z",
      "updatedAt": "2026-10-12T12:00:00.000Z"
    }
  ],
  "total": 1,
  "projection": "standard",
  "paging": {
    "limit": 20,
    "page": 1,
    "hasMore": false
  },
  "coverage": "relay_local_only",
  "dateSemantics": "legacy_dueDate_instant",
  "asOf": "2026-10-12T12:00:00.000Z"
}
GET/api/v1/contacts/cleanuprequired scopes: contacts.read

Review missing usable contact details and possible duplicate contacts without changing records. Exact normalized email/full phone-digit matches are stronger cues than same names; country/reachability/consent are not inferred. Shared details may be intentional. Groups with identical members combine their reasons. At most 20 members are displayed per group; memberTotal/memberTruncated describe the rest. Counts cover loaded contacts only, and partial reads never prove no other issues exist. Limit 1–100 (default20); kind all/missing_details/duplicates; follow nextCursor with the same kind/limit. A changed input fingerprint returns409 cleanup_changed; restart the review. fresh1 bypasses cached contact data. computedAt is evaluation time, not a provider snapshot timestamp. API keys remain freshly checked during the bounded read. No notes/tasks/deals/profile data, writes, merge or outreach.

Generated OpenAPI contract; complete synthetic response example.

curl -X GET "https://www.hellorelay.io/api/v1/contacts/cleanup" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{
  "findings": [
    {
      "id": "cleanup-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "kind": "missing_details",
      "reasons": [
        "missing_phone"
      ],
      "strength": "missing_details",
      "contacts": [
        {
          "id": "synthetic-c1",
          "name": "Jane Example",
          "email": "jane@example.invalid",
          "phone": null
        }
      ],
      "memberTotal": 1,
      "memberTruncated": false
    }
  ],
  "total": 1,
  "summary": {
    "contactsRead": 1,
    "missingEmail": 0,
    "missingPhone": 1,
    "duplicateGroups": 0
  },
  "coverage": "crm_contact_book",
  "sourceStatus": "complete",
  "sourceFreshness": "fresh_read",
  "computedAt": "2026-10-12T12:00:00.000Z",
  "fingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "nextCursor": null
}
POST/api/v1/contactsrequired scopes: contacts.write

Create a contact

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/contacts" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com" }'

Response

{ "id": "c1", "name": "Jane Doe" }
PUT/api/v1/contacts/:idrequired scopes: contacts.write

Update a contact

Legacy contract; response example shows an illustrative subset.

curl -X PUT "https://www.hellorelay.io/api/v1/contacts/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "email": "new@example.com" }'

Response

{ "id": "c1" }
DELETE/api/v1/contacts/:idrequired scopes: contacts.write

Delete a contact (permanent, removed from GHL too)

Legacy contract; response example shows an illustrative subset.

curl -X DELETE "https://www.hellorelay.io/api/v1/contacts/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "success": true }
POST/api/v1/tasksrequired scopes: tasks.write

Create a task (title required; contact/opportunity links and assignee optional, defaults to key owner; unlinked tasks remain local to Relay)

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/tasks" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Call Jane", "ghlContactId": "c1" }'

Response

{ "id": "t1" }
GET/api/v1/tasks/:idrequired scopes: tasks.read

Get a task

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/tasks/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "id": "t1", "title": "Call Jane" }
PUT/api/v1/tasks/:idrequired scopes: tasks.write

Update a task (edits and completions mirror to GHL when the task is contact-linked)

Legacy contract; response example shows an illustrative subset.

curl -X PUT "https://www.hellorelay.io/api/v1/tasks/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "completed" }'

Response

{ "id": "t1", "status": "completed" }
DELETE/api/v1/tasks/:idrequired scopes: tasks.write

Delete a task (mirrors to GHL when contact-linked)

Legacy contract; response example shows an illustrative subset.

curl -X DELETE "https://www.hellorelay.io/api/v1/tasks/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "success": true }
GET/api/v1/pipeline/stagesrequired scopes: pipeline.read

List pipelines and stages

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/pipeline/stages" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "pipelines": [{ "id": "p1", "stages": [{ "id": "s1", "name": "New" }] }] }
GET/api/v1/pipeline/opportunitiesrequired scopes: pipeline.read

List opportunities

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/pipeline/opportunities" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "opportunities": [{ "id": "o1" }], "total": 1 }
POST/api/v1/pipeline/opportunitiesrequired scopes: pipeline.write

Create an opportunity (name/pipelineId/pipelineStageId required; contactId optional — omitting it provisions a contact from the deal name; optional monetaryValue, status: open|won|lost|abandoned)

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/pipeline/opportunities" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "123 Main St", "pipelineId": "p1", "pipelineStageId": "s1", "contactId": "c1", "monetaryValue": 450000, "status": "open" }'

Response

{ "id": "o1" }
PUT/api/v1/pipeline/opportunities/:idrequired scopes: pipeline.write

Update an opportunity

Legacy contract; response example shows an illustrative subset.

curl -X PUT "https://www.hellorelay.io/api/v1/pipeline/opportunities/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "won" }'

Response

{ "id": "o1", "status": "won" }
DELETE/api/v1/pipeline/opportunities/:idrequired scopes: pipeline.write

Delete an opportunity (permanent, removed from GHL too)

Legacy contract; response example shows an illustrative subset.

curl -X DELETE "https://www.hellorelay.io/api/v1/pipeline/opportunities/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "success": true }
GET/api/v1/transactionsrequired scopes: transactions.read

List transactions (optional type=buy|sell, contactId)

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/transactions" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "transactions": [{ "id": "o1", "propertyAddress": "123 Main St", "dealStatus": "active" }] }
POST/api/v1/transactionsrequired scopes: transactions.write

Create a transaction (type/propertyAddress required; contactId optional — omitting it provisions a contact from the deal name; optional price, dealStatus: active|under_contract, closeDate, sellerDisclosureDeadline, dueDiligenceDeadline, financingAppraisalDeadline — all four deadlines required when under_contract; dates are ISO datetimes)

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/transactions" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "contactId": "c1", "type": "buy", "propertyAddress": "123 Main St", "price": 450000 }'

Response

{ "id": "o1", "propertyAddress": "123 Main St", "dealStatus": "active" }
GET/api/v1/transactions/:idrequired scopes: transactions.read

Get a transaction (id = opportunity id)

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/transactions/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "id": "o1", "propertyAddress": "123 Main St", "closeDate": null }
PUT/api/v1/transactions/:idrequired scopes: transactions.write

Update a transaction (any create field plus pipelineStageId/status; setting dealStatus back to active clears all deadlines)

Legacy contract; response example shows an illustrative subset.

curl -X PUT "https://www.hellorelay.io/api/v1/transactions/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "dealStatus": "under_contract", "closeDate": "2026-08-30T00:00:00Z", "sellerDisclosureDeadline": "2026-08-01T00:00:00Z", "dueDiligenceDeadline": "2026-08-10T00:00:00Z", "financingAppraisalDeadline": "2026-08-20T00:00:00Z" }'

Response

{ "id": "o1", "dealStatus": "under_contract" }
DELETE/api/v1/transactions/:idrequired scopes: transactions.write

Delete a transaction (removes the GHL opportunity and the deal metadata)

Legacy contract; response example shows an illustrative subset.

curl -X DELETE "https://www.hellorelay.io/api/v1/transactions/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "success": true }
GET/api/v1/marketing/accountsrequired scopes: marketing.read

List connected social accounts

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/marketing/accounts" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "accounts": [{ "id": "a1", "platform": "facebook" }] }
GET/api/v1/marketing/postsrequired scopes: marketing.read

List social posts (optional fromDate/toDate/accountIds/status)

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/marketing/posts" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "posts": [{ "id": "sp1", "summary": "New listing!" }] }
POST/api/v1/marketing/postsrequired scopes: marketing.write

Create or schedule a social post

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/marketing/posts" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "accountIds": ["a1"], "summary": "New listing!", "status": "draft" }'

Response

{ "post": { "id": "sp1" } }
GET/api/v1/inbox/conversationsrequired scopes: inbox.read

List conversations (optional search/contactId)

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/inbox/conversations" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "conversations": [{ "id": "cv1", "contactId": "c1" }] }
POST/api/v1/inbox/conversationsrequired scopes: inbox.write

Start a conversation with a contact

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/inbox/conversations" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "contactId": "c1" }'

Response

{ "conversationId": "cv1" }
GET/api/v1/inbox/conversations/:id/messagesrequired scopes: inbox.read

Read a conversation thread

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/inbox/conversations/:id/messages" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "messages": [{ "id": "m1", "body": "Hi" }] }
POST/api/v1/inbox/conversations/:id/messagesrequired scopes: inbox.write

Send a message in a conversation

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/inbox/conversations/:id/messages" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "type": "sms", "message": "Thanks for reaching out!" }'

Response

{ "messageId": "m1" }
GET/api/v1/webhooksrequired scopes: webhooks.read

List registered webhook endpoints (signing secrets are never returned again)

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/webhooks" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "webhooks": [{ "id": "wh1", "url": "https://example.com/hook", "events": ["message.inbound"], "active": true }] }
POST/api/v1/webhooksrequired scopes: webhooks.write

Register a webhook endpoint. Events: message.inbound (reply lands in the inbox), contact.created (new GHL contact), opportunity.stage_changed (pipeline stage move), transaction.closed (closeDate first persisted). Deliveries are JSON POSTs with X-Relay-Event, X-Relay-Timestamp, and X-Relay-Signature: v1=HMAC_SHA256(secret, timestamp + "." + rawBody) — verify with the secret returned once here. Best-effort delivery (one retry); keep polling as a fallback for missed events.

Legacy contract; response example shows an illustrative subset.

curl -X POST "https://www.hellorelay.io/api/v1/webhooks" \
  -H "Authorization: Bearer relay_sk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hook", "events": ["message.inbound", "contact.created", "opportunity.stage_changed", "transaction.closed"] }'

Response

{ "id": "wh1", "url": "https://example.com/hook", "events": ["message.inbound", "contact.created", "opportunity.stage_changed", "transaction.closed"], "active": true, "secret": "<returned once>" }
DELETE/api/v1/webhooks/:idrequired scopes: webhooks.write

Remove a webhook endpoint

Legacy contract; response example shows an illustrative subset.

curl -X DELETE "https://www.hellorelay.io/api/v1/webhooks/:id" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "success": true }
GET/api/v1/usersrequired scopes: users.read

List your account's team members, including deactivated ones (isActive: false) since historical records still reference them. ghlUserId is the id that contact/opportunity assignedTo fields reference — use it to attribute CRM records to people in an external system. name and ghlUserId are null for members never linked to the CRM.

Legacy contract; response example shows an illustrative subset.

curl -X GET "https://www.hellorelay.io/api/v1/users" \
  -H "Authorization: Bearer relay_sk_live_xxxx"

Response

{ "users": [{ "id": "u1", "email": "agent@example.com", "name": "Jane Agent", "ghlUserId": "ghl_u_1", "role": "agent", "isActive": true }] }