Smartify

Partner API

Read and update the leads Smartify shares with your organization, and your own leads, from your CRM or automation tool with an API key.

The Partner API lets your CRM, your own software or an automation tool work with the leads your organization can see in Smartify Pro: leads you own and leads Smartify shares with you. You can read them, move them through the pipeline, add notes, activities, tasks, reminders and files, unlock shared leads, and refer customers to Smartify. For real-time updates, add a webhook.

Base URL

https://api.pro.smartify.in/v1

The paths below are relative to this base URL. Every request is over HTTPS and uses JSON.

API keys

  1. An owner or admin of your organization opens Settings → Integrations in Smartify Pro and creates a key, choosing the scopes it needs.
  2. The key (spk_live_…) is shown once. Store it in your secret manager; Smartify keeps only a hash of it.
  3. Send it on every request: Authorization: Bearer spk_live_…
export SMARTIFY_API_KEY="spk_live_your_key_here"
curl -s -H "Authorization: Bearer $SMARTIFY_API_KEY" https://api.pro.smartify.in/v1/me
  • A key belongs to your organization, not to a person, and sees exactly what your team sees in the app, never more.
  • Up to 10 active keys per organization. A revoked key stops working within about a minute.
  • A key cannot manage keys. It can manage webhook endpoints only with the webhooks:manage scope, and only the ones it created.
  • A key never sees partnership, dealership, Smartify Pro or support inquiries, even if one was shared with you.

Keep keys out of browsers and mobile apps. Call the API from your server or from an automation tool you trust.

Scopes

Each key has one or more scopes. A request that needs a scope the key lacks gets 403 insufficient_scope.

ScopeAllows
leads:readRead leads, lead history, attachments and the event feed.
leads:writeEdit your own manual leads, change stages, and add or change notes, activities, tasks, reminders and attachments.
leads:unlockUnlock a shared lead. This spends credits exactly as unlocking in the app does.
referrals:writeRefer a customer to Smartify.
catalog:readRead the Smartify catalog (once Smartify has approved catalog access for your organization).
catalog:submitSubmit, change and remove your own catalog products for Smartify review.
webhooks:manageCreate, change and delete the webhook endpoints this key created (see Webhooks).

Give a key only the scopes its job needs. GET /me works with any key and lists the key's scopes.

Endpoints

Read

RequestReturns
GET /meThe key: key_id, key_name, org_id, org_name, scopes. Use it to check a key works.
GET /leadsYour leads, newest first. Query: limit (1 to 100, default 25), after, stage.
GET /leads?updated_since=...The change feed: every lead changed at or after that ISO 8601 time, oldest first.
GET /leads/{leadId}One lead, as the app shows it to you, with header ETag: W/"updated_at".
GET /leads/{leadId}/historyNotes, activities and stage changes, newest first.
GET /leads/{leadId}/attachmentsThe files on a lead.
GET /eventsYour organization's event feed (see Webhooks).

Lists return data, next_cursor and has_more. Pass next_cursor back as after until has_more is false or data is empty. A cursor only resumes the listing that issued it.

Write

RequestScopeWhat it does
PATCH /leads/{leadId}leads:writeEdit a lead your organization created by hand.
POST /leads/{leadId}/stageleads:writeMove a lead to another stage.
POST /leads/{leadId}/notesleads:writeAdd a note to the lead timeline.
POST /leads/{leadId}/activitiesleads:writeLog a call, email, meeting or note.
PATCH or DELETE /leads/{leadId}/activities/{itemId}leads:writeChange or remove an activity.
POST /leads/{leadId}/tasksleads:writeAdd a task.
PATCH or DELETE /leads/{leadId}/tasks/{itemId}leads:writeChange or remove a task.
POST /leads/{leadId}/remindersleads:writeAdd a reminder.
PATCH or DELETE /leads/{leadId}/reminders/{itemId}leads:writeChange or remove a reminder.
POST /leads/{leadId}/attachments/upload-urlleads:writeGet a short-lived URL to upload a file to.
POST /leads/{leadId}/attachmentsleads:writeRecord an uploaded file on the lead.
DELETE /leads/{leadId}/attachments/{itemId}leads:writeRemove a file from the lead.
POST /leads/{leadId}/unlockleads:unlockUnlock a shared lead.
POST /referralsreferrals:writeRefer a customer to Smartify.
GET or POST /webhookswebhooks:manageList the webhook endpoints this key created, or create one.
GET, PATCH or DELETE /webhooks/{endpointId}webhooks:manageRead, change or delete one of them.
POST /webhooks/{endpointId}/rotate-secret and .../testwebhooks:manageRotate the signing secret, or send a ping.

