Skip to content

🤝 CRM API & Webhooks

Connect any tool to a store's Customers 360: read contacts merged across every channel (orders, quotes, invoices, WhatsApp, contact forms, newsletter, visits), with where each lead came from (Facebook post, YouTube video, Google search, e-mail campaign, affiliate, QR code…) and the next best action; push leads and activities from your forms, lead ads or another CRM; read the deal pipeline; ask your numbers in plain language; and receive signed webhooks as things happen.

Designed for server-to-server integrations: Zapier, Make, n8n, an ERP, a BI tool, a spreadsheet script. It complements the Clients API (storefront, customer token) and the v2.0 APIs.

Base URL

As with the Clients API, the base URL is the store's own domain (sub-domain or custom domain):

https://<store-domain>/api/crm/v1 — for example https://test10.devaito.com/api/crm/v1

OpenAPI 3.1

GET /openapi.json (no key needed) returns the machine-readable description of every endpoint: import it into Postman or Insomnia, or generate a client.


🔑 Authentication

  1. In the store admin, open Customers 360 › API & webhooks (/admin/customers-360/api).
  2. Click Create a key, name it after the tool that will use it, and tick only the permissions it needs.
  3. Copy the key: it is shown once. Devaito keeps only a SHA-256 fingerprint of the secret. You can revoke a key at any time; the page shows when each key was last used.

Keys look like dvk_<10 hex>_<32 hex>. Send them in either header:

Authorization: Bearer dvk_3f9a1c0b7e_6d2e8f...
# or
X-Api-Key: dvk_3f9a1c0b7e_6d2e8f...

A key belongs to one store and only works on that store's domain.

Permissions

PermissionAllows
contacts:lireGET /contacts, GET /contacts/{id}
leads:ecrirePOST /leads
activites:ecrirePOST /activities
affaires:lireGET /deals
questions:lireGET /ask
objets:lireGET /objects, GET /objects/{object}/records, GET /objects/{object}/records/{id} (Studio objects)
objets:ecrirePOST /objects/{object}/records, POST /links (Studio objects)

Limits and language

  • 120 requests per minute; beyond that the API answers 429.
  • Send Accept-Language: fr, en or ar to get the human-readable fields (source labels, next action, timeline titles, answers) in that language. Default: English.
  • Responses are JSON. Dates use YYYY-MM-DD HH:MM:SS; the webhook envelope's created_at is ISO 8601.

Errors

Every error has the same shape:

{
  "error": {
    "code": "missing_scope",
    "message": "This key does not have the \"leads:ecrire\" permission."
  }
}
StatusCodeWhen
401invalid_keyMissing, wrong or revoked key
403missing_scopeThe key lacks the permission of this endpoint
404unknown_storeNo store answers on this domain
404not_foundUnknown contact id
422validation_errorInvalid body (e.g. a lead without e-mail or phone, a field sent as an array)
422not_understood/ask: the question could not be turned into numbers
429—Rate limit reached: retry after the delay in the Retry-After header
503store_unavailableThe store is asleep or temporarily unavailable
402quota_reachedStudio objects: the plan's limit is reached; details.lien is the page to choose a plan
409read_onlyStudio objects: built-in objects (customer, product, order, booking, contact, quote) are read only
404not_enabledStudio objects are not enabled for this store

👥 Contacts

A contact is one person, even if they ordered as a guest, sent a quote request with another phone number, wrote on WhatsApp and filled a contact form: Customers 360 merges every trace by e-mail and phone.

GET /contacts

Permission contacts:lire. Most recent activity first.

Query parameterDescription
limit1 to 100, default 50
cursorValue of next_cursor from the previous page
qSearch in name, e-mails and phones
segmenttous (all, default) · a_relancer (has a next action) · devis (open quote or quote request) · meilleurs (has revenue) · nouveaux (first contact in the last 30 days) · inactifs (customers inactive for 90 days or more)
updated_sinceOnly contacts with activity since this date (ISO 8601)

Request:

curl https://test10.devaito.com/api/crm/v1/contacts?segment=a_relancer&limit=2 \
  -H "Authorization: Bearer $DEVAITO_KEY" \
  -H "Accept-Language: en"

Example response:

