Base URL
https://app.dmu.email/api/v1
Authentication
Bearer dmk_… API key
Rate limit
120 requests a minute per key

Update a contact by email

PUT/contacts/by-email/{email}

Creates the contact with an address, or changes the one that has it. This is the call that keeps a CRM in step.

Needs the permissioncontacts:write

It matches the shared contacts, or with visibility: "personal", on a key that includes them, the key’s person’s own. A new contact answers 201 Created, a changed one 200.

Put the address in the path URL-encoded once, so anna@papadakis-acc.gr is anna%40papadakis-acc.gr. The body changes only the fields it names, as Update a contact does.

A field you leave out is untouched, and null or an empty string clears it. Text is put on one line and trimmed. A custom text field holds up to 1,000 characters.

Custom fields by kind: a Number takes a comma as the decimal point too; a Date starts YYYY-MM-DD, and anything after is dropped; a Choice matches one of its choices, ignoring case; a Person takes a colleague’s address or user id and is returned as their address. A key your organisation doesn’t have fails the whole item.

What the API writes is locked in the app, which shows it read only: the CRM is the source of truth. Sending the value a field already has doesn’t lock it, and clearing a field with null unlocks it.

Shared and personal contacts are kept apart, so the same address can be one shared contact and one personal one.

Path parameters

emailstringrequired
The contact’s address.

Body

emailslist of stringsoptional
Replaces every address, up to 10. Repeats are dropped, ignoring case. The first is its key.
phonestringoptional
Replaces the first phone number only.
phoneslist of stringsoptional
Replaces every phone number, up to 10, each up to 50 characters of digits, spaces and + ( ) - . / # * x.
firstNamestringoptional
Or lastName, company, jobTitle: up to 200 characters each.
fieldsobjectoptional
Custom fields by key, merged one by one: a key you leave out is untouched. Each must already exist in DMU Mail. See the notes for each kind.
visibilitystringoptional
shared shares a personal contact. Nothing makes a shared one personal again.

Returns

The contact as data, and beside it:

createdboolean
Whether it was added.
changedboolean
Whether anything differed. False when the contact already read that way.

Errors

400invalid
A value is wrong, such as an address or a phone number. The error’s field says which.
400unknown_field
fields has a key your organisation doesn’t have. fields lists them.
401invalid_key
The key is wrong, expired or revoked. With no Authorization header at all, the code is missing_credentials.
403missing_scope
The key doesn’t have contacts:write. The X-Required-Scope header names what’s missing.
403personal_not_allowed
A personal contact, on a key that doesn’t include personal contacts.
409one_way
A shared contact can’t be made personal again.
413too_large
The request body is over 256 KB.
429rate_limited
More than 120 requests a minute from this key. Wait for Retry-After seconds.
curl https://app.dmu.email/api/v1/contacts/by-email/anna%40papadakis-acc.gr \
  -X PUT \
  -H "Authorization: Bearer dmk_7f3a9c2e1b4d_Xq8v…" \
  -H "Content-Type: application/json" \
  -d '{
    "company": "Λογιστήριο Παπαδάκη",
    "fields": { "customer_number": "ΠΕΛ-0412" }
  }'

Response

200 OK

{
  "data": {
    "id": "3d7a51c2",
    "visibility": "shared",
    "source": "api",
    "firstName": "Άννα",
    "lastName": "Παπαδάκη",
    "company": "Λογιστήριο Παπαδάκη",
    "jobTitle": "Accountant",
    "email": "anna@papadakis-acc.gr",
    "emails": ["anna@papadakis-acc.gr"],
    "phones": ["210 412 3380"],
    "fields": {
      "vat_number": "094536781",
      "customer_number": "ΠΕΛ-0412"
    },
    "locked": [
      "first_name",
      "last_name",
      "company",
      "job_title",
      "email",
      "phone",
      "vat_number",
      "customer_number"
    ],
    "createdAt": "2026-10-25T06:12:40.000Z",
    "updatedAt": "2026-10-25T06:31:05.000Z"
  },
  "created": false,
  "changed": true
}

Every response wraps its result in data. A page of threads adds nextCursor, which is null on the last page.