Developers

REST API

JSON over HTTPS.

Basics

Base URLhttps://app.dmhandled.com/api/v1/
AuthenticationAuthorization: Bearer dmh_cust_… — a key from Settings → API & MCP
FormatJSON in (Content-Type: application/json), JSON out
Errors{"error": {"code": "…", "message": "…"}} with a 4xx status: 400 invalid · 401 no or wrong key · 402 not on your plan · 404 not found · 415 send JSON · 429 slow down
Limits120 writes a minute and 3,000 an hour per key; a 429 carries Retry-After
curl https://app.dmhandled.com/api/v1/whoami \
  -H "Authorization: Bearer $DMH_KEY"

Customers and consents

A customer is identified by your own id — the {ref} in the path. A consent is identified by purpose + given_at + source: send it again with withdrawn_at to withdraw it.

PUT /contacts/{ref} — create or update

curl -X PUT https://app.dmhandled.com/api/v1/contacts/1042 \
  -H "Authorization: Bearer $DMH_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Ana García",
    "email": "ana@example.com",
    "phone": "+34600111222",
    "lang": "es",
    "email_confirmed": false,
    "phone_confirmed": false,
    "consents": [{
      "purpose": "reply_whatsapp",
      "granted": true,
      "text": "I agree that Flamingo Real Estate may reply to my enquiry on WhatsApp.",
      "lang": "en",
      "given_at": "2026-09-28T10:00:00Z",
      "source": "website_form"
    }]
  }'

Returns the customer (201 when new, 200 when updated). Fields you leave out are unchanged; the consent fields marked * are required.

FieldMeaning
phoneE.164, e.g. +34600111222
dmh_idFor a customer first met in the chat ("ref": null in an event): their dmh_id attaches your {ref} to that contact
email_confirmed, phone_confirmedtrue when your system has already proven the address or number is theirs. Otherwise the chat checks it before using it.
consents[].purpose *reply_email · reply_whatsapp · future_email · future_whatsapp
consents[].text *The exact words the person agreed to
consents[].given_at *ISO 8601 date-time
consents[].source *Where it was given, e.g. website_form
consents[].withdrawn_at, withdrawn_bySet when it was withdrawn

An active reply_whatsapp or future_whatsapp consent with a phone number lets your team send that person an approved WhatsApp template after the 24-hour window.

GET /contacts/{ref} · GET /contacts?email=… · GET /contacts?phone=…

{
  "ref": "1042", "dmh_id": 311, "name": "Ana García",
  "email": "ana@example.com", "email_confirmed": true,
  "phone": "+34600111222", "phone_confirmed": false, "lang": "es",
  "created_at": "…", "updated_at": "…",
  "consents": [{"purpose": "reply_whatsapp", "granted": true, "text": "…", "lang": "en",
                "given_at": "2026-09-28T10:00:00+00:00", "source": "website_form",
                "withdrawn_at": null, "withdrawn_by": null, "origin": "api", "active": true}]
}

origin is api for what you sent and chat for what the chat collected. A person first met in the chat has "ref": null and a dmh_id.

DELETE /contacts/{ref}

Erases the customer and their consents. Returns 204.

The same-browser pass

After a visitor sends your form, your server asks for a pass and puts it on that visitor's page. The chat then knows who it is talking to and can offer back the details you hold. A pass lasts 30 days.

curl -X POST https://app.dmhandled.com/api/v1/contacts/1042/identity \
  -H "Authorization: Bearer $DMH_KEY"
# → {"token": "eyJ…", "expires_at": "2026-10-28T10:00:00+00:00"}

Put it on the page before the chat script loads, for that visitor only:

<script>
  window.DMHandled = Object.assign(window.DMHandled || {}, { identity: "eyJ…" });
</script>

Keep your API key on your server; never put it in a web page.

Knowledge (listings, FAQs, prices)

Push what the assistant answers from, by your own id, whenever it changes.

# add or update one piece
curl -X PUT https://app.dmhandled.com/api/v1/knowledge/listing-88 \
  -H "Authorization: Bearer $DMH_KEY" -H "Content-Type: application/json" \
  -d '{"heading": "Villa Mar", "text": "Villa Mar has three bedrooms, a sea view and a heated pool. Price: 2.1 million euros.", "url": "https://example.com/villa-mar"}'

# remove it
curl -X DELETE https://app.dmhandled.com/api/v1/knowledge/listing-88 -H "Authorization: Bearer $DMH_KEY"

# up to 50 at once
curl -X POST https://app.dmhandled.com/api/v1/knowledge/bulk \
  -H "Authorization: Bearer $DMH_KEY" -H "Content-Type: application/json" \
  -d '{"items": [
        {"external_ref": "listing-88", "heading": "Villa Mar", "text": "Villa Mar has three bedrooms and a sea view."},
        {"external_ref": "listing-89", "heading": "Casa Sol", "text": "Casa Sol has four bedrooms and a private pool."}]}'

# what is there
curl "https://app.dmhandled.com/api/v1/knowledge?external_ref=listing-88" -H "Authorization: Bearer $DMH_KEY"

Text is 15 to 8,000 characters per piece; an account holds up to 5,000 pieces.

Events

The same events the webhooks deliver, in order — to catch up, or instead of webhooks.

curl "https://app.dmhandled.com/api/v1/events?after=0&limit=100" -H "Authorization: Bearer $DMH_KEY"
# → {"events": [{"id": 57, "event": "consent.granted", "at": "…", "data": {…}}, …],
#    "next_after": 57, "more": false}

Keep the last id you processed and pass it as after next time. At most 200 per page.

Webhook endpoints

# register (the secret is shown ONCE — store it)
curl -X POST https://app.dmhandled.com/api/v1/webhooks \
  -H "Authorization: Bearer $DMH_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/dmh/webhook", "events": ["contact.confirmed", "consent.granted", "consent.withdrawn", "handover.created"]}'

curl https://app.dmhandled.com/api/v1/webhooks -H "Authorization: Bearer $DMH_KEY"
curl -X DELETE https://app.dmhandled.com/api/v1/webhooks/12 -H "Authorization: Bearer $DMH_KEY"

Leave events out to receive everything. Up to 10 endpoints per account; https only. How to verify what you receive: Webhooks.