# Grouthna CRM API

> REST API for Grouthna CRM: leads, WhatsApp messages and templates, conversations, booking, verification codes and calls.

Base URL: https://timdad.online/api/v1
Authorization: Bearer gr_live_… — create a key (with only the scopes it needs) in Settings → API. 120 requests a minute per key; errors are { "error": { "code", "message" } }.

## Endpoints

### POST /leads — Add or update a lead (scope: leads:write)
A website form, landing page or another system. Upserts by phone: 201 when created, 200 when the number already exists. Ad fields (platform, campaign, ad, click ids, utm_*) credit the exact ad.
Body: {"phone":"0551234567","name":"Noura","service_code":"LASER","source":"website","campaign":"Summer offer","utm_source":"snapchat","gclid":"…"}
Response: {"id":"lead-uuid","created":true}

### GET /leads — List leads (scope: leads:read)
Newest first.
Query: updated_since (ISO date), stage (stage key), limit (1–200 (50)), before (ISO date, for paging)
Response: {"data":[{"id":"lead-uuid","name":"Noura","phone":"+966551234567","stage":"new","source":"website","campaign":"Summer offer","opted_out":false,"created_at":"2026-10-09T09:00:00Z"}],"next_before":null}

### GET /leads/{id} — One lead (scope: leads:read)
The lead with its appointments.
Response: {"id":"lead-uuid","name":"Noura","phone":"+966551234567","stage":"booked","appointments":[{"id":"appt-uuid","start_at":"2026-10-12T07:00:00Z","status":"booked","doctor":"Dr. Sara","service":"Cleaning"}]}

### POST /messages — Send a WhatsApp message (scope: messages:write)
Three shapes. A template goes any time (creates the lead if new). Text and files go into the client's open conversation: on the official number only if the client wrote in the last 24 hours; QR numbers, Telegram and website chat any time — otherwise 409 window_closed (send a template). Files are fetched once from a public HTTPS link (no redirects, 20 MB).
Body: {"phone":"0551234567","template":"invoice_ready","params":["Noura","INV-1001"],"header":{"link":"https://example.com/inv-1001.pdf","filename":"invoice.pdf"},"buttons":["ORD-1001"]}
Plain text: {"phone":"0551234567","text":"Your order is on its way"}
A file: {"phone":"0551234567","media_url":"https://example.com/menu.pdf","caption":"Our menu"}
Response: {"queued":true,"type":"text","message_id":"msg-uuid","conversation_id":"conv-uuid","lead_id":"lead-uuid"}
Errors: 404 not_found — no approved template with this name; 409 window_closed / opted_out / blocked; 422 params count, media type or size

### GET /conversations — A client's conversations (scope: messages:read)
Up to 10 conversations (newest first) with their latest messages, oldest → newest. Team notes are left out.
Query: phone (required), limit (messages per conversation, 1–200 (50)), before (ISO date, to page back)
Response: {"data":{"contact":{"id":"contact-uuid","name":"Noura","phone":"+966551234567","opted_out":false,"blocked":false},"conversations":[{"id":"conv-uuid","channel":{"type":"whatsapp","name":"Main number"},"window_open":true,"messages":[{"id":"msg-uuid","direction":"in","kind":"text","text":"Hello","status":"received","at":"2026-10-09T09:00:00Z"}]}]}}

### GET /templates — List templates (scope: templates:read)
WhatsApp templates and their status.
Query: status (APPROVED | PENDING | REJECTED | DRAFT | PAUSED)
Response: {"data":[{"name":"invoice_ready","category":"UTILITY","language":"ar","status":"APPROVED","body":"Hi {{1}}, your invoice {{2}} is ready.","variables":2,"header":{"type":"DOCUMENT"},"buttons":[{"type":"URL","text":"Pay","url":"https://pay.example.com/{{1}}","dynamic":true}]}]}