{
  "data": [
    {
      "id": "e7a9abb64e12fa437",
      "name": "Yasmine Alaoui",
      "emails": [
        "yasmine.alaoui@example.com"
      ],
      "phones": [
        "+212612345678"
      ],
      "registered": false,
      "source": {
        "key": "facebook",
        "channel": "pub",
        "detail": null,
        "campaign": "autumn-collection",
        "label": "Facebook ad · Autumn collection",
        "tracked": true
      },
      "orders_count": 2,
      "paid_orders_count": 2,
      "revenue": 1840.0,
      "currency": "MAD",
      "open_quotes_amount": 0,
      "unpaid_amount": 0,
      "channels": [
        "site",
        "whatsapp",
        "email"
      ],
      "first_contact_at": "2026-05-14 10:21:07",
      "last_activity_at": "2026-07-06 18:02:44",
      "next_action": {
        "code": "reactiver",
        "text": "Win them back with an offer",
        "why": "2 orders, nothing for 94 days."
      },
      "url": "https://test10.devaito.com/admin/customers/e7a9abb64e12fa437/360"
    }
  ],
  "total": 37,
  "next_cursor": "2"
}
FieldDescription
sourceWhere the person first came from. key: facebook, instagram, youtube, tiktok, google, google_maps, chatgpt, email, affilie (affiliate), qr, whatsapp, prospection, api, airbnb, booking, agent (callback left to your assistant), import, direct… channel: pub (paid ad), social, recherche (search), local, ia (AI assistant), email, affiliation, messagerie, ota (booking platform)… tracked is true when the visit itself was observed on the site.
channelsEvery channel the person used: site, whatsapp, email, phone, quote, reservation…
bookings_count, nights, next_booking_atHotels, rentals, tours, appointments: bookings (cancelled and expired holds excluded), nights stayed, next arrival
next_actionWhat to do now, and why (the same suggestion the merchant sees in Customers 360), or null
urlThe contact's 360° page in the store admin

GET /contacts/{id}

Permission contacts:lire. The same fields plus timeline (latest 100 events: visits, forms, orders, payments, quotes, messages, activities) and deals. An old id (before two records were merged) still finds the person. Postal addresses and internal notes are never returned.

{
  "data": {
    "id": "e7a9abb64e12fa437",
    "name": "Yasmine Alaoui",
    "emails": [
      "yasmine.alaoui@example.com"
    ],
    "phones": [
      "+212612345678"
    ],
    "source": {
      "key": "facebook",
      "channel": "pub",
      "detail": null,
      "campaign": "autumn-collection",
      "label": "Facebook ad · Autumn collection",
      "tracked": true
    },
    "...": "...",
    "timeline": [
      {
        "at": "2026-07-06 18:02:44",
        "type": "commande",
        "title": "Order #10482",
        "detail": "Cash on delivery · delivered",
        "amount": 920.0
      },
      {
        "at": "2026-05-14 10:24:51",
        "type": "visite",
        "title": "Left their details",
        "detail": "Form /contact",
        "amount": null
      },
      {
        "at": "2026-05-14 10:21:07",
        "type": "visite",
        "title": "Arrived from Facebook ad · Autumn collection",
        "detail": "Page /",
        "amount": null
      }
    ],
    "deals": [
      {
        "number": "Q-2026-0042",
        "stage": "accepte",
        "won": true,
        "lost": false,
        "amount": 920.0
      }
    ]
  }
}

🧲 Leads

POST /leads

Permission leads:ecrire. Sends a lead from any tool (Typeform, Facebook Lead Ads, Google Ads lead forms, a landing page, another CRM). It appears in Customers 360 and in Audience › Leads with its source, the merchant gets a next action, and the lead.created webhook fires.

FieldDescription
email or phoneRequired: at least one (phone: 8 digits or more)
name, company, cityOptional
messageWhat the person asked
sourceWhere the lead comes from, shown to the merchant, e.g. typeform, facebook-lead-ads
external_idYour id for this lead. Makes the call idempotent: sending the same id again updates the lead instead of creating a duplicate; only the fields you send are changed, and the status the merchant gave the lead is kept
curl -X POST https://test10.devaito.com/api/crm/v1/leads \
  -H "Authorization: Bearer $DEVAITO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Karim Benali","email":"karim@example.com","phone":"+212 6 61 23 45 67","company":"Atlas Bâtiment","source":"typeform","message":"Quote for 40 m²","external_id":"tf-88123"}'

201 when created, 200 when the same external_id was updated:

{
  "data": {
    "id": "lead_812",
    "created": true
  }
}

📞 Activities

POST /activities

Permission activites:ecrire. Logs a call, e-mail, task, meeting or note on the person found by e-mail or phone (from your telephony, help desk or calendar). It appears in the contact's timeline.

