Developers

The Smartnatic API.
Connect your platform to design firms.

Architecture, engineering and interior design firms run their projects in Smartnatic. With a key from a firm, your platform can show that firm's projects and progress, send it new design requests, and hear the moment a stage moves on. REST and JSON, signed webhooks, and an OpenAPI description you can generate a client from.

Free for partnersBase URL https://smartnatic.com/api/v1Signed webhooks
The short version Free for partners. Firms on a paid plan get 120 requests a minute and 5,000 a day; trial firms get read-only access with 1,000 requests a day. Need more? Contact us.

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.

  1. The firm's owner (or a principal) opens Settings, Integrations, API in Smartnatic.
  2. They name the key after your platform, tick the access it needs (see scopes) and press Create key.
  3. 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

ScopeAllowsPlans
projects:readList and read projects: type, location, area, summary, public cover image, stages with progress, gates and deliverable counts.Paid and trial
portfolio:readThe firm's public portfolio, exactly what its own public portfolio page shows.Paid and trial
leads:writeSend a lead into the firm's CRM. The firm's owners are notified.Paid
projects:writeCreate a project (from raw fields or from a lead) and change its name, summary, location, area and external reference.Paid
clients:readAdds the client's name and contacts to projects. Personal data: off unless the firm ticks it.Paid
webhooks:manageList, 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 planFirm on the free trial
ScopesAllRead-only: projects:read, portfolio:read
Requests per key120 a minute, 5,000 a day120 a minute, 1,000 a day
Webhook endpointsUp to 5None

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 a next_cursor: pass it back as ?cursor=. The last page has next_cursor: null.
  • Safe retries. Add an Idempotency-Key header (8 to 120 characters) to POST /leads and POST /projects. Sending the same key with the same body again returns the first result, with Idempotent-Replayed: true, instead of creating a second one. Keys are remembered for 24 hours. Leads and projects also take your own external_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." } }
StatusCodeMeaning
400invalid_json, invalid_cursor, invalid_idempotency_keyThe request could not be read.
401invalid_api_keyNo key, a wrong key, or a revoked or expired one.
403insufficient_scope, plan_required, subscription_required, firm_suspendedThe key or the firm may not do this.
404not_foundNo such item in this firm.
409conflict, idempotency_in_progress, limit_reachedClashes with something that exists or is still running.
413, 415payload_too_large, unsupported_media_typeBody over 32 KB, or not JSON.
422validation_error, idempotency_key_reusedA field breaks a rule (the message names it), or an Idempotency-Key was reused for a different request.
429rate_limitedOver a limit. See Retry-After.
500internal_errorOur fault. Retry with backoff.

7. Endpoints

CallScopeWhat it does
GET /meanyThe firm, the key's scopes, the plan's limits.
GET /projectsprojects:readProjects, newest first. Filters: status (active, on_hold, completed, cancelled), updated_since, featured.
GET /projects/{id}projects:readOne project with its stages, gates and deliverable counts.
POST /projectsprojects:writeCreate a project, from raw fields or from a lead (lead_id).
PATCH /projects/{id}projects:writeChange name, summary, location, area or external_ref. Stages and gates cannot be changed through the API: they need a signer in Smartnatic.
GET /portfolioportfolio:readThe firm's public portfolio items.
POST /leadsleads:writeSend a lead into the firm's CRM.
GET /webhooks, POST /webhooks, DELETE /webhooks/{id}webhooks:manageManage 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

EventWhen
project.createdA project is created in the firm (in Smartnatic or through the API).
project.updatedA project's details change. data.changed lists which fields.
stage.changedA stage changes state (for example in_progress to in_review), with from_state and to_state.
gate.passedA gate (a review, a client approval, an authority decision) is passed.
deliverable.approvedA deliverable is approved.
lead.createdA lead is created in the firm's CRM, whichever way it came in.
pingThe firm pressed Send test event.
  • Every event is a POST of {"id", "type", "created_at", "firm_id", "data"} with the headers Smartnatic-Signature, Smartnatic-Event and Smartnatic-Delivery.
  • Answer with any 2xx within 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-Delivery id: 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.