Slingvo Zapier Integration & Public API
Developer documentation · Slingvo Public API v1
Slingvo is a sales dialer and CRM built for real estate agents. The Slingvo Zapier integration lets an account send contact cards and call dispositions from Slingvo to the CRM you already use, and lets other tools create or update contacts in Slingvo.
- Base URL:
https://api.slingvo.com/api/public/v1 - Authentication:
Authorization: Bearer slk_... - Rate limit: 60 requests per minute per API key
- Support: helpme@slingvo.com
Contents
- Status of this document
- What the integration does
- Getting started: create an API key and connect Zapier
- Authentication and base URL
- Scopes
- Conventions, rate limits and errors
- Endpoint reference
- Webhooks
- Verifying webhook signatures
- Zapier triggers, actions and fields
- Data fields sent to other CRMs
- Limitations
- Support and legal
1. Status of this document
This page separates what has been tested from what is described by the API contract only. Nothing in the second group should be read as a tested guarantee.
| Item | Status |
|---|---|
| Request and response shapes for every endpoint in section 7 | Verified against Slingvo's staging API (read-only calls, plus creating and deleting test records). |
Error codes, the 60/minute limit header, pagination, validation messages |
Verified on staging. |
contact.sent_to_crm accepted as a subscribable event |
Verified on staging. |
| Webhook payload shapes (section 8) and the signing scheme (section 9) | Per the Slingvo API contract. The payloads in this page are illustrative examples built from that contract with fake data. They are not captures of live deliveries. |
| Zapier signature verification against a real Zapier-delivered webhook | Not yet exercised. |
Production hostname api.slingvo.com |
Live. DNS resolves, TLS is valid, and the host is served over https by the production backend (checked 2026-09-30 CT). |
| Public API v1 on production (temporary note: delete this row once production goes live) | As of 2026-09-30 the public API v1 endpoints are deployed on staging only. GET https://api.slingvo.com/api/public/v1/me currently returns 404 until the public API reaches production, so the examples on this page were tested against a staging host with the same paths. |
| Slingvo app listing in the Zapier directory | Not published yet. The integration is private while it is under review. |
| In-app screen for creating API keys | Not confirmed. See section 3. |
All example data is fictional: phone numbers use the 555-01xx range and email addresses use example.com. Identifiers are placeholders.
2. What the integration does
The Slingvo Zapier integration connects your Slingvo account to the thousands of apps on Zapier. The main use is sending Slingvo activity into a CRM:
- When an agent logs a disposition (for example "Appointment Set") on a call, a Zap can push that contact, with the disposition, to your CRM.
- When a contact is created or updated in Slingvo, a Zap can create or update the matching record in your CRM.
- When an agent clicks Send to CRM on a contact card, a Zap can receive the full contact card. (This trigger is hidden in Zapier until it has been verified end to end.)
- In the other direction, a Zap can create, update or find Slingvo contacts from lead sources such as forms and ad platforms.
It is designed to work with any CRM that has a Zapier app, for example Lofty, Follow Up Boss, Wise Agent and BoldTrail. Those are examples of CRMs available on Zapier; this page does not claim an official partnership with any of them.
Under the hood the integration uses the Slingvo Public API documented below: a REST API over HTTPS with JSON bodies, API-key authentication and signed webhooks.
3. Getting started: create an API key and connect Zapier
Who can create a key
Only a Slingvo account admin can create API keys. Agent accounts cannot. An account can hold up to 25 active keys.
Create a key
Keys are created with the account-level endpoint POST /api/api-keys (note: no /public/v1), called as a logged-in admin session. The key is not accepted for this route: requests that use an slk_ key to call /api/api-keys return 401.
Request body:
{
"name": "Zapier",
"scopes": ["contacts:read", "contacts:write", "webhooks:manage"]
}
The response contains the key in data.key. It looks like slk_.... It is shown once. Slingvo stores only a SHA-256 hash, so a lost key cannot be recovered; create a new one and revoke the old one.
In-app screen not confirmed. When this page was written, an in-app settings page for creating API keys could not be confirmed in the Slingvo web app. If you are an admin and do not see a way to create a key, email helpme@slingvo.com and we will help you create one.
List and revoke keys (admin session, same base path):
| Action | Request |
|---|---|
| List keys | GET /api/api-keys |
| Revoke a key | DELETE /api/api-keys/:id |
Revoking a key also revokes every webhook subscription that key created.
Connect in Zapier
The Slingvo integration is currently private (not yet listed in the public Zapier app directory), so you connect through an invitation link from Slingvo. Once you have access:
- In Zapier, create a Zap (or edit one) and choose Slingvo as the app for a trigger or action.
- When asked to connect an account, choose Sign in / Add a new account.
- Paste your API key (
slk_...) into the Slingvo API Key field. - Zapier tests the connection by calling
GET /me. If it succeeds, the account is connected and labelled with your Slingvo account name. - Pick a trigger or action and continue building the Zap.
The key you use in Zapier needs the three scopes contacts:read, contacts:write and webhooks:manage (see section 5).
Example: Slingvo disposition → your CRM
- Trigger: Slingvo – Disposition Logged.
- Action: your CRM's "Create/Update Contact" (or "Add Lead") action.
- Map Slingvo fields (for example
fullName,email,phone,disposition) to the CRM's fields. The mapping is your choice and depends on the CRM; see section 11 for what Slingvo provides.
4. Authentication and base URL
Base URL
https://api.slingvo.com/api/public/v1
Every request needs an API key in the Authorization header:
Authorization: Bearer slk_xxxxxxxxxxxxxxxxxxxxxxxx
Requests with a body use JSON:
Content-Type: application/json
Accept: application/json
Every successful response has this shape:
{ "success": true, "data": { } }
A key identifies one Slingvo account (tenant). Everything you read or write through the key belongs to that account.
Quick check that a key works:
curl -s https://api.slingvo.com/api/public/v1/me \
-H "Authorization: Bearer $SLINGVO_API_KEY"
5. Scopes
Scopes are chosen when the key is created. They do not imply each other.
| Scope | Allows |
|---|---|
contacts:read |
Find and list contacts; list dispositions, custom fields, lists and folders |
contacts:write |
Create and update contacts |
webhooks:manage |
Subscribe, list, rotate and unsubscribe webhooks |
| (none) | GET /me needs a valid key but no particular scope |
A request made without the needed scope returns 403 insufficient_scope.
6. Conventions, rate limits and errors
Rate limit
- 60 requests per minute per API key.
- Every response includes
X-RateLimit-Limit(60) andX-RateLimit-Remaining. - When the limit is exceeded you get
429 rate_limitedwith aRetry-Afterheader (seconds). Wait that long before retrying. - Requests with a missing or invalid key share a stricter per-IP budget of 30 per minute.
Error envelope
All errors from the API use one shape:
{
"success": false,
"error": {
"code": "validation_error",
"message": "An email or phone is required"
}
}
Error codes
| HTTP status | error.code |
Meaning |
|---|---|---|
| 400 | validation_error |
The request is malformed or a value is invalid (for example Invalid limit, Invalid cursor, Webhook URL must be https, Unknown event). |
| 401 | unauthorized |
The API key is missing, malformed, revoked or unknown. |
| 403 | insufficient_scope |
The key is valid but lacks the scope this endpoint needs. |
| 403 | forbidden |
The action is not allowed for this key. |
| 404 | not_found |
The contact or webhook subscription does not exist in this account. |
| 409 | conflict |
The request conflicts with existing data: an update would attach an email or phone that belongs to another contact; an Idempotency-Key was reused with a different body; the webhook URL is already subscribed; or the account has reached the subscription limit (50). |
| 409 | ambiguous_match |
GET /contacts/find was given an email and a phone that match two different contacts. |
| 429 | rate_limited |
Rate limit exceeded. See Retry-After. |
| 500 | internal_error |
Server error. The message is always Internal server error; retry later or contact support. |
A request to a path that does not exist on the API host returns a plain HTML "Cannot GET ..." 404 page rather than the JSON envelope. Check the path if you see one.
Pagination
List endpoints that paginate use an opaque cursor: pass limit (1–100, default 25) and cursor. The response includes nextCursor, which is null on the last page.
Data formats
- Phone numbers are stored and returned in E.164 (
+15555550142). You can send common US formats; they are normalized. - Email addresses are lowercased.
- Timestamps are ISO-8601 UTC.
7. Endpoint reference
All paths below are relative to https://api.slingvo.com/api/public/v1. In the examples, $KEY is your API key.
The contact object
Returned by the contact endpoints and inside contact.created / contact.updated events.
{
"id": "ckm8x2q1a0001abcd1234wxyz",
"fullName": "Maria Alvarez",
"address": "4821 Maple Grove Ln",
"address2": "",
"city": "Austin",
"state": "TX",
"zip": "78745",
"mailingAddress": "",
"mailingAddress2": "",
"mailingCity": "",
"mailingState": "",
"mailingZip": "",
"source": "facebook",
"tags": ["expired-listing", "zapier"],
"notes": ["Asked about relisting in the spring."],
"description": "",
"status": "PENDING",
"disposition": "Appointment Set",
"customFields": { "MLS ID": "4471290", "Timeline": "0-3 months" },
"emails": [
{ "id": "cke1abcd", "email": "maria.alvarez@example.com", "isPrimary": true }
],
"phones": [
{ "id": "ckp1abcd", "number": "+15125550142", "type": "MOBILE", "isBestNumber": true, "isDnc": false }
],
"createdAt": "2026-09-28T14:03:11.000Z",
"updatedAt": "2026-09-29T16:45:02.000Z"
}
| Field | Notes |
|---|---|
phones[].type |
One of MOBILE, TELEPHONE, HOME, WORK. |
phones[].isDnc |
true if the number is flagged Do Not Call in Slingvo. |
customFields |
Keys are the custom field names returned by GET /custom-fields. |
disposition, status |
Read-only through the API (see section 12). |
GET /me
Returns information about the API key and account. Use it to test a connection.
Scope: none (valid key only)
curl -s https://api.slingvo.com/api/public/v1/me -H "Authorization: Bearer $KEY"
{
"success": true,
"data": {
"keyId": "ckk0example0001",
"name": "Zapier",
"prefix": "slk_abcd",
"scopes": ["contacts:read", "contacts:write", "webhooks:manage"],
"tenant": {
"id": "ckt0example0001",
"name": "Example Realty Team",
"email": "admin@example.com"
}
}
}
GET /contacts
Lists contacts, newest first.
Scope: contacts:read
| Query parameter | Description |
|---|---|
limit |
1–100, default 25. Other values return 400 validation_error ("Invalid limit"). |
cursor |
The nextCursor from the previous page. A bad cursor returns 400 ("Invalid cursor"). |
curl -s "https://api.slingvo.com/api/public/v1/contacts?limit=2" \
-H "Authorization: Bearer $KEY"
{
"success": true,
"data": {
"contacts": [
{ "id": "ckm8x2q1a0001abcd1234wxyz", "fullName": "Maria Alvarez", "...": "full contact object" },
{ "id": "ckm8x2q1a0002abcd1234wxyz", "fullName": "Sam Rivera", "...": "full contact object" }
],
"nextCursor": "eyJpZCI6IjEyMyJ9"
}
}
nextCursor is null on the last page.
GET /contacts/find
Finds one contact by email and/or phone.
Scope: contacts:read
| Query parameter | Description |
|---|---|
email |
Email address (case-insensitive). |
phone |
Phone number in any common format; matched against the stored E.164 number. |
At least one is required, otherwise 400 ("email or phone is required"). An unparseable phone returns 400 ("Phone could not be normalized"). If email and phone match two different contacts, the response is 409 ambiguous_match.
curl -s "https://api.slingvo.com/api/public/v1/contacts/find?email=maria.alvarez@example.com" \
-H "Authorization: Bearer $KEY"
{
"success": true,
"data": {
"contact": { "id": "ckm8x2q1a0001abcd1234wxyz", "fullName": "Maria Alvarez", "...": "full contact object" },
"matchCount": 1
}
}
No match:
{ "success": true, "data": { "contact": null, "matchCount": 0 } }
If several contacts match, the most recently updated one is returned and matchCount tells you how many matched.
POST /contacts
Creates a contact, or returns the existing one if the email or phone already matches.
Scope: contacts:write
| Body field | Type | Notes |
|---|---|---|
fullName |
string | Or use firstName and/or lastName. A name is required. |
email / emails |
string / array | At least one email or phone is required. |
phone / phones |
string / array | Normalized to E.164. |
address, address2, city, state, zip |
string | Property address. |
mailingAddress, mailingAddress2, mailingCity, mailingState, mailingZip |
string | Mailing address. |
tags |
array of strings | |
notes |
string or array | |
source |
string | Lead source. Defaults to public-api. |
customFields |
object | Keys are field names from GET /custom-fields. |
listId / folderId |
string | Place the contact in a list or folder (IDs from GET /lists, GET /folders). If you omit listId, the contact is owned by the account admin and agents on the team will not see it. |
Optional header Idempotency-Key (1–200 characters: letters, digits, _ - : .). Reusing a key with a different body returns 409 conflict.
curl -s -X POST https://api.slingvo.com/api/public/v1/contacts \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lead-2026-000123" \
-d '{
"fullName": "Jordan Lee",
"email": "jordan.lee@example.com",
"phone": "(512) 555-0143",
"city": "Austin",
"state": "TX",
"source": "website-form",
"tags": ["zapier"],
"notes": "Requested a home valuation.",
"listId": "ckl0example0001"
}'
201 Created (new contact):
{
"success": true,
"data": {
"contact": {
"id": "ckm8x2q1a0003abcd1234wxyz",
"fullName": "Jordan Lee",
"emails": [{ "id": "cke2abcd", "email": "jordan.lee@example.com", "isPrimary": true }],
"phones": [{ "id": "ckp2abcd", "number": "+15125550143", "type": "MOBILE", "isBestNumber": true, "isDnc": false }],
"source": "website-form",
"tags": ["zapier"],
"notes": ["Requested a home valuation."],
"...": "remaining contact fields"
},
"deduped": false
}
}
200 OK with "deduped": true means a contact with that email or phone already existed. It is returned unchanged, nothing was overwritten, and no contact.created event is emitted.
Missing contact details:
{ "success": false, "error": { "code": "validation_error", "message": "An email or phone is required" } }
PATCH /contacts/:id
Partially updates a contact. Send only the fields you want to change.
Scope: contacts:write
Merge rules:
- Phones and emails are merged in; existing ones are never deleted.
- Blank values are ignored (they do not erase data).
- Do-not-call and invalid-number flags are never cleared.
- Notes are appended.
- Tags, when present, replace the existing tags. Send
"tags": []to clear them. - customFields are merged; set a key to
nullto remove it. - Attaching an email or phone that belongs to a different contact returns
409 conflict.
curl -s -X PATCH https://api.slingvo.com/api/public/v1/contacts/ckm8x2q1a0003abcd1234wxyz \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "512-555-0144",
"notes": "Prefers text messages.",
"tags": ["zapier", "valuation"],
"customFields": { "Timeline": "3-6 months" }
}'
{
"success": true,
"data": {
"contact": {
"id": "ckm8x2q1a0003abcd1234wxyz",
"fullName": "Jordan Lee",
"phones": [
{ "id": "ckp2abcd", "number": "+15125550143", "type": "MOBILE", "isBestNumber": true, "isDnc": false },
{ "id": "ckp3abcd", "number": "+15125550144", "type": "MOBILE", "isBestNumber": false, "isDnc": false }
],
"tags": ["zapier", "valuation"],
"notes": ["Requested a home valuation.", "Prefers text messages."],
"customFields": { "Timeline": "3-6 months" },
"...": "remaining contact fields"
}
}
}
An unknown id returns:
{ "success": false, "error": { "code": "not_found", "message": "Contact not found" } }
GET /dispositions
Lists the account's dispositions. Dispositions are configured per account, so your list will differ from the example.
Scope: contacts:read
{
"success": true,
"data": {
"dispositions": [
{ "id": "ckd0example0001", "label": "Appointment Set", "value": "APPOINTMENT_SET", "color": "#22c55e", "isSystem": false, "isActive": true, "order": 3, "ownerUserId": null },
{ "id": "ckd0example0002", "label": "No Answer", "value": "NO_ANSWER", "color": "#9ca3af", "isSystem": false, "isActive": true, "order": 2, "ownerUserId": null }
]
}
}
GET /custom-fields
Lists the account's custom contact fields. Use the name as the key in customFields when creating or updating contacts.
Scope: contacts:read
{
"success": true,
"data": {
"customFields": [
{ "id": "ckf0example0001", "name": "MLS ID", "type": "text", "options": [], "sortOrder": 1, "ownerUserId": null },
{ "id": "ckf0example0002", "name": "Timeline", "type": "text", "options": [], "sortOrder": 2, "ownerUserId": null }
]
}
}
type and options describe the field. All custom fields observed on staging were text with no options; do not assume other types behave differently until you have tested them.
GET /lists
Lists the account's contact lists. Use an id as listId in POST /contacts.
Scope: contacts:read
{
"success": true,
"data": { "lists": [ { "id": "ckl0example0001", "name": "Zapier Leads" } ] }
}
GET /folders
Lists the account's folders. Use an id as folderId in POST /contacts.
Scope: contacts:read
{
"success": true,
"data": { "folders": [ { "id": "ckg0example0001", "name": "Expired Listings" } ] }
}
POST /hooks/subscribe
Registers a webhook URL to receive events. The Zapier integration calls this automatically when a Zap with a Slingvo trigger is turned on.
Scope: webhooks:manage
| Body field | Type | Notes |
|---|---|---|
url |
string | Must be https. http returns 400 ("Webhook URL must be https"). Private and loopback addresses are not allowed, and redirects are not followed (per the API contract). |
events |
array of strings | One or more of contact.created, contact.updated, disposition.logged, contact.sent_to_crm. An unknown name returns 400 ("Unknown event"). |
curl -s -X POST https://api.slingvo.com/api/public/v1/hooks/subscribe \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/slingvo", "events": ["disposition.logged"] }'
201 Created:
{
"success": true,
"data": {
"id": "cks0example0001",
"url": "https://hooks.example.com/slingvo",
"events": ["disposition.logged"],
"secret": "whsec_EXAMPLE_ONLY_NOT_A_REAL_SECRET",
"createdAt": "2026-09-29T15:00:00.000Z"
}
}
Save secret now. It is returned only in this response and is used to verify webhook signatures (section 9). If you lose it, call POST /hooks/:id/rotate.
Limits: an account can have up to 50 live subscriptions, and subscribing the same active URL twice returns 409 conflict.
GET /hooks
Lists your webhook subscriptions. The secret is never included.
Scope: webhooks:manage
{
"success": true,
"data": {
"subscriptions": [
{
"id": "cks0example0001",
"url": "https://hooks.example.com/slingvo",
"events": ["disposition.logged"],
"createdAt": "2026-09-29T15:00:00.000Z",
"disabledAt": null
}
]
}
}
A non-null disabledAt means Slingvo stopped delivering to that subscription after repeated failures (see "Delivery and retries").
DELETE /hooks/:id
Removes a subscription. The Zapier integration calls this automatically when a Zap is turned off.
Scope: webhooks:manage
curl -s -X DELETE https://api.slingvo.com/api/public/v1/hooks/cks0example0001 \
-H "Authorization: Bearer $KEY"
{ "success": true, "data": { "id": "cks0example0001", "revoked": true } }
An unknown id returns 404 not_found ("Webhook subscription not found").
POST /hooks/:id/rotate
(Not required by the Zapier integration.) Generates a new secret for the subscription, re-enables it if it was disabled, and returns the new secret once.
Scope: webhooks:manage
8. Webhooks
Instead of polling, subscribe to events and Slingvo will POST a JSON body to your URL when they happen.
Events
| Event | Fires when |
|---|---|
contact.created |
A contact is created in Slingvo, in the app or through the API. A POST /contacts call that matches an existing contact does not fire it. |
contact.updated |
A contact is updated, in the app or through the API. |
disposition.logged |
An agent logs a disposition on a contact (for example after a call). |
contact.sent_to_crm |
An agent clicks Send to CRM on a contact in the Slingvo app. |
Event envelope
Every delivery uses this envelope:
{
"id": "evt_example_0001",
"type": "contact.created",
"createdAt": "2026-09-29T15:30:00.000Z",
"contactId": "ckm8x2q1a0001abcd1234wxyz",
"dispositionLogId": null,
"origin": "app",
"apiKeyId": null,
"data": { }
}
| Field | Notes |
|---|---|
id |
Unique event id. The same value is sent as the X-Slingvo-Delivery header. Use it to de-duplicate, because a delivery can be retried. |
type |
The event name. |
origin |
app (a person using Slingvo) or public_api (a call through this API). |
apiKeyId |
For public_api events, the id of the API key that made the change. null otherwise. If your Zap both listens to events and writes contacts, ignore events whose apiKeyId is your own key to avoid loops. |
data |
Event-specific; see below. |
Delivery headers
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
Slingvo-Webhooks/1 |
X-Slingvo-Event |
The event name |
X-Slingvo-Delivery |
The event id (same as id) |
X-Slingvo-Signature |
t=<unix seconds>,v1=<hex HMAC> (see section 9) |
contact.created and contact.updated
data.contact is the contact object.
{
"id": "evt_example_0001",
"type": "contact.created",
"createdAt": "2026-09-29T15:30:00.000Z",
"contactId": "ckm8x2q1a0001abcd1234wxyz",
"dispositionLogId": null,
"origin": "public_api",
"apiKeyId": "ckk0example0001",
"data": {
"contact": {
"id": "ckm8x2q1a0001abcd1234wxyz",
"fullName": "Maria Alvarez",
"city": "Austin",
"state": "TX",
"source": "facebook",
"tags": ["expired-listing"],
"notes": [],
"customFields": {},
"emails": [{ "id": "cke1abcd", "email": "maria.alvarez@example.com", "isPrimary": true }],
"phones": [{ "id": "ckp1abcd", "number": "+15125550142", "type": "MOBILE", "isBestNumber": true, "isDnc": false }],
"createdAt": "2026-09-29T15:30:00.000Z",
"updatedAt": "2026-09-29T15:30:00.000Z",
"...": "remaining contact fields as in the contact object"
}
}
}
disposition.logged
{
"id": "evt_example_0002",
"type": "disposition.logged",
"createdAt": "2026-09-29T16:45:02.000Z",
"contactId": "ckm8x2q1a0001abcd1234wxyz",
"dispositionLogId": "ckdl0example0001",
"origin": "app",
"apiKeyId": null,
"data": {
"dispositionLogId": "ckdl0example0001",
"contactId": "ckm8x2q1a0001abcd1234wxyz",
"disposition": { "id": "ckd0example0001", "label": "Appointment Set", "value": "APPOINTMENT_SET" },
"appliedById": "cku0example0001",
"folderId": null,
"source": "CALL",
"contact": {
"id": "ckm8x2q1a0001abcd1234wxyz",
"fullName": "Maria Alvarez",
"emails": [{ "id": "cke1abcd", "email": "maria.alvarez@example.com", "isPrimary": true }],
"phones": [{ "id": "ckp1abcd", "number": "+15125550142", "type": "MOBILE", "isBestNumber": true, "isDnc": false }],
"customFields": { "MLS ID": "4471290" },
"...": "remaining contact fields"
}
}
}
| Field | Notes |
|---|---|
data.source |
CALL (logged after a call) or MANUAL (logged by hand). |
data.contact |
The contact card at the time of the event, in the same camelCase shape as the contact object. It is null if the contact no longer exists. |
data.folderId |
The folder the contact was in when the disposition was applied, if any. |
An automatic "Contacted" outcome that Slingvo refuses to apply does not emit this event.
contact.sent_to_crm
Fires when an agent clicks Send to CRM in the Slingvo app. It is not fired by ordinary contact or disposition changes. apiKeyId is null and origin is app.
Unlike the other events, data here uses snake_case names, with a complete contact card:
{
"id": "ckm8x2q1a0001abcd1234wxyz:2026-09-29T18:04:00.000Z",
"type": "contact.sent_to_crm",
"createdAt": "2026-09-29T18:04:00.000Z",
"contactId": "ckm8x2q1a0001abcd1234wxyz",
"dispositionLogId": null,
"origin": "app",
"apiKeyId": null,
"data": {
"event": "contact.sent_to_crm",
"event_id": "ckm8x2q1a0001abcd1234wxyz:2026-09-29T18:04:00.000Z",
"sent_at": "2026-09-29T18:04:00.000Z",
"source": "contact_card",
"disposition": "Appointment Set",
"contact": {
"id": "ckm8x2q1a0001abcd1234wxyz",
"full_name": "Maria Alvarez",
"first_name": "Maria",
"last_name": "Alvarez",
"emails": [{ "email": "maria.alvarez@example.com", "is_primary": true }],
"phones": [{ "number": "+15125550142", "type": "MOBILE", "is_best_number": true }],
"address": { "street": "4821 Maple Grove Ln", "city": "Austin", "state": "TX", "zip": "78745" },
"mailing_address": { "street": null, "city": null, "state": null, "zip": null },
"tags": ["expired-listing"],
"source": "facebook",
"notes": ["Asked about relisting in the spring."],
"custom_fields": { "MLS ID": "4471290", "Timeline": "0-3 months" },
"created_at": "2026-09-28T14:03:11.000Z",
"updated_at": "2026-09-29T16:45:02.000Z"
}
}
}
| Field | Notes |
|---|---|
data.source |
Where the button was clicked: dialer or contact_card. This is not the lead source; the lead source is data.contact.source. |
data.disposition |
The contact's current disposition label, or null. |
data.sent_at |
UTC timestamp with milliseconds. data.event_id is the contact id plus this timestamp, so clicking the button again creates a new event. |
contact.address.street |
Address line 1 and line 2 joined with , . |
contact.first_name / last_name |
Derived by splitting full_name at the first space (Slingvo stores a single full name). |
contact.custom_fields |
Keyed by the custom field label. |
The payload deliberately leaves out: internal owner ids, secrets, dialer status, call records, and the per-number do-not-call flag.
Delivery and retries
- Slingvo considers a delivery successful when your endpoint returns any
2xxstatus. Respond quickly (the request times out after 10 seconds). - Any other result, or a timeout, is retried after 1 minute, 5 minutes, 30 minutes and 2 hours. The fifth failed attempt is final.
- After 10 consecutive failures the subscription is disabled (
disabledAtis set inGET /hooks). CallPOST /hooks/:id/rotateto issue a new secret and re-enable it. - Delivery is at-least-once. De-duplicate on the envelope
id.
(Retry timing is taken from the Slingvo API contract and has not been observed on live deliveries.)
9. Verifying webhook signatures
Each delivery is signed so you can confirm it came from Slingvo and was not altered.
X-Slingvo-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e6e2d1e5d5c1e0f5e2b1c
tis the Unix time in seconds when Slingvo signed the delivery.v1is the lowercase hex HMAC-SHA256 of the stringt + "." + rawBody, using the subscription'swhsec_...secret as the key.rawBodyis the exact bytes of the request body, before any JSON parsing. Re-serializing parsed JSON will not produce the same bytes.
To verify:
- Split the header on
,and readtandv1. - Reject the request if
tdiffers from the current time by more than 300 seconds (replay protection). - Compute the HMAC over
`${t}.${rawBody}`. - Compare with
v1using a constant-time comparison. Reject on mismatch.
HTTP header names are case-insensitive; your framework may lower-case X-Slingvo-Signature.
Node.js example
const crypto = require("crypto");
function verifySlingvoSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
if (!signatureHeader) return false;
const parts = {};
for (const piece of signatureHeader.split(",")) {
const [k, v] = piece.split("=");
parts[k.trim()] = (v || "").trim();
}
if (!parts.t || !parts.v1) return false;
const ageSeconds = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!Number.isFinite(ageSeconds) || ageSeconds > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: capture the raw body
// app.post("/slingvo", express.raw({ type: "application/json" }), (req, res) => {
// const ok = verifySlingvoSignature(
// req.body.toString("utf8"),
// req.get("X-Slingvo-Signature"),
// process.env.SLINGVO_WEBHOOK_SECRET
// );
// if (!ok) return res.sendStatus(401);
// res.sendStatus(200);
// });
Python example
import hmac, hashlib, time
def verify_slingvo_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
if not header:
return False
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
return False
try:
if abs(time.time() - int(t)) > tolerance:
return False
except ValueError:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
Zapier users
If you use the Slingvo triggers in Zapier, Zapier subscribes for you and the integration performs this check internally. In a Zapier code step you would read the raw request from bundle.rawRequest (headers and body). Zapier may rename the signature header, for example to Http-X-Slingvo-Signature. The signature check inside Zapier has not yet been exercised with a live Zapier delivery.
10. Zapier triggers, actions and fields
Triggers
| Trigger | Event | Notes |
|---|---|---|
| New Contact | contact.created |
Has an option, "Ignore changes made through the API / Zapier?", to avoid loops. |
| Updated Contact | contact.updated |
Same option as above. |
| Disposition Logged | disposition.logged |
Includes the contact card. |
| Contact Sent to CRM | contact.sent_to_crm |
Hidden until verified end to end. |
Actions and searches
| Name | Type | Endpoint |
|---|---|---|
| Create Contact | Action | POST /contacts |
| Update Contact | Action | PATCH /contacts/:id |
| Find Contact | Search | GET /contacts/find |
| Find or Create Contact | Search + action | GET /contacts/find, then POST /contacts |
Custom fields appear as extra input fields (Create/Update) and as customFields__<name> outputs once a sample contact contains them.
Fields available in a Zap
Zapier flattens the webhook payload into simple fields so they are easy to map into another app.
| Field | Description |
|---|---|
contactId |
Slingvo contact id |
fullName, firstName, lastName |
Name. First and last are derived from fullName. |
email, phone |
Primary email and best phone number |
allEmails, allPhones |
All values, comma-separated |
emails[], phones[] |
Full lists |
address, address2, city, state, zip, fullAddress |
Property address |
mailingAddress, mailingAddress2, mailingCity, mailingState, mailingZip |
Mailing address |
source, tags (comma-separated), notes (joined), description, status, disposition |
Contact details |
customFields |
Object of custom values |
createdAt, updatedAt |
Timestamps |
id, eventType, eventCreatedAt, origin, apiKeyId |
Event details |
dispositionLogId, dispositionId, disposition, dispositionValue, appliedById, folderId, dispositionSource |
Disposition Logged only |
sentAt, crmSource, eventId |
Contact Sent to CRM only |
11. Data fields sent to other CRMs
What a CRM receives depends on the Zap you build: you choose which Slingvo fields to map to which CRM fields. Slingvo makes these fields available.
| Slingvo field | Content | Typical CRM destination (your mapping) |
|---|---|---|
fullName (and derived firstName, lastName) |
Contact name | Name fields |
email / emails[] |
Email addresses (primary flagged) | |
phone / phones[] |
Phone numbers in E.164, with type (MOBILE, TELEPHONE, HOME, WORK) and best-number flag |
Phone fields |
address, address2, city, state, zip |
Property address | Address |
mailingAddress…mailingZip |
Mailing address | Mailing address, if the CRM has one |
source |
Lead source | Source |
tags |
Tags | Tags / categories |
notes |
Notes entered in Slingvo | Note |
disposition |
The contact's current disposition label | Stage, status, tag or note |
customFields |
Values of your custom fields | Custom fields |
createdAt, updatedAt |
Timestamps | Optional |
Not sent: internal owner ids, secrets, dialer status, call records.
Do-not-call information is available in the phones[].isDnc field of the contact object (and in Zapier as part of phones[]). It is not included in the contact.sent_to_crm payload, so your CRM will not know a number is flagged in Slingvo unless you map it from another trigger.
Every CRM names its fields differently. Slingvo has not verified field mappings for any specific CRM, so check your own CRM's field list in the Zap editor.
12. Limitations
- Single name field. Slingvo stores one
fullName. First and last names are derived by splitting at the first space. - Disposition and status are read-only through the API. You can read them on contacts and in events, but cannot set them with
POSTorPATCH. - Nothing is deleted by an update. Phones, emails and notes can only be added. Blank values are ignored and do-not-call flags are never cleared.
- Tags are replaced on update when you send
tags. - No contact delete endpoint.
- Contacts created without
listIdare owned by the admin and not visible to agents. - Webhooks require HTTPS.
- Rate limit: 60 requests per minute per key.
13. Support and legal
- Support: helpme@slingvo.com
- Privacy policy: https://slingvo.com/privacy-policy
- Terms of service: https://slingvo.com/terms
When you contact support, include the approximate time (with time zone) of the request, the endpoint, and the HTTP status and error.code. Never send your API key or webhook secret.
© Slingvo. Documentation for Public API v1.