FieldDescription
email or phoneRequired: who the activity is about
typecall, email, task, meeting or note (default)
subject, content, statusOptional text
atWhen it happened (ISO 8601), default now
external_idIdempotency: the same id updates the activity
curl -X POST https://test10.devaito.com/api/crm/v1/activities \
  -H "Authorization: Bearer $DEVAITO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"karim@example.com","type":"call","subject":"Discovery call","content":"Wants delivery before November","at":"2026-10-09T15:30:00+01:00","external_id":"call-5531"}'
{
  "data": {
    "id": "activity_4410"
  }
}

💼 Deals

GET /deals

Permission affaires:lire. The pipeline built from facts, not from manual updates. stage: demande (quote requested) → cree (drafted) → envoye (sent) → ouvert (viewed by the customer) → accepte → paye. The forecast weights each open deal by the probability of its stage — learned from the store's own history when there is enough of it.

{
  "data": {
    "open": [
      {
        "id": "2981",
        "number": "Q-2026-0051",
        "contact_id": "e5b1c0d9a2",
        "contact_name": "Atlas Bâtiment",
        "stage": "ouvert",
        "won": false,
        "lost": false,
        "source": "devaito",
        "amount": 48000.0,
        "currency": "MAD",
        "created_at": "2026-10-02",
        "viewed_at": "2026-10-03 09:12:40",
        "accepted_at": null
      }
    ],
    "won": [
      "..."
    ],
    "lost": [
      "..."
    ],
    "summary": {
      "open_amount": 212400.0,
      "weighted_forecast": 96300.0,
      "won_this_month": 31500.0,
      "win_rate": 0.4167,
      "avg_days_to_sign": 6.5,
      "currency": "MAD",
      "stage_probabilities": {
        "demande": 0.1,
        "cree": 0.2,
        "envoye": 0.3,
        "ouvert": 0.5,
        "accepte": 0.9,
        "paye": 1
      },
      "probabilities_from_history": true
    }
  }
}

💬 Ask your numbers

GET /ask

Permission questions:lire. A question in French, English or Arabic becomes a precise query on the store's data — sales, orders, customers, products, quotes, invoices, visits, leads, by period, month, source, city… The answer says exactly how it was calculated. Through the API, questions are understood by rules only: no model, no cost.

curl "https://test10.devaito.com/api/crm/v1/ask?q=revenue%20by%20month%20this%20year" \
  -H "Authorization: Bearer $DEVAITO_KEY"
{
  "data": {
    "question": "revenue by month this year",
    "understood_as": {
      "sujet": "ventes",
      "groupe": "mois",
      "periode": {
        "cle": "annee"
      },
      "filtre": [],
      "limite": 10,
      "ordre": "desc"
    },
    "answer": "You collected 184,250 MAD this year. Best month: Sep (31,900 MAD).",
    "value": 184250.0,
    "change_pct": null,
    "period": "this year",
    "series": [
      {
        "label": "Aug",
        "value": 27400.0,
        "display": "27,400 MAD"
      },
      {
        "label": "Sep",
        "value": 31900.0,
        "display": "31,900 MAD"
      }
    ],
    "series_type": "temps",
    "how_it_is_calculated": "Paid orders (cancelled excluded) by order date, plus paid invoices without an order, in the store currency."
  }
}

Other examples: new customers this month, leads by source last 30 days, unpaid invoices, bookings by channel this year, nights this quarter, chiffre d'affaires par ville ce trimestre, عدد الطلبات هذا الأسبوع.


Studio objects

Your own kinds of records — enrolments, members, rentals, tickets — defined in Studio › Objects (or by your copilot), next to the store's built-in objects. Each object has typed fields (texte, texte_long, nombre, montant, date, datetime, booleen, choix, fichier, lien); up to 6 are indexed (filterable and sortable). Built-in objects (client, produit, commande, reservation, contact, devis) are listed and readable, never written. Record ids are 26-character ULIDs. JSON shapes: Object, Record, RecordWrite, Link, LinkWrite in /openapi.json.

GET /objects

Permission objets:lire. Every object of the store with its fields (key, type, indexed, personal, required, choices, object for links).

GET /objects/{object}/records

Permission objets:lire. Filters on indexed fields (or status, customer_id, created_at, updated_at), one sort, 100 records per page at most.

GET /api/crm/v1/objects/enrolment/records?filter[level]=B1&filter[start][gte]=2026-10-01&sort=-start&per_page=50

The response has data (records: id, object, version, title, status, customer_id, values, dates) and meta (page, per_page, total, total_exact, pages).

GET /objects/{object}/records/{id}

Permission objets:lire. One record and its links (direction out or in).

