Referință
Formulare, contact și rezervări
Endpoint-uri publice care scriu în căsuța de mesaje primite și în calendarul dumneavoastră. Nu necesită cheie API, deoarece sunt concepute pentru a fi apelate de vizitatori: captcha, idempotența și limitele de cereri fac parte din contract.
Înainte de a începe#
- Fără cheie API. Contactul și rezervările sunt adresate prin slug-ul spațiului de lucru:
/v1/public/{orgSlug}/…. Formularele sunt adresate prin cheia proprie a formularului:/v1/forms/{key}/…. - Fiecare scriere acceptă un antet
Idempotency-Key, orice șir unic de până la 200 de caractere. Refolosiți-l când reîncercați aceeași trimitere, iar API-ul va returna primul rezultat în loc să salveze unul nou. SDK-ul îl generează pentru dumneavoastră. - Fiecare scriere este limitată per IP al vizitatorului (implicit 10 pe minut și 60 pe oră). Dacă trimiteți de pe propriul server, transmiteți mai departe
x-forwarded-forșiuser-agentale vizitatorului; altfel, toți vizitatorii împart aceeași limită. - Un formular răspunde cu
404 FORM_UNAVAILABLEdacă nu este activ sau dacă modulul Formulare nu este activat pentru spațiul de lucru.
Aceleași rute există și fără /public/{orgSlug} (/v1/contacts, /v1/appointments, …). Acestea deservesc un singur site, anterior spațiilor de lucru; folosiți întotdeauna varianta cu slug.
Formular de contact#
POST/v1/public/{orgSlug}/contacts
Salvează o solicitare în mesajele primite și notifică echipa.
| Câmp | Tip | |
|---|---|---|
name | șir | Obligatoriu. |
email | șir | Opțional; dacă este trimis, trebuie să fie o adresă validă. |
phone, subject, message | șir | Opțional. |
turnstileToken | șir | Token-ul Cloudflare Turnstile, atunci când spațiul de lucru îl verifică. |
Orice alt câmp este respins cu 400. Răspunsul este 201, cu solicitarea salvată.
curl https://api.console.citronos.com/v1/public/your-org/contacts \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5b0d7c1e-8f3a-4c62-9d1b-2e7f6a4c9b10" \
-d '{
"name": "Ivan Petrov",
"email": "ivan@example.com",
"phone": "+359888000000",
"subject": "Quote",
"message": "A quote for BIO 6, please.",
"turnstileToken": "0.AbC…"
}'const enquiry = await citron.submitContact(
{ name: 'Ivan Petrov', email: 'ivan@example.com', subject: 'Quote', message: 'A quote for BIO 6, please.' },
{ turnstileToken, idempotencyKey, orgSlug: 'your-org' }, // idempotencyKey is generated when omitted
);
console.log(enquiry.id, enquiry.status);{
"data": {
"id": "cmf3m1c2d0010s601k4j3h2g1",
"organizationId": "cmf2x0r1k0000s6014h3j5k7l",
"name": "Ivan Petrov",
"email": "ivan@example.com",
"phone": "+359888000000",
"subject": "Quote",
"message": "A quote for BIO 6, please.",
"status": "new",
"source": "contact_form",
"consentAt": "2026-09-24T08:15:02.114Z",
"meta": { "idempotencyKey": "5b0d7c1e-8f3a-4c62-9d1b-2e7f6a4c9b10" },
"createdAt": "2026-09-24T08:15:02.120Z",
"updatedAt": "2026-09-24T08:15:02.120Z"
},
"meta": { "requestId": "0f6c1d2e-3b4a-4c5d-8e9f-a0b1c2d3e4f5" }
}Turnstile. Fiecare spațiu de lucru verifică token-ul într-unul din trei moduri: dezactivat, monitorizare (verificat și înregistrat, dar niciodată respins) sau obligatoriu (un token lipsă sau invalid primește 400). Dacă Cloudflare nu poate fi contactat, solicitarea este acceptată, astfel încât o întrerupere nu vă face niciodată să pierdeți un lead.
Reîncercări. O reîncercare cu același Idempotency-Key returnează solicitarea salvată prima dată și primește răspuns înaintea verificării captcha, deoarece token-urile Turnstile sunt de unică folosință.
Formulare#
Formularele create în modulul Formulare al consolei au propria cheie publică. Preluați definiția când formularul este afișat, randați câmpurile acestuia și trimiteți valorile înapoi împreună cu token-ul de randare.
GET/v1/forms/{key}
Câmpurile formularului, mesajele, setările anti-spam și un token de randare semnat.
POST/v1/forms/{key}/submissions
Trimite un formular, ca JSON (201) sau ca simplu formular HTML (303).
# No API key: the form key is public
curl https://api.console.citronos.com/v1/forms/quote// When the form is SHOWN: fields to render, the render token, the Turnstile site key
const form = await citron.forms.get('quote');
// When it is sent
const result = await citron.forms.submit(
'quote',
{ name: 'Ivan Petrov', email: 'ivan@example.com', product: 'BIO 6' },
{ renderToken: form.renderToken, turnstileToken, utm: { utm_source: 'newsletter' } },
);
console.log(result.id, result.status, result.warnings);{
"data": {
"key": "quote",
"name": "Request a quote",
"fields": [
{ "key": "name", "type": "text", "label": "Name", "required": true },
{ "key": "email", "type": "email", "label": "Email", "required": true },
{ "key": "product", "type": "select", "label": "Product", "required": false, "options": ["BIO 6", "BIO 10"] },
{ "key": "message", "type": "longText", "label": "Message", "required": false }
],
"successMessage": "Thank you, we will be in touch.",
"redirectUrl": null,
"honeypotField": "_hp",
"renderTokenField": "_rt",
"renderToken": "Y21mM24wYS4xNzU4…",
"turnstileSiteKey": "0x4AAAAAAA…"
}
}curl https://api.console.citronos.com/v1/forms/quote/submissions \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9a4e2f10-6b7c-4d8e-a1f2-3c4d5e6f7a8b" \
-d '{
"data": { "name": "Ivan Petrov", "email": "ivan@example.com", "product": "BIO 6" },
"renderToken": "Y21mM24wYS4xNzU4…",
"turnstileToken": "0.AbC…",
"utm": { "utm_source": "newsletter" }
}'{
"data": { "id": "cmf3n4q5r0020s601t6y7u8i9", "status": "accepted" },
"warnings": [{ "code": "UNKNOWN_FIELDS_DROPPED", "keys": ["phone2"] }]
}| Câmp din corpul cererii | |
|---|---|
data | Cheia câmpului → valoare, conform definiției. Cheile necunoscute sunt eliminate și raportate în warnings ca UNKNOWN_FIELDS_DROPPED. |
renderToken | Din definiția preluată la afișarea formularului. |
turnstileToken | Obligatoriu când formularul are Turnstile activat (turnstileSiteKey este setat). |
referrer, utm | Atribuire opțională; utm păstrează doar cheile utm_*. |
Protecție anti-spam#
- O trimitere cu câmpul-capcană completat (
_hp), cu token de randare lipsă sau invalid ori trimisă mai repede decât timpul minim de completare al formularului (implicit 3 secunde) este păstrată ca spam. Primește totuși același răspunsaccepted, astfel încât boții nu află nimic. - Cu Turnstile activat, un token lipsă sau invalid primește
400. - Pe lângă limitele per IP se aplică și limite per formular: implicit 30 de trimiteri per IP pe oră și 500 per formular pe oră, ajustabile pentru fiecare formular. Peste acestea:
429 RATE_LIMITED. - Un formular poate enumera site-urile care au voie să îl trimită; orice alt
Originprimește403 ORIGIN_NOT_ALLOWED.
Fără JavaScript#
Funcționează și un simplu <form method="post">. API-ul răspunde cu o redirecționare 303 către URL-ul de redirecționare al formularului sau către propria pagină de mulțumire și afișează erorile într-o pagină scurtă.
<!-- No JavaScript needed: the API answers 303 to your redirect URL or its thank-you page -->
<form method="post" action="https://api.console.citronos.com/v1/forms/quote/submissions">
<input name="name" required>
<input name="email" type="email" required>
<!-- honeypot: keep it empty and hidden from people -->
<input name="_hp" tabindex="-1" autocomplete="off" hidden>
<!-- when the form has Turnstile on, the widget adds cf-turnstile-response -->
<div class="cf-turnstile" data-sitekey="0x4AAAAAAA…"></div>
<button>Send</button>
</form>Rezervări#
GET/v1/public/{orgSlug}/appointment-types
Tipurile de programări pe care le pot rezerva vizitatorii.
GET/v1/public/{orgSlug}/appointments/slots
Intervalele libere pentru un tip de programare: typeId (obligatoriu), mode, from, to, staffId, locationId.
POST/v1/public/{orgSlug}/appointments
Rezervă un interval.
# 1. What can be booked
curl https://api.console.citronos.com/v1/public/your-org/appointment-types
# 2. Free slots for one type (from/to default to now and 30 days ahead)
curl "https://api.console.citronos.com/v1/public/your-org/appointments/slots?typeId=cmf3p1…&mode=online&from=2026-09-25T00:00:00.000Z&to=2026-10-09T00:00:00.000Z"
# 3. Book one of them
curl https://api.console.citronos.com/v1/public/your-org/appointments \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2c9f8e7d-6b5a-4d3c-9b2a-1f0e9d8c7b6a" \
-d '{
"contact": { "name": "Ivan Petrov", "email": "ivan@example.com", "phone": "+359888000000" },
"appointmentTypeId": "cmf3p1…",
"staffUserId": "cmf2y7…",
"mode": "online",
"startsAt": "2026-09-29T07:30:00.000Z",
"note": "About BIO 6"
}'const types = await citron.booking.types({ orgSlug: 'your-org' });
const slots = await citron.booking.slots(
{ typeId: types[0].id, mode: 'online', from: new Date(), to: new Date(Date.now() + 14 * 86_400_000) },
{ orgSlug: 'your-org' },
);
const appointment = await citron.booking.book(
{
contact: { name: 'Ivan Petrov', email: 'ivan@example.com', phone: '+359888000000' },
appointmentTypeId: types[0].id,
staffUserId: slots[0].staffUserId,
mode: 'online',
startsAt: slots[0].startsAt,
},
{ idempotencyKey, orgSlug: 'your-org' },
);
console.log(appointment.id, appointment.status);{
"data": [
{
"startsAt": "2026-09-29T07:30:00.000Z",
"endsAt": "2026-09-29T08:00:00.000Z",
"staffUserId": "cmf2y7…",
"staffName": "Maria Georgieva",
"mode": "online",
"label": "пн 29 септ. · 10:30"
}
],
"meta": { "requestId": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a" }
}{
"data": {
"id": "cmf3q2w3e0030s601r4t5y6u7",
"organizationId": "cmf2x0r1k0000s6014h3j5k7l",
"appointmentTypeId": "cmf3p1…",
"staffUserId": "cmf2y7…",
"contactId": "cmf3q2w3e0029s601a1s2d3f4",
"mode": "online",
"status": "requested",
"startsAt": "2026-09-29T07:30:00.000Z",
"endsAt": "2026-09-29T08:00:00.000Z",
"timezone": "Europe/Sofia",
"idempotencyKey": "2c9f8e7d-6b5a-4d3c-9b2a-1f0e9d8c7b6a",
"appointmentType": { "id": "cmf3p1…", "slug": "consultation", "nameBg": "Консултация", "nameEn": "Consultation", "durationMinutes": 30, "mode": "both", "active": true },
"contact": { "id": "cmf3q2w3e0029s601a1s2d3f4", "name": "Ivan Petrov", "email": "ivan@example.com", "phone": "+359888000000" },
"createdAt": "2026-09-24T08:20:11.504Z",
"updatedAt": "2026-09-24T08:20:11.504Z"
},
"meta": { "requestId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" }
}| Câmp din corpul cererii | |
|---|---|
contact | name (între 2 și 120 de caractere, obligatoriu), email, phone (între 6 și 40 de caractere). |
appointmentTypeId | Obligatoriu. |
mode | online sau in_office. Obligatoriu. |
startsAt | ISO 8601, valoarea startsAt a unui interval. Obligatoriu. |
staffUserId, locationId | Din interval. |
note | Până la 1000 de caractere. |
- Corpul cererii este validat strict: un câmp necunoscut primește
400. Nu există câmp captcha; acest endpoint este protejat de limita per IP. - Serverul verifică din nou intervalul. Dacă între timp l-a ocupat altcineva, răspunsul este
400; preluați din nou intervalele. - Rezervările noi pornesc cu starea
requestedpână când echipa dumneavoastră le confirmă în consolă. - O reîncercare cu același
Idempotency-Keyreturnează programarea rezervată prima dată.
Această referință acoperă doar API-ul public și respectă contractul de compatibilitate al API-ului: răspunsurile publice pot doar să primească câmpuri noi. Descrierea OpenAPI a acestor rute se află la /developers/openapi.json.