Skip to content

Frappe API Standard for Glific Webhook Compatibility

Last verified: 2026-05-13. Rules below are unchanged by CR-002 v2 and CR-003 — the flat-map contract, status envelope, and numeric-suffix-expansion rules all stay as written.

All Frappe whitelisted API endpoints in this project are consumed by Glific webhook nodes. Glific only reliably reads flat, top-level key-value pairs from webhook responses. Every API endpoint must follow the rules in this document — both for new endpoints and when modifying existing ones.

Source of truth split: - This doc defines HOW endpoints should be shaped (the contract every endpoint follows). - docs/glific-api-reference-v2.md defines WHAT each specific endpoint returns (per-endpoint schema, status enums, sample responses). v2 supersedes v1 as of 2026-05-13.

When in doubt, this doc wins.


Rules

1. No Frappe message wrapper

Do NOT return a dict from @frappe.whitelist() methods — Frappe wraps the return value under a top-level "message" key, forcing Glific to navigate one extra level. Instead, write keys directly to the response:

@frappe.whitelist(allow_guest=True)
def my_endpoint(...):
    payload = {
        "success": True,
        "status": "ok",
        "current_week": 3,
        ...
    }
    frappe.local.response.update(payload)
    # do not return anything

Glific then reads @results.webhook.current_week directly, not @results.webhook.message.current_week.

Convention break note: this is a deliberate departure from Frappe's default @whitelist behavior. Internal callers that need the response should call the underlying business-logic functions directly, NOT via HTTP. The whitelisted endpoint is for Glific only.


2. Flat structure only — no nested objects

Every value in the response must be a scalar (string, number, boolean, or None). No dicts, no lists of dicts. If your logical model has nesting, flatten it with prefixed keys:

Nested (bad) Flat (good)
question.text question_text
question.options.A option_a
user.profile.name user_name
attempt.score.percentage score_percentage

3. No arrays

Glific cannot reliably iterate arrays inside a webhook response. If you have a list, flatten to indexed keys with a count:

{
  "item_count": 3,
  "item_1": "apple",
  "item_2": "banana",
  "item_3": "cherry"
}

Cap the number of indexed keys at a documented maximum (e.g. item_1 through item_10).

For complex items, use the same numeric-suffix pattern across multiple fields:

{
  "content_count": 2,
  "content_1_type": "Reading Activity",
  "content_1_id": "RA-001",
  "content_2_type": "Quiz",
  "content_2_id": "QZ-008"
}

4. snake_case, ASCII-only, no special characters in keys

Glific's @results.x.y expression syntax breaks on spaces, ?, ., -, and non-ASCII characters in keys. Stick to lowercase_snake_case. If source data has dirty keys, normalize them before writing the response.


5. Sanitize values that go into keys or IDs

If an ID field embeds free-form text (e.g. "What is X?QN12345"), extract the clean code with a regex before returning it. Never return user-generated text concatenated with system IDs as a single value.


6. Status envelope — success + status required

Every response must include these two keys, even on success:

{
  "success": true,
  "status": "<short_machine_readable_code>"
}

status is a snake_case enum (quiz_resumed, quiz_completed, not_found, already_attempted, invalid_input, etc.) so Glific flows can branch on it with a Split-by-Expression node.

user_message (a short human-readable string safe to display in WhatsApp) is optional — include it when there's a natural user-facing message for the response, omit when the response is consumed by flow logic only.


7. Errors return HTTP 200 with success: false, not 4xx/5xx

Glific treats non-200 responses as webhook failures and kills the flow. For expected business errors (validation failures, "user not enrolled", "quiz already submitted"), return HTTP 200 with:

{
  "success": false,
  "status": "<error_code>",
  "user_message": "<message safe to show the user>",
  "error_detail": "<optional, for logs>"
}

Reserve real HTTP error codes (500, etc.) for genuine unhandled exceptions.


8. Numeric and boolean types stay typed

Return 5, not "5". Return true, not "true". Glific's comparison operators behave differently on strings vs numbers.


9. Keep strings WhatsApp-friendly

Any string that may end up displayed in WhatsApp (user_message, question_text, option text, etc.) should be plain text, no HTML, no markdown unless WhatsApp-supported:

  • *bold*
  • _italic_
  • ~strike~
  • `code`

10. Test contract with curl before declaring done

After implementing or modifying an endpoint, run it via curl or bench execute and verify the JSON response is:

  • Flat (no nested objects, no arrays)
  • Has no top-level message wrapper
  • Uses correct types (numbers as numbers, booleans as booleans)
  • Includes success and status keys

Variant responses — documented, not enforced in code

When an endpoint's response shape varies by state (e.g. quiz_completed=true vs false, "submission accepted" vs "duplicate"), the MVP discipline is:

  1. Each variant gets its own entry in the API reference doc (docs/glific-api-reference-v2.md) with the full key list for that variant.
  2. Flow builders branch on status first, then read only the fields documented for that variant.
  3. Backend code does NOT need to return every key across all variants — that's documentation discipline, not code discipline.

Example: submit_answer has two variants — one when more questions remain, one when the quiz is complete. The reference doc shows both. The backend returns whichever variant applies. The Glific flow reads status first (quiz_in_progress vs quiz_completed) and only accesses the fields valid for that variant.

This is a deliberate simplification — enforcing "always return every key with sentinels" adds backend complexity for marginal flow-builder benefit, given the doc + status-first branching pattern works.


Documenting status enum values

Every endpoint's status enum must be enumerated in docs/glific-api-reference-v2.md in that endpoint's section. New status values are a safe additive change (Glific flows handling only known values continue to work). Renaming or removing existing status values is a breaking change — coordinate with the Glific team before shipping.


Versioning posture

Breaking changes (renaming keys, removing keys, changing types) require:

  1. Glific team notified at least one sprint ahead.
  2. Old endpoint stays callable under a v1 path namespace.
  3. New shape lives at a v2 path namespace.
  4. Old endpoint is deprecated, not deleted; deletion only after Glific confirms zero flows reference it.

Field additions are non-breaking — flow builders reading old keys still work. Add freely; document in the reference doc.


Reference example — good response

{
  "success": true,
  "status": "quiz_resumed",
  "user_message": "Welcome back! Continuing from question 2.",
  "quiz_attempt_id": "2rscijc6nd",
  "quiz_name": "BasicQuiz_Quiz_B",
  "total_questions": 5,
  "questions_answered": 1,
  "correct_so_far": 1,
  "question_index": 2,
  "question_id": "QN43903",
  "question_text": "Which is an example of a need?",
  "question_type": "multiple_choice",
  "option_count": 4,
  "option_a": "Chocolate",
  "option_b": "Video game",
  "option_c": "Food",
  "option_d": "Movie ticket"
}

All values are scalars. No nested objects. No arrays. Flat top-level keys. success and status both present. user_message included because the response will be displayed to the student.


How to use this doc

  • Writing a new whitelisted endpoint: read this doc end-to-end before starting. Reference it in the PR description.
  • Modifying an existing endpoint: check the response shape against these rules. If the existing shape violates them, fix it as part of the change (it's a pre-existing bug).
  • Reviewing a PR that touches a whitelisted endpoint: run rule 10 (curl test) and verify rules 1–9 by inspection.
  • In task descriptions: reference "Follow docs/api-standard-glific.md" so feature-builder and code-reviewer agents pull it into their context automatically.