The catalog endpoints are on their own page.

How writes behave

  • A key acts as your organization in the app, never as an admin. A write succeeds only when the same action would succeed for your team in Smartify Pro.
  • A lead your organization cannot see answers 404 lead_not_found, the same as a lead that does not exist. A shared lead you have not unlocked answers 403 lead_locked to anything that touches its details.
  • Every successful write returns the lead's new ETag header. Create calls return 201 with the new item in data. Deletes return 204 with no body.
  • Writes show up in your webhook feed with an origin naming your key, so you can ignore your own echoes.

Keep your CRM in step

Store the updated_at of the newest lead you processed, and ask for changes from that time:

curl -s -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  "https://api.pro.smartify.in/v1/leads?updated_since=2026-11-13T00:00:00Z&limit=100"

The boundary is inclusive, so you can see the same lead twice: upsert by lead_id. With webhooks set up, use this feed only to catch up after downtime.

What a lead shows

You get the same fields the app shows your organization. A lead shared with you but not yet unlocked is a preview ("masked": true, no contact details), and its history answers 403 lead_locked until you unlock it.

In history, your own team's entries show their names. Everything else, including work Smartify's team did before sharing the lead, is credited to Smartify (created_by: "smartify").

Edit a lead

PATCH /leads/{leadId}

Only leads your organization added by hand can be edited. A lead Smartify shared or assigned to you answers 403 not_editable.

Send any of these fields; the rest stay as they are.

FieldNotes
nameCannot be empty.
phoneCannot be empty; must be a valid phone number.
emailA valid email address, or "" to clear it.
cityCannot be empty.
addressText.
property_type, carpet_area, estimated_budget, possession_statusText. possession_status is at most 100 characters.
requirements_textText, at most 2000 characters.
curl -s -X PATCH https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123 \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: W/"2026-11-13T10:15:02.874Z"' \
  -d '{ "city": "Pune", "requirements_text": "3 BHK, wants curtain motors" }'

The response is { "data": { …the lead… } } with the new ETag. A body with no editable fields is 400 invalid_parameter.

Change a lead's stage

POST /leads/{leadId}/stage
FieldNotes
toRequired. The stage to move to.
reasonRequired for LOST and NOT_INTERESTED (one of a fixed list, see below), and when moving backwards or re-opening a closed lead (free text, at least 3 characters).
noteOptional note, up to 2000 characters.
deal_valueRequired for WON. A positive number, for example 150000.
won_atOptional for WON: an ISO 8601 time, not more than a day in the future. Defaults to now.
estimate_idOptional for QUOTED: the Pro estimate you are quoting from.
curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/stage \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-sync-8841" \
  -d '{ "to": "WON", "deal_value": 150000, "note": "Signed today" }'

The response is { "data": { …the lead… } } with the new ETag.

Stage moves follow the same rules as the app:

FromForward toBack or re-open to (needs reason)
NEWUNLOCKEDnone
UNLOCKEDCONTACTED, NOT_INTERESTEDnone
CONTACTEDQUOTED, NOT_INTERESTEDUNLOCKED
QUOTEDWON, LOST, NOT_INTERESTEDCONTACTED
CUSTOMER_ACCEPTEDWON, LOST, NOT_INTERESTEDQUOTED
WONnoneQUOTED
LOSTnoneQUOTED
NOT_INTERESTEDnoneCONTACTED, QUOTED

CUSTOMER_ACCEPTED is set by Smartify when a customer accepts a quote on the public quote page. You cannot move a lead into it.

Moving to LOST also needs a reason of price_too_high, chose_competitor, project_postponed, no_response or other. Moving to NOT_INTERESTED needs declined_before_quote, wrong_contact, project_cancelled or other.

A move the rules do not allow answers 422 stage_transition_not_allowed, and tells you what is allowed from where the lead is now:

{
  "type": "about:blank",
  "title": "Unprocessable request",
  "status": 422,
  "detail": "A lead can't move from NEW to WON.",
  "code": "stage_transition_not_allowed",
  "from": "NEW",
  "allowed_transitions": [{ "to": "UNLOCKED", "requires_reason": false }]
}

