Grouthna CRM API

Leads, WhatsApp messages and templates, conversations, booking, verification codes and calls — over HTTPS with JSON.

OpenAPI 3.1 (JSON)llms.txt

Authentication

Base URL https://timdad.online/api/v1. Send Authorization: Bearer gr_live_…; create a key in Settings → API with only the scopes it needs (a key can expire). 120 requests a minute per key. Errors: { "error": { "code": "…", "message": "…" } } with 400 / 401 / 403 / 404 / 409 / 422 / 429.

curl https://timdad.online/api/v1/templates?status=APPROVED \n  -H "Authorization: Bearer gr_live_…"

Endpoints

POST /api/v1/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.

Request
{
  "phone": "0551234567",
  "name": "Noura",
  "service_code": "LASER",
  "source": "website",
  "campaign": "Summer offer",
  "utm_source": "snapchat",
  "gclid": "…"
}
Response
{
  "id": "lead-uuid",
  "created": true
}

GET /api/v1/leads

List leads · scope leads:read

Newest first.

  • 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 /api/v1/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 /api/v1/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).

Request
{
  "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"
}
  • 404 not_found — no approved template with this name
  • 409 window_closed / opted_out / blocked
  • 422 params count, media type or size

GET /api/v1/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.

  • 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 /api/v1/templates

List templates · scope templates:read

WhatsApp templates and their status.

  • 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 /api/v1/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 /api/v1/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).

Request
{
  "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"
  }
}
  • 409 conflict — same name and language
  • 422 invalid_template — the builder's checks

GET /api/v1/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 /api/v1/slots

Free times · scope appointments:read

Free times per doctor (Riyadh dates).

  • 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 /api/v1/appointments

Appointments · scope appointments:read

Appointments between two dates (62 days max).

  • from — ISO date
  • to — ISO date
Response
{
  "data": [
    {
      "id": "appt-uuid",
      "lead_id": "lead-uuid",
      "client": "Noura",
      "phone": "+966551234567",
      "status": "confirmed"
    }
  ]
}

POST /api/v1/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.

Request
{
  "phone": "0551234567",
  "ttl_minutes": 5
}
Response
{
  "sent": true,
  "otp_id": "otp-uuid",
  "expires_at": "2026-10-09T09:05:00Z"
}

POST /api/v1/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 }.

Request
{
  "phone": "0551234567",
  "code": "4821"
}
Response
{
  "valid": true
}

POST /api/v1/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).

Request
{
  "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

Add an HTTPS address in Settings → API and pick events. Each POST has a JSON body { id, event, created_at, data } and the headers X-Grouthna-Event, X-Grouthna-Timestamp and X-Grouthna-Signature: sha256=HMAC_SHA256(secret, `timestamp.body`). Check the signature and reject old timestamps. Failed deliveries are retried.

  • 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

MCP

AI assistants (Claude and others) can work with the account through the MCP server — see Settings → API → MCP.