// developers
Read and write your CRM data programmatically. Create a key in Settings → API Keys.
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.
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_xxxxxxxxBase URL:
https://www.hellorelay.io/api/v1Each 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.read | Read contacts, their notes and explicit local activity logs |
contacts.write | Create, 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.read | Read tasks |
tasks.write | Create, update, complete, and delete tasks |
pipeline.read | Read pipelines, stages, and opportunities |
pipeline.write | Create, update, and delete opportunities |
transactions.read | Read transactions (deals with property, deadlines, and status) |
transactions.write | Create, update, and delete transactions |
marketing.read | Read social posts and connected accounts |
marketing.write | Create and schedule social posts |
inbox.read | Read conversations and message threads |
inbox.write | Send messages and start conversations |
webhooks.read | List registered webhook endpoints |
webhooks.write | Register and remove webhook endpoints |
users.read | Read team members (id, email, CRM user id) for cross-system identity mapping |
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.
| 400 | Request validation failed |
| 401 | Missing, invalid, expired, or revoked key |
| 403 | Valid key lacking the required scope |
| 404 | Resource not found in your account |
| 409 | Conflict (e.g. duplicate contact) |
| 410 | Original local action succeeded; replay response expired. Retained IDs remain; do not execute it again |
| 429 | Rate limit exceeded (120 requests/minute per key) |
| 500 | Unexpected internal failure (include X-Request-Id when reporting it) |
| 502 | Upstream error (the upstream status may also be forwarded) |
/api/v1/contactsrequired scopes: contacts.readList 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
}/api/v1/contacts/searchrequired scopes: contacts.readSearch 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
}/api/v1/contacts/:idrequired scopes: contacts.readRead 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"
}
]
}/api/v1/contacts/:id/activitiesrequired scopes: contacts.readList 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"
}/api/v1/contacts/:id/activitiesrequired scopes: contacts.writeExplicitly 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"
}
}/api/v1/contacts/:id/notesrequired scopes: contacts.readList 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"
}/api/v1/contacts/:id/notesrequired scopes: contacts.writeAppend 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"
}
}/api/v1/contacts/:id/notes/:noteIdrequired scopes: contacts.writeEdit 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"
}
}/api/v1/contacts/:id/notes/:noteId/preview-deleterequired scopes: contacts.write + contacts.readPreview 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
}
}
}
}/api/v1/contacts/:id/notes/:noteIdrequired scopes: contacts.write + contacts.readPermanently 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"
}
}/api/v1/contacts/:id/profilerequired scopes: contacts.readRead 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"
}/api/v1/contacts/:id/profilerequired scopes: contacts.write + contacts.readRequires 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"
}
}/api/v1/tasksrequired scopes: tasks.readList 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"
}/api/v1/contacts/cleanuprequired scopes: contacts.readReview 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
}/api/v1/contactsrequired scopes: contacts.writeCreate 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" }/api/v1/contacts/:idrequired scopes: contacts.writeUpdate 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" }/api/v1/contacts/:idrequired scopes: contacts.writeDelete 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 }/api/v1/tasksrequired scopes: tasks.writeCreate 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" }/api/v1/tasks/:idrequired scopes: tasks.readGet 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" }/api/v1/tasks/:idrequired scopes: tasks.writeUpdate 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" }/api/v1/tasks/:idrequired scopes: tasks.writeDelete 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 }/api/v1/pipeline/stagesrequired scopes: pipeline.readList 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" }] }] }/api/v1/pipeline/opportunitiesrequired scopes: pipeline.readList 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 }/api/v1/pipeline/opportunitiesrequired scopes: pipeline.writeCreate 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" }/api/v1/pipeline/opportunities/:idrequired scopes: pipeline.writeUpdate 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" }/api/v1/pipeline/opportunities/:idrequired scopes: pipeline.writeDelete 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 }/api/v1/transactionsrequired scopes: transactions.readList 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" }] }/api/v1/transactionsrequired scopes: transactions.writeCreate 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" }/api/v1/transactions/:idrequired scopes: transactions.readGet 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 }/api/v1/transactions/:idrequired scopes: transactions.writeUpdate 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" }/api/v1/transactions/:idrequired scopes: transactions.writeDelete 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 }/api/v1/marketing/accountsrequired scopes: marketing.readList 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" }] }/api/v1/marketing/postsrequired scopes: marketing.readList 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!" }] }/api/v1/marketing/postsrequired scopes: marketing.writeCreate 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" } }/api/v1/inbox/conversationsrequired scopes: inbox.readList 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" }] }/api/v1/inbox/conversationsrequired scopes: inbox.writeStart 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" }/api/v1/inbox/conversations/:id/messagesrequired scopes: inbox.readRead 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" }] }/api/v1/inbox/conversations/:id/messagesrequired scopes: inbox.writeSend 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" }/api/v1/webhooksrequired scopes: webhooks.readList 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 }] }/api/v1/webhooksrequired scopes: webhooks.writeRegister 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>" }/api/v1/webhooks/:idrequired scopes: webhooks.writeRemove 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 }/api/v1/usersrequired scopes: users.readList 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 }] }