requires_reason is true when the move needs a reason. For LOST and NOT_INTERESTED the entry also has a reasons array with the values you may send; for moves backwards or re-opening the reason is free text. A missing or invalid reason or deal_value answers 400 invalid_parameter with detail saying what to fix.

Notes, activities, tasks and reminders

All four are added with a POST on the lead and return 201 with the new item in data. Tasks, reminders and activities can then be changed with PATCH (send only the fields to change) and removed with DELETE.

Notes

curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/notes \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-note-5521" \
  -d '{ "note": "Customer asked for a site visit on Saturday." }'

note is required, up to 2000 characters.

Activities

FieldNotes
typeRequired: call, email, meeting or note. Cannot be changed later.
contentRequired, up to 4000 characters.
titleOptional, up to 200 characters.
occurred_atOptional ISO 8601 time with a timezone, for example 2026-11-13T10:00:00+05:30. Not more than 5 minutes in the future.
sourceOptional label, up to 80 characters.
duration_seconds, outcome, directionCalls only. duration_seconds is a whole number from 0 to 86400. outcome is one of connected, no_answer, busy, voicemail, failed, cancelled, completed, other. direction is inbound or outbound.

Any other field is refused with 400 invalid_parameter.

curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/activities \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-call-9917" \
  -d '{ "type": "call", "content": "Walked through the quote.", "direction": "outbound", "outcome": "connected", "duration_seconds": 420 }'

Tasks

FieldNotes
nameRequired on create, up to 200 characters.
descriptionOptional text.
due_dateOptional due date, as an ISO 8601 string.
statusThe app uses TO_DO (the default), IN_PROGRESS and DONE. Setting DONE completes the task and sends a lead.task.completed event.
priorityDefaults to MEDIUM.
curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/tasks \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Send curtain motor quote", "due_date": "2026-11-20" }'

Reminders

FieldNotes
titleRequired on create, up to 200 characters.
date_timeRequired on create. ISO 8601 date-time.
descriptionOptional text.

Ids and legacy items

Every item has an id. Items written before ids existed get ids like legacy_<16 hex characters>. Use them like any other id in PATCH and DELETE paths.

An item that does not exist answers 404 not_found. Editing two different items on one lead at the same time is safe: both changes land. In the rare case two writes hit the same lead at the very same instant, one can answer 409 conflict; send it again.

Files

Attaching a file takes two requests. Files can be PDFs, images (PNG, JPEG, WebP, GIF, HEIC), Word and Excel files, CSV, plain text and DWG or DXF drawings, up to 15 MB each, and a lead holds up to 50 files (the 51st answers 409).

  1. Ask for an upload URL:
curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/attachments/upload-url \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "file_name": "floor-plan.pdf", "file_type": "application/pdf", "file_size": 482113 }'

The response is { "data": { "upload_url": "…", "file_url": "…" } }. The upload_url is valid for 15 minutes.

  1. PUT the file to upload_url with the same Content-Type and a Content-Length equal to file_size.

  2. Record it on the lead, using the file_url from step 1:

curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/attachments \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "file_name": "floor-plan.pdf", "file_type": "application/pdf", "file_url": "FILE_URL_FROM_STEP_1", "file_size": 482113 }'

Smartify checks that the file exists, its type and its real size before recording it.

Unlock a shared lead

curl -s -X POST https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/unlock \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Idempotency-Key: unlock-CRM-1794550000000-123"

The response is { "data": { …the unlocked lead… } } with the new ETag. Unlocking needs the leads:unlock scope and spends credits exactly as unlocking in the app does. If your organization cannot pay for it, the answer is 402 payment_required and carries a subscription_url, where you can unlock without credits:

{
  "type": "about:blank",
  "title": "Payment required",
  "status": 402,
  "detail": "…",
  "code": "payment_required",
  "subscription_url": "https://pro.smartify.in/settings/billing"
}

Always send an Idempotency-Key on unlock, so a retry after a timeout cannot spend credits twice.

Refer a customer to Smartify

POST /referrals

Needs the referrals:write scope. Smartify contacts the customer on WhatsApp to confirm, and you earn the referral commission if the referral converts.

FieldNotes
name, phone, city, reasonRequired. reason says what the customer wants.
email, notesOptional.
curl -s -X POST https://api.pro.smartify.in/v1/referrals \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: referral-2026-11-13-001" \
  -d '{
    "name": "Test Customer",
    "phone": "+910000000000",
    "city": "Pune",
    "reason": "Wants a smart home setup for a new 3 BHK",
    "notes": "Prefers calls after 6 pm"
  }'
{
  "data": {
    "message": "Referral submitted. Customer will receive a WhatsApp verification.",
    "lead_id": "REF-1794550000000-123",
    "whatsapp_delivery": "SENT"
  }
}

