• Virtual Marketer CRM logo
    Virtual Marketer CRMDocumentation
  • Introduction
Getting Started
  • Quickstart
  • Core Concepts
Connect your AI
  • Custom connector
  • CLI & editors
  • Rate limits
Integrations
  • MCP
  • Webhooks
  • Website leads
  • OpenAPI 3.1.0
  • N8N
  • WhatsApp API
App guide
  • Dashboard
  • Inbox
  • Records
  • Profile
  • Company
  • Lead sending
  • API Keys
  • Filter Syntax
Licensing & security
  • Architecture & Security
  • Go back
  1. Introduction
  2. Website leads

Website leads

A form on your own site creating contacts in the CRM — which endpoint, which key, which fields, and what the CRM deliberately does not do for you.

A newsletter form, a gated download, a contact form: the sign-up should land as a contact in your workspace. It can, in a single call — but not through a webhook, and not with your own API key. This page describes the route we recommend, and names the three things that deliberately do not exist for it.

The direction is the opposite of what people expect

Webhooks point out of the CRM. You subscribe to events like contact.created and the CRM calls your endpoint. There is no endpoint a form could post to without authenticating — that would be an open write surface onto your records, which is why it does not exist.

So your server calls the CRM, not the other way round: POST /api/v1/contacts with the x-api-key header. The form itself never calls the CRM directly — the key belongs on your server and must not reach the browser.

Step 1: a user of its own for the form

Do not use your personal key. An API key inherits the permissions of its user, and per-key permissions do not exist. A key cut from your account can do everything you can — on a web server that is more trust than a sign-up form needs.

Create a role of its own under Company → Roles instead, invite a user for it, and generate the key as that user.

Know in advance how narrow the role can get, because one limit is surprising.

The role editor grants create, edit and delete separately per area, plus read access (none / own / all). A create-only role is therefore a matter of ticking one box.

And read access none does not work. Someone who may not read everything has to assign every record they create to themselves — otherwise they would create a record their own author can no longer see. Creating then fails with 400 and "You must assign yourself to this item" at the userIds field. For a form that is the wrong direction: the lead should belong to sales, not to the web server.

So the narrowest role a form can carry is:

  • Contacts: create yes, edit no, delete no, read access all.
  • Everything else off.

Then, signed in as that user: Profile → API & Connectors → Add. The key is shown once, belongs in an environment variable on your server, and not in your repository. Details under API keys.

What that is worth in an incident, and what it is not: a key cut this way sees no deals, organizations, invoices or mailboxes, and you can delete it without anything else stopping. It does read every contact, though, and it can update and delete them too, because the editor does not separate those — your whole contact base hangs on the key sitting on your web server. If that is more than you want to expose, put the write behind a small service of your own that only lets creates through.

Step 2: create the contact

curl -X POST https://crm.virtual-marketer.de/api/v1/contacts \
     -H "x-api-key: $VM_CRM_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "firstName": "Jana",
       "lastName": "Beck",
       "notes": "Requested the media kit on 2026-09-17, form /media-kit",
       "identifiers": [
         { "provider": "mail", "value": "jana.beck@example.de" }
       ]
     }'

201 with the created contact, 400 with a readable validation error.

What to know about it:

  • firstName must not be blank; lastName is required and may be empty. A form with a single "Name" field writes the whole name into firstName and "" into lastName — that is valid, and more honest than guessing a surname.
  • Email address and phone number are not contact fields, they are channels. They go into identifiers as { provider, value }, with provider one of mail, google, outlook, whatsapp. This is not just a different spelling: when the same person later writes to a connected mailbox or sends a WhatsApp message, the inbox attaches it to exactly this contact. An address that only sits in notes does not do that.
  • A value that cannot be normalized is a 400, and then nothing is created at all. Validate the format on your side before sending, or you lose the sign-up entirely.
  • Custom columns go into customFieldValues as { columnId, value }. The ids come from GET /api/v1/contacts/configuration; for a select column, value is the option's UUID, not its label. Select columns that have a default option are filled by the CRM on create, so a lead lands in the first pipeline stage without your form sending it.

Step 3: the same address twice

The CRM deduplicates on its own, but only by channel: an email address or a WhatsApp number can hang off exactly one contact. A second POST with an address that is already linked is therefore a 400:

✖ This channel is already linked to another contact.
  → at identifiers[0].value

That is not an outage, it is the answer "we know this one already". Handle exactly this case separately: do not retry it, do not report it as a failure — the person is already in the CRM. Any other 400 is a real error in your data; a 5xx or a timeout is worth retrying.

What follows from that:

  • You do not need a lookup first for data quality. It is only worth doing if your page wants to answer the sender differently ("you are already on the list"): POST /api/v1/contacts/search with { "searchTerm": "jana.beck@example.de" }.
  • The second sign-up is not taken over. If the same person submits the form again, with a new phone number or a different request, nothing is created and nothing changes. Record it on your side, or let a second user with update rights follow up — deliberately not the same key as the one on the web server.
  • Deduplication is by channel, not by name. The same person with two addresses becomes two contacts.

What the CRM does not do here

  • It sends no notification mail when a record is created. The "new lead" mail is your own server's job in the same request — it already holds the data. If you would rather send it from an automation, subscribe to contact.created with a webhook and trigger it there.
  • It is not your queue. If the CRM is briefly unreachable, the sign-up must not be lost: store it on your side first, then send it, and retry on failure. A form that only forwards a sign-up to somebody else's API loses it on every network error.
  • It does not document consent. When somebody agreed, and to which text, is something you have to record yourself — either in your own system or in a custom column on the contact. Do not reuse a column that already means something else in your workspace.

Next

  • API keys: create, rotate, revoke.
  • OpenAPI: the full REST reference with every field.
  • Webhooks: the other direction — CRM events to your systems.
The direction is the opposite of what people expect
Step 1: a user of its own for the form
Step 2: create the contact
Step 3: the same address twice
What the CRM does not do here
Next