1. Getting a key
You never sign up for the API yourself. Each firm you work with creates a key for you and decides what it may do. You only ever see that firm's data, and the firm can revoke the key at any time.
- The firm's owner (or a principal) opens Settings, Integrations, API in Smartnatic.
- They name the key after your platform, tick the access it needs (see scopes) and press Create key.
- The key, which starts with
snk_live_, is shown once. They copy it and send it to you over a safe channel. Smartnatic keeps only a fingerprint of it, so a lost key cannot be shown again: the firm simply creates a new one and revokes the old one.
A firm can hold 10 active keys at once. Use one key per firm and per platform, and store it like a password.
2. Authentication
Send the key in the Authorization header of every request. The API is for servers: never put a key in a browser or a mobile app.
curl https://smartnatic.com/api/v1/me \
-H "Authorization: Bearer $SMARTNATIC_KEY"
Answer
{
"data": {
"firm": { "id": "clx8f2...", "name": "Studio Nour", "country": "AE", "currency": "AED", "locale": "en" },
"key": { "id": "clx9a1...", "name": "Listings portal", "scopes": ["projects:read", "portfolio:read"] },
"plan": "paid",
"limits": { "requests_per_minute": 120, "requests_per_day": 5000, "webhook_endpoints": 5, "scopes_available": ["..."] }
}
}
GET /me works with any valid key and tells you which firm it belongs to, what it may do and the limits of the firm's plan. A revoked or expired key gets 401. If the firm's trial has ended or its account is suspended, every call gets 403 with the code subscription_required or firm_suspended.
3. Scopes
| Scope | Allows | Plans |
|---|---|---|
projects:read | List and read projects: type, location, area, summary, public cover image, stages with progress, gates and deliverable counts. | Paid and trial |
portfolio:read | The firm's public portfolio, exactly what its own public portfolio page shows. | Paid and trial |
leads:write | Send a lead into the firm's CRM. The firm's owners are notified. | Paid |
projects:write | Create a project (from raw fields or from a lead) and change its name, summary, location, area and external reference. | Paid |
clients:read | Adds the client's name and contacts to projects. Personal data: off unless the firm ticks it. | Paid |
webhooks:manage | List, register and delete webhook endpoints. | Paid |
A call without the scope it needs gets 403 insufficient_scope. A scope the firm's plan does not include gets 403 plan_required, also for a key that was made while the firm was paying.
4. Plans and limits
The API is free for partners. What a key may do depends on the plan of the firm that created it.
| Firm on a paid plan | Firm on the free trial | |
|---|---|---|
| Scopes | All | Read-only: projects:read, portfolio:read |
| Requests per key | 120 a minute, 5,000 a day | 120 a minute, 1,000 a day |
| Webhook endpoints | Up to 5 | None |
Over a limit you get 429 rate_limited with a Retry-After header in seconds. Wait that long, then carry on. Firms whose trial ended, or whose account is suspended, get 403 until that is sorted out. Need more? Contact us.
5. Conventions
- JSON everywhere. Send
Content-Type: application/json; bodies are at most 32 KB. Unknown fields are refused, so a typo never silently does nothing. - Envelope. One item comes as
{"data": {...}}, a list as{"data": [...], "next_cursor": ...}. - Dates are ISO 8601 in UTC, for example
2026-09-19T10:02:11.000Z. - Money is
{"amount": "45000000", "currency": "AED"}: minor units (cents, fils) as a string, and an ISO 4217 currency. - Areas are
{"value": 420, "unit": "m2"}or"sq_ft", in the firm's unit system. - Stage names come in the project's language. Add
?locale=de(or en, ar, fr, ...) to get them in another one where the firm has it. - Pagination. Lists take
?limit=(1 to 100, default 25). When there is more, the answer has anext_cursor: pass it back as?cursor=. The last page hasnext_cursor: null. - Safe retries. Add an
Idempotency-Keyheader (8 to 120 characters) toPOST /leadsandPOST /projects. Sending the same key with the same body again returns the first result, withIdempotent-Replayed: true, instead of creating a second one. Keys are remembered for 24 hours. Leads and projects also take your ownexternal_ref, which stays unique for good.
curl "https://smartnatic.com/api/v1/projects?limit=50" -H "Authorization: Bearer $SMARTNATIC_KEY"
# { "data": [ ... 50 projects ... ], "next_cursor": "eyJjIjoiMjAyNi0w..." }
curl "https://smartnatic.com/api/v1/projects?limit=50&cursor=eyJjIjoiMjAyNi0w..." -H "Authorization: Bearer $SMARTNATIC_KEY"
# { "data": [ ... ], "next_cursor": null } <- last page
6. Errors
Every error has the same shape, with a stable code to branch on and a message for people.
HTTP/1.1 403 Forbidden
{ "error": { "code": "insufficient_scope", "message": "This key does not have the leads:write scope." } }
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json, invalid_cursor, invalid_idempotency_key | The request could not be read. |
| 401 | invalid_api_key | No key, a wrong key, or a revoked or expired one. |
| 403 | insufficient_scope, plan_required, subscription_required, firm_suspended | The key or the firm may not do this. |
| 404 | not_found | No such item in this firm. |
| 409 | conflict, idempotency_in_progress, limit_reached | Clashes with something that exists or is still running. |
| 413, 415 | payload_too_large, unsupported_media_type | Body over 32 KB, or not JSON. |
| 422 | validation_error, idempotency_key_reused | A field breaks a rule (the message names it), or an Idempotency-Key was reused for a different request. |
| 429 | rate_limited | Over a limit. See Retry-After. |
| 500 | internal_error | Our fault. Retry with backoff. |
7. Endpoints
| Call | Scope | What it does |
|---|---|---|
GET /me | any | The firm, the key's scopes, the plan's limits. |
GET /projects | projects:read | Projects, newest first. Filters: status (active, on_hold, completed, cancelled), updated_since, featured. |
GET /projects/{id} | projects:read | One project with its stages, gates and deliverable counts. |
POST /projects | projects:write | Create a project, from raw fields or from a lead (lead_id). |
PATCH /projects/{id} | projects:write | Change name, summary, location, area or external_ref. Stages and gates cannot be changed through the API: they need a signer in Smartnatic. |
GET /portfolio | portfolio:read | The firm's public portfolio items. |
POST /leads | leads:write | Send a lead into the firm's CRM. |
GET /webhooks, POST /webhooks, DELETE /webhooks/{id} | webhooks:manage | Manage where events are sent. |
Every field and answer is described in the OpenAPI 3.1 document. Projects never include fees, rates, invoices or private files, and sample projects are never listed.
8. Example: show a firm's projects on property listings
A real-estate or property platform can show which firm designed a building, how far the design is and a public cover image, next to the listing. Ask the firm for a key with projects:read and portfolio:read; that works on the trial too.
curl "https://smartnatic.com/api/v1/projects?status=active&featured=true&locale=en" \
-H "Authorization: Bearer $SMARTNATIC_KEY"
Answer (shortened)
{
"data": [{
"id": "clxp41...", "code": "HA-012", "name": "Hills Villa", "status": "active",
"type": "villa", "location": "Dubai Hills Estate", "region": "Dubai",
"area": { "value": 420, "unit": "m2" },
"summary": "A courtyard house for a family of five.", "featured": true,
"cover_image_url": "https://.../projects/hills-villa.jpg",
"current_stage": { "id": "clxs7...", "code": "DD", "name": "Design Development", "state": "in_progress" },
"progress_pct": 46,
"stages": [{ "code": "SD", "name": "Schematic Design", "state": "approved", "progress_pct": 100,
"gates": [{ "name": "Client approval", "kind": "client_approval", "state": "passed" }],
"deliverables": { "total": 6, "approved": 6, "in_review": 0 } }],
"deliverables": { "total": 21, "approved": 9, "in_review": 3 },
"external_ref": null, "created_at": "2026-05-02T09:14:00.000Z", "updated_at": "2026-09-18T15:40:12.000Z"
}],
"next_cursor": null
}
Keep your copy fresh by asking only for what changed. updated_at moves when a project's details, stages or gates change. For instant updates, use webhooks instead of polling.
# Every 15 minutes: only what changed since the last run
curl "https://smartnatic.com/api/v1/projects?updated_since=2026-09-19T08:00:00Z" \
-H "Authorization: Bearer $SMARTNATIC_KEY"
# The firm's public portfolio, exactly as its own portfolio page shows it
curl "https://smartnatic.com/api/v1/portfolio" -H "Authorization: Bearer $SMARTNATIC_KEY"
9. Example: send design requests from a marketplace
When a property owner asks for design work on your platform, send it to the firm as a lead. It lands in the firm's CRM and the firm's owners are notified at once. Needs leads:write (paid plans). name and an email or phone number are required; everything else is optional.
curl -X POST https://smartnatic.com/api/v1/leads \
-H "Authorization: Bearer $SMARTNATIC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: design-request-58213" \
-d '{
"name": "Layla Haddad",
"email": "layla@example.com",
"phone": "+971 50 123 4567",
"property_type": "villa",
"location": "Dubai Hills Estate",
"area": 420,
"budget": { "amount": "45000000", "currency": "AED" },
"message": "Interior redesign of the ground floor and the garden pavilion.",
"source": "design-marketplace",
"external_ref": "design-request-58213"
}'
HTTP/1.1 201 Created
{ "data": { "id": "clxl9...", "status": "new", "source": "design-marketplace",
"external_ref": "design-request-58213", "title": "Layla Haddad, villa in Dubai Hills Estate", ... } }
# Sent again with the same Idempotency-Key (or the same source + external_ref):
HTTP/1.1 201 Created (Idempotent-Replayed: true) or HTTP/1.1 200 OK
# ... and still one lead in the firm's CRM.
Use source for your platform's own label, and external_ref for your id of the request: the pair is unique in each firm, so sending it twice never makes two leads. When the firm takes the job on, your platform can open the project from the lead (projects:write):
curl -X POST https://smartnatic.com/api/v1/projects \
-H "Authorization: Bearer $SMARTNATIC_KEY" \
-H "Content-Type: application/json" \
-d '{ "lead_id": "clxl9...", "type": "villa", "external_ref": "design-request-58213" }'
10. Example: follow stages in a CRM
A CRM or a client dashboard can move its own deal or record along as the design progresses. Register an endpoint (the firm can also do this in Settings, Integrations, API) and Smartnatic calls it when a stage changes state or a gate is passed.
curl -X POST https://smartnatic.com/api/v1/webhooks \
-H "Authorization: Bearer $SMARTNATIC_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://crm.example.com/hooks/smartnatic", "events": ["stage.changed", "gate.passed"] }'
# 201: { "data": { "id": "...", "events": [...], "secret": "whsec_..." } } <- store the secret now
What your endpoint receives
POST /hooks/smartnatic HTTP/1.1
Content-Type: application/json
Smartnatic-Event: stage.changed
Smartnatic-Delivery: clxd0...
Smartnatic-Signature: t=1790000000,v1=5d41402abc4b2a76b9719d911017c592...
{
"id": "evt_4f1c...", "type": "stage.changed", "created_at": "2026-09-19T10:02:11.000Z",
"firm_id": "clx8f2...",
"data": {
"project_id": "clxp41...", "project_code": "HA-012",
"stage": { "id": "clxs7...", "code": "DD", "name": "Design Development",
"from_state": "in_progress", "to_state": "in_review", "progress_pct": 80 }
}
}
11. Webhooks
| Event | When |
|---|---|
project.created | A project is created in the firm (in Smartnatic or through the API). |
project.updated | A project's details change. data.changed lists which fields. |
stage.changed | A stage changes state (for example in_progress to in_review), with from_state and to_state. |
gate.passed | A gate (a review, a client approval, an authority decision) is passed. |
deliverable.approved | A deliverable is approved. |
lead.created | A lead is created in the firm's CRM, whichever way it came in. |
ping | The firm pressed Send test event. |
- Every event is a
POSTof{"id", "type", "created_at", "firm_id", "data"}with the headersSmartnatic-Signature,Smartnatic-EventandSmartnatic-Delivery. - Answer with any
2xxwithin 10 seconds. Redirects are not followed and count as a failure. - Failed deliveries are retried after about 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. Retries of one delivery carry the same
Smartnatic-Deliveryid: use it to ignore a delivery you already handled. - After 20 failures in a row the endpoint is switched off and the firm's owners are told; they can switch it on again once it is fixed.
- Events carry no personal data: leads come without name, email, phone or message. Read the details through the API if the key allows it.
- Endpoints must be
https://on a public address. Webhooks are available to firms on a paid plan, up to 5 endpoints each.
12. Verifying signatures
Each endpoint has its own signing secret (whsec_...), shown once when the endpoint is created or its secret is rotated. The header looks like t=1790000000,v1=5d41...: t is the time of sending in Unix seconds, v1 is the hex HMAC-SHA256 of t + "." + raw body with the secret. Check it on the raw body, before parsing, compare in constant time, and refuse anything older than 5 minutes.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.SMARTNATIC_WEBHOOK_SECRET; // whsec_...
// Verify against the RAW body, before any JSON parsing.
app.post("/hooks/smartnatic", express.raw({ type: "application/json" }), (req, res) => {
const parts = Object.fromEntries(
(req.get("Smartnatic-Signature") || "").split(",").map((p) => p.split("="))
);
const t = Number(parts.t);
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
const valid =
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - t) <= 300; // refuse old (replayed) deliveries
if (!valid) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
// Deduplicate on req.get("Smartnatic-Delivery"): a retry carries the same id.
if (event.type === "stage.changed") {
// update the deal / listing with event.data.stage
}
res.sendStatus(200); // answer fast; do slow work after replying
});
PHP
<?php
$secret = getenv('SMARTNATIC_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input'); // the RAW body
$header = $_SERVER['HTTP_SMARTNATIC_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $pair) {
[$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
$parts[trim($k)] = trim($v);
}
$t = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - $t) > 300) {
http_response_code(400);
exit('bad signature');
}
$event = json_decode($body, true);
// Deduplicate on $_SERVER['HTTP_SMARTNATIC_DELIVERY']: a retry carries the same id.
if ($event['type'] === 'stage.changed') {
// update the record with $event['data']['stage']
}
http_response_code(200);
13. Security
- Keys are stored only as a SHA-256 fingerprint; they are shown once and can be revoked by the firm at any moment.
- Webhook addresses are checked on every delivery: https only, and never a private, loopback, link-local or reserved address, whatever the host name resolves to.
- Every change made through the API is recorded in the firm's API log (never the request body) and kept 90 days.
- Found a security problem? Write to hello@smartnatic.com.