POST /objects/{object}/records

Permission objets:ecrire. Without id: creates (201). With id: updates only the values sent (200, version + 1). Values are checked against the fields (types, required fields, choice lists, links that must exist in this store); unknown keys are ignored.

POST /api/crm/v1/objects/enrolment/records
{"values": {"student": 482, "level": "B1", "start": "2026-10-12"}}

POST /links

Permission objets:ecrire. Links a Studio record to a Studio record or a built-in one; both must belong to this store. Idempotent.

POST /api/crm/v1/links
{"from": {"object": "enrolment", "id": "01K…"}, "to": {"object": "client", "id": "482"}, "type": "student"}

🔔 Webhooks

In Customers 360 › API & webhooks, subscribe an HTTPS address to one or more events. Devaito POSTs a signed JSON to it within about a minute of each event. The Test button delivers a ping event right away, and every delivery can be inspected and re-sent from the same page.

Events

EventWhen
lead.createdA visitor leaves their e-mail or phone on the site (with where they came from), or a lead arrives through POST /leads
form.submittedA contact form of the site is sent (all fields)
order.createdA new order
order.paidAn order becomes paid
booking.createdA booking becomes real — confirmed or awaiting your confirmation, never an unpaid hold: hotels, rentals, tours, appointments. With dates, guests, channel (direct, airbnb, booking…), amount, currency and customer
quote.sent, quote.viewed, quote.acceptedA quote is sent, opened by the customer, accepted
invoice.paidAn invoice is paid
object.created, object.updated, object.statusA record of a Studio object is created, updated, or its status changes. data: object, id, version, status, previous_status, and record (title, status, values) read at delivery time

Payload

POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Devaito-Webhooks/1.0
X-Devaito-Event: lead.created
X-Devaito-Delivery: evt_90321
X-Devaito-Signature: t=1791561600,v1=5f2c9a…
{
  "id": "evt_90321",
  "type": "lead.created",
  "created_at": "2026-10-09T15:42:10+01:00",
  "store": "test10.devaito.com",
  "data": {
    "kind": "website",
    "name": "Yasmine Alaoui",
    "email": "yasmine.alaoui@example.com",
    "phone": "0612345678",
    "source": {
      "key": "youtube",
      "channel": "social",
      "detail": null,
      "campaign": "demo-video",
      "label": "YouTube · Demo video"
    },
    "landing_page": "/products/linen-shirt",
    "visits": 3,
    "form_page": "/contact",
    "created_at": "2026-10-09 15:41:58"
  }
}

Orders carry id, code, total, currency, paid, delivery_status, customer (name, e-mail, phone) and source (where the buyer came from, when known). Quotes and invoices carry number, status, total, currency and customer. Use id (also in X-Devaito-Delivery) to ignore a delivery you already processed.

Verify the signature

Each subscription has its own secret (whsec_…), shown when you create it. v1 is the HMAC-SHA256 of <t>.<raw body> with that secret. Compare in constant time and reject timestamps older than 5 minutes to block replays.

PHP

$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_DEVAITO_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, getenv('DEVAITO_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}
$event = json_decode($body, true);

Node.js (Express)

const crypto = require('crypto');
app.post('/devaito', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = Object.fromEntries(req.get('X-Devaito-Signature').split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.DEVAITO_WEBHOOK_SECRET)
    .update(`${sig.t}.${req.body}`).digest('hex');
  const ok = sig.v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1))
    && Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  res.sendStatus(200);
});

Delivery and retries

  • Answer with any 2xx within 8 seconds. Redirects are not followed.
  • Otherwise Devaito tries again after 1 min, 5 min, 30 min, 2 h and 6 h, then gives up on that delivery.
  • After 50 failures in a row the subscription is paused; once your endpoint is fixed, a successful Test from the page turns it back on.
  • Each event is delivered once per subscription; a subscription only receives events that happen after it was created.
  • Addresses must be public HTTPS (port 443 or 8443). Private, local and internal addresses are refused, at creation and before every delivery.

🧩 Recipes

  • Facebook / Google lead ads → Devaito: in Zapier or Make, trigger on the new lead, action “Webhooks › POST” to /leads with source = facebook-lead-ads and external_id = the lead id.
  • New paid order → accounting or ERP: subscribe to order.paid.
  • Sales team alerts: subscribe to lead.created and quote.viewed, forward to Slack or WhatsApp — the payload already says where the lead came from.
  • Calls from your telephony: POST /activities with type = call after each call.
  • Weekly reporting: a spreadsheet script calls /ask?q=revenue by source last week.