Why there is a step in between
KeyCRM's API needs an Authorization: Bearer header and its own JSON shape. Formgong webhooks send a fixed JSON body and cannot add custom headers. So a webhook pointed straight at KeyCRM would fail with 401.
- Make: free to start, no code. Custom webhook → HTTP Make a request.
- n8n: Webhook node → HTTP Request node. Good if you self-host.
- Your own script: a Cloudflare Worker or any HTTPS endpoint. The code is at the end of this page.
Prepare KeyCRM
- API key: Settings → General → API key → Generate API key. Only the CRM owner can refresh it. A refreshed key breaks every integration that used the old one.
- Source: add a source such as Website form, so you can see where leads come from.
GET /order/sourcelists source IDs. - Pipeline:
GET /pipelineslists pipeline IDs. Withoutpipeline_id, KeyCRM uses the first pipeline.
Lead or order? Use a pipeline card (POST /pipelines/cards) for enquiries; it needs only a contact. Use an order (POST /order) for a real purchase; it needs source_id and buyer.
Recipe with Make
- Set up the Custom webhook as in the Make recipe and send one real submission.
- Add a filter:
eventequal tosubmission.created. Test deliveries then never create cards. - Add HTTP → Make a request. URL
https://openapi.keycrm.app/v1/pipelines/cards, method POST. - Authentication type API key: create a keychain that sends the header
Authorizationwith the valueBearerplus your key. Add the headerAccept: application/json. - Body content type application/json. Choose input method Data structure: Make then escapes quotes and line breaks a visitor typed. Fill it as shown below.
- Set Parse response to Yes, run once and check the new card in KeyCRM.
{
"title": "{{1.form.name}}: {{1.submission.fields.name}}",
"source_id": 12,
"pipeline_id": 3,
"contact": {
"full_name": "{{1.submission.fields.name}}",
"email": "{{1.submission.fields.email}}",
"phone": "{{1.submission.fields.phone}}"
},
"manager_comment": "{{1.submission.fields.message}} | Page: {{1.submission.page_url}} | Formgong ID: {{1.submission.id}}",
"utm_source": "{{1.submission.fields.utm_source}}"
}Replace 12 and 3 with your source and pipeline IDs. Leave out keys you do not collect.
Recipe with n8n
- Set up the Webhook node as in the n8n recipe, with Only Run If
{{ $json.body.event === 'submission.created' }}. - Add an HTTP Request node: method POST, URL
https://openapi.keycrm.app/v1/pipelines/cards. - Authentication: Generic Credential Type → Header Auth, name
Authorization, valueBearerplus your key. - Turn on Send Body, content type JSON, Specify Body: Using JSON.
JSON.stringifykeeps quotes in messages from breaking the JSON. - In the node settings, turn on Retry On Fail to ride out a 429.
Body (n8n expression)
{
"title": {{ JSON.stringify($json.body.form.name + ": " + ($json.body.submission.fields.name ?? "")) }},
"source_id": 12,
"pipeline_id": 3,
"contact": {
"full_name": {{ JSON.stringify($json.body.submission.fields.name ?? "") }},
"email": {{ JSON.stringify($json.body.submission.fields.email ?? "") }},
"phone": {{ JSON.stringify($json.body.submission.fields.phone ?? "") }}
},
"manager_comment": {{ JSON.stringify(($json.body.submission.fields.message ?? "") + " | Page: " + ($json.body.submission.page_url ?? "") + " | Formgong ID: " + $json.body.submission.id) }},
"utm_source": {{ JSON.stringify($json.body.submission.fields.utm_source ?? "") }}
}Field mapping
| Formgong | KeyCRM card |
|---|---|
fields.name | contact.full_name |
fields.email | contact.email |
fields.phone | contact.phone (international format with +) |
fields.message, page_url, submission.id | manager_comment |
fields.utm_source … utm_content | utm_source … utm_content |
| Anything else | custom_fields with the field's uuid from GET /custom-fields |
UTM values reach the webhook only if your form posts them as normal fields, for example hidden inputs named utm_source. KeyCRM stores times in UTC, and so does Formgong's created_at.
Orders instead of cards
For an order form, post to /order. Put submission.id in source_uuid, the order number in the source. That makes a repeated delivery easy to find.
POST https://openapi.keycrm.app/v1/order
Authorization: Bearer <your KeyCRM API key>
Content-Type: application/json
Accept: application/json
{
"source_id": 12,
"source_uuid": "<submission.id>",
"buyer": {
"full_name": "<fields.name>",
"email": "<fields.email>",
"phone": "<fields.phone>"
},
"buyer_comment": "<fields.message>",
"products": [
{ "sku": "<fields.sku>", "name": "<fields.product>", "price": 0, "quantity": 1 }
]
}Troubleshooting
- 401: the key is wrong or
Beareris missing. A refreshed key must be updated in Make or n8n. - 422: a required field is missing, such as
contacton a card orsource_idon an order. Check the response body. - 429 Too Many Requests: more than 20 requests a minute per key. KeyCRM may block API access after repeated overuse, so keep other integrations in mind.
- Same lead twice: Formgong retried after a slow or failed answer. Search by
source_uuidor the ID in the comment before creating a new record. - Cards from tests: add the
eventfilter from the steps above.
Without Make or n8n: a small script
If you prefer no extra tool, host a tiny endpoint. It checks Formgong's signature, builds the KeyCRM body and returns an error when KeyCRM fails, so Formgong retries.
Cloudflare Worker (JavaScript)
// Env: FORMGONG_SECRET (signing secret), KEYCRM_KEY (KeyCRM API key)
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("POST only", { status: 405 });
const raw = await request.text();
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(env.FORMGONG_SECRET),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(raw));
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
if (request.headers.get("x-signature") !== "sha256=" + hex) return new Response("bad signature", { status: 401 });
const data = JSON.parse(raw);
if (data.event !== "submission.created") return new Response("ignored");
const f = data.submission.fields;
const res = await fetch("https://openapi.keycrm.app/v1/pipelines/cards", {
method: "POST",
headers: { authorization: "Bearer " + env.KEYCRM_KEY, "content-type": "application/json", accept: "application/json" },
body: JSON.stringify({
title: data.form.name + ": " + (f.name ?? ""),
source_id: 12,
contact: { full_name: f.name ?? "", email: f.email ?? "", phone: f.phone ?? "" },
manager_comment: (f.message ?? "") + " | Formgong ID: " + data.submission.id,
}),
});
// A non-2xx answer makes Formgong retry (30 s, 2 min, 10 min, 30 min).
return new Response(res.ok ? "ok" : "keycrm " + res.status, { status: res.ok ? 200 : 502 });
},
};Questions
Can Formgong send leads to KeyCRM without Make or n8n?
Not directly. KeyCRM needs a Bearer key header and its own JSON, and Formgong webhooks cannot add headers or reshape the body. A small script you host works as the middle step too.
Should I create a card or an order?
A card for enquiries and quote requests; it needs only a contact. An order when the form is a purchase with products; it needs a source and a buyer.
Where does the API key live?
In the Make keychain or the n8n credential, never in your website code. Formgong never sees it.
Will I still get the Telegram message?
Yes. The webhook runs alongside email and Telegram. Your team sees the lead in Telegram at once, and the card appears in KeyCRM a moment later.
Sources
Checked on 03.10.2026 against these public pages: KeyCRM OpenAPI documentation, KeyCRM Help: where to get an API key, Make: HTTP app, n8n docs: HTTP Request node, Formgong webhook reference.