### GET /templates/{name} — Template status (scope: templates:read)
One template, with the rejection reason when WhatsApp rejected it.
Response: {"data":{"name":"invoice_ready","status":"PENDING","rejected_reason":null}}

### POST /templates — Create a template (scope: templates:write)
Created and sent to WhatsApp for approval (submit: false keeps it a draft). Variables are {{1}}, {{2}}… with one example each. A media header is fetched from header.link (JPG/PNG 5 MB, MP4 16 MB, PDF 20 MB).
Body: {"name":"invoice_ready","category":"UTILITY","language":"ar","body":"Hi {{1}}, your invoice {{2}} is ready.","examples":["Noura","INV-1001"],"footer":"Thank you","header":{"type":"DOCUMENT","link":"https://example.com/sample.pdf","filename":"invoice.pdf"},"buttons":[{"type":"URL","text":"Pay","url":"https://pay.example.com/{{1}}"}]}
Response: {"data":{"name":"invoice_ready","status":"PENDING"}}
Errors: 409 conflict — same name and language; 422 invalid_template — the builder's checks

### GET /services — Services (scope: appointments:read)
Active services with price, duration and doctors (for booking widgets).
Response: {"data":[{"id":"svc-uuid","code":"CLEAN","name":"Cleaning","price_sar":250,"duration_min":30,"doctors":[{"id":"doc-uuid","name":"Dr. Sara","accepts_new":true}]}]}

### GET /slots — Free times (scope: appointments:read)
Free times per doctor (Riyadh dates).
Query: service_id (required), from (YYYY-MM-DD), days (1–14 (7))
Response: {"data":[{"doctor_id":"doc-uuid","doctor":"Dr. Sara","accepts_new":true,"slots":[{"start_at":"2026-10-12T07:00:00Z","date":"2026-10-12","label":"10:00 ص"}]}]}

### GET /appointments — Appointments (scope: appointments:read)
Appointments between two dates (62 days max).
Query: from (ISO date), to (ISO date)
Response: {"data":[{"id":"appt-uuid","lead_id":"lead-uuid","client":"Noura","phone":"+966551234567","status":"confirmed"}]}

### POST /otp — Send a verification code (scope: otp:send)
A code on WhatsApp through the business's approved verification template. Grouthna makes the code unless you send one.
Body: {"phone":"0551234567","ttl_minutes":5}
Response: {"sent":true,"otp_id":"otp-uuid","expires_at":"2026-10-09T09:05:00Z"}

### POST /otp/verify — Check a code (scope: otp:send)
Valid once, until it expires; 5 tries per code. A wrong code: { valid: false, reason, attempts_left }.
Body: {"phone":"0551234567","code":"4821"}
Response: {"valid":true}

### POST /calls — Log a call (scope: calls:write)
Any phone system reports a finished call. It lands on the client's card; a missed incoming call alerts the owner. Idempotent per (source, external_id).
Body: {"phone":"0551234567","direction":"in","status":"answered","duration_sec":134,"extension":"101","source":"grandstream","external_id":"1696512345.12"}
Response: {"id":"call-uuid","lead_id":"lead-uuid","duplicate":false}

## Webhooks
Webhooks: add an HTTPS address in Settings → API and pick events. Each POST carries X-Grouthna-Event, X-Grouthna-Timestamp and X-Grouthna-Signature = sha256=HMAC_SHA256(secret, `${timestamp}.${body}`) — check it, and reject old timestamps.

- lead.created: a new lead
- lead.stage_changed: a lead moved to another stage
- appointment.booked: an appointment was booked
- appointment.status_changed: confirmed / attended / no-show / cancelled
- message.received: a client wrote
- message.status: an outgoing message was delivered, read or failed
- channel.status: a number connected or disconnected
- call.logged: a call was logged
- ticket.created: a ticket or complaint was opened
- ticket.status_changed: a ticket moved on
- payment.paid: a payment link was paid

## Full reference
https://timdad.online/developers
https://timdad.online/api/v1/openapi.json