whatsapp_delivery is SENT when the verification went out and FAILED when it could not be confirmed. The referral is created either way.

A customer who is already known to Smartify answers 409 conflict. A referral that your organization sharing has been suspended for answers 403 forbidden.

The old referral endpoint that took an x-api-key header keeps working until 2026-12-13 and then stops. Create a partner API key with the referrals:write scope and move to POST /v1/referrals before then.

Idempotency

Send an Idempotency-Key header on any POST to make it safe to retry. It must be 1 to 255 printable characters, or the answer is 400 invalid_parameter. Use a value that is unique for each thing you mean to do, such as an id from your own system.

  • The same key with the same request within 48 hours replays the first response, without doing the work again.
  • The same key with a different request (a different body, lead or endpoint) answers 422 idempotency_key_reused.
  • The same key while the first request is still running answers 409 idempotency_in_progress. Wait a moment and retry.
  • Server errors (5xx) are not stored, so retry them with the same key. Other answers, including refusals such as 422 or 403, are stored and replayed: after fixing the request, use a new key.
  • Keys are scoped to the API key that sent them.
  • This holds for notes and activities too: a retry with the same key never adds a second note or activity.
  • Catalog submissions do not take an Idempotency-Key; a submission is keyed by your SKU (see the Partner Catalog API).

PATCH and DELETE are naturally safe to repeat, so they do not take the header. They do take If-Match (below).

Conditional writes with ETag

Every read and write of a lead returns ETag: W/"<updated_at>". Send that value back in an If-Match header on a PATCH, a DELETE or a stage change to make the write conditional: if the lead changed in the meantime, the write is refused with 412 precondition_failed and nothing changes. Fetch the lead again and decide what to do.

curl -s -X DELETE https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123/tasks/task_123 \
  -H "Authorization: Bearer $SMARTIFY_API_KEY" \
  -H 'If-Match: W/"2026-11-13T10:15:02.874Z"'

If-Match is optional. It works on lead edits, stage changes and on PATCH and DELETE of activities, tasks, reminders and attachments. Without it a write goes ahead on whatever the lead looks like now.

Errors

Errors from the API are RFC 9457 application/problem+json with a stable code. Some add fields, such as allowed_transitions or subscription_url.

StatuscodeMeaning
400invalid_parameterA query parameter, header or body field is missing or malformed. detail says which.
400invalid_jsonThe body is not a JSON object.
401invalid_keyThe key was revoked moments ago.
402payment_requiredUnlocking needs credits you do not have. See subscription_url.
403insufficient_scopeThe key lacks the scope the route needs.
403lead_lockedUnlock the lead before reading or changing its details.
403not_editableOnly your organization's own manual leads can be edited.
403forbiddenThe app would refuse this action for your organization. detail says why.
403catalog_not_enabledSmartify has not approved catalog access for your organization.
404lead_not_foundNo such lead, or not one your organization can see. The two are deliberately the same.
404not_foundNo such item or endpoint.
405method_not_allowedThe path exists but not with this method.
409conflictTwo writes hit the same lead at the same instant. Retry.
409idempotency_in_progressThe first request with this Idempotency-Key is still running.
412precondition_failedThe lead changed since the ETag you sent. Fetch it again.
422stage_transition_not_allowedThe stage move is not allowed. See allowed_transitions.
422idempotency_key_reusedThe Idempotency-Key was already used for a different request.
429rate_limitedOver 120 requests a minute for this key. Wait Retry-After seconds.
5xxinternal_error or request_failedRetry with backoff, with the same Idempotency-Key if you sent one.

Some errors come from the gateway before the API sees the request. They are plain JSON with a message and no code:

StatusmessageMeaning
401UnauthorizedMissing, malformed or revoked key: the usual bad-key answer.
403Missing Authentication TokenNo such route, or a method the route does not have.

Branch on the HTTP status first, then on code.

Limits

  • 120 requests a minute per key. Over the limit you get 429 rate_limited with a Retry-After header; wait that many seconds.
  • Up to 10 active keys per organization, and 25 webhook endpoints.

The machine-readable contract is the OpenAPI 3.1 document.

Other ways to connect