End-to-End Examples

Last updated 23 May 2026

Four end-to-end webhook examples, a JSON acknowledgement, a website form, an authenticated lookup, and a ping vs event branch

Complete round trips you can adapt. Each shows the incoming request, the workflow steps, the tokens used, and the response.

All examples assume a published virtual workflow whose starting step is the endpoint listener (see Creating an endpoint).

CALLER External system POST /listener/… { amount: 2500 } 200 OK { "status": "ok" } request response FLEXIE · VIRTUAL WORKFLOW Decision email not empty? Create note on contact Endpoint reply return_type: data SYNC run mode __data.amount = 2500 Listener: return_type = continue · auth: JWT off · CORS off

Example 1: Receive a webhook and acknowledge

Goal: another system tells Flexie an invoice was raised; Flexie records a note on the matching contact and replies "received."

The request the other system sends:

curl -X POST https://your-flexie/listener/abc123…/def456… \
  -H "Content-Type: application/json" \
  -d '{
        "invoice_amount": 2500,
        "customer_email": "john@example.com"
      }'

The workflow:

  1. Decision (virtual condition): {{ __data.customer_email }} is not empty
    • No: endpoint response action returns {"error":"email required"}.
    • Yes: continue.
  2. Action, Create a note on the contact found by email, body: Invoice raised: {{ formatCurrency(__data.invoice_amount, "€") }}.
  3. Endpoint response (return_type = data): { "status": "received" }.

Endpoint listener settings: return_type = continue (so step 3 produces the reply), workflow run mode = Sync.

Key point: the body fields are read from the __data namespace, {{ __data.customer_email }}, {{ __data.invoice_amount }}, not at the top level.

Example 2: Create-or-update a contact from a website form

Goal: your website posts a sign-up; Flexie creates the contact if new, updates it if it exists, and redirects the visitor to a thank-you page.

The request (form-encoded, straight from a browser):

POST /listener/…/…
Content-Type: application/x-www-form-urlencoded

first_name=John&last_name=Doe&email=john@example.com

Endpoint listener settings: allow_cors = on, cors_domains = https://www.yoursite.com (because a browser calls it directly); return_type = redirect; return_redirect = https://www.yoursite.com/thanks?email={{ __data.email | url_encode }}.

The workflow:

  1. Action, Store a value existing = {{ findOne("contact", "email", __data.email).id | default("") }}.
  2. Decision (virtual condition): {{ __data.existing }} is not empty
    • Yes: Update that contact's first_name and last_name.
    • No: Create a contact from {{ __data.first_name }}, {{ __data.last_name }}, {{ __data.email }}.

The redirect (set on the listener) sends the visitor on regardless of branch.

If you would rather return JSON to a front-end script than redirect, set return_type = data and return { "ok": true }.

Example 3: A small custom lookup API (authenticated)

Goal: an internal tool asks Flexie for a contact's lifetime value and gets JSON back. Only the tool may call it.

Endpoint listener settings:

  • add_authentication = on; endpoint_key = internal-tools; endpoint_secret = a strong secret the tool also holds.
  • return_type = continue; workflow run mode = Sync.

The request:

curl https://your-flexie/listener/…/… \
  -H "Authorization: Bearer <JWT signed HS256, iss=internal-tools>" \
  -H "Content-Type: application/json" \
  -d '{ "contact_id": 42 }'

(The token must be signed with HS256, HS384, or HS512 and carry iss = internal-tools.)

The workflow:

  1. Endpoint response (return_type = data):
{
  "contact_id": {{ __data.contact_id }},
  "lifetime_value": {{ findDealsValue("contact", "won", "*", __data.contact_id) }},
  "open_cases": {{ getCaseStats("contact", __data.contact_id).open }}
}

Because the workflow is Sync, the response is produced within the request and returned straight to the tool. An unauthenticated or wrongly-signed call is rejected before any step runs.

Example 4: Branch the reply by what arrived

Goal: one endpoint handles both "ping" health checks and real events.

The workflow:

  1. Decision (virtual condition): {{ __data.type }} equals ping
    • Yes: Endpoint response (data): { "pong": true }.
    • No: process the real event, then Endpoint response (data): { "status": "processed" }.

Listener: return_type = continue, workflow run mode = Sync. Two different replies from one endpoint, chosen by a virtual condition.

Recurring lessons from these examples

  • Body fields are under __data ({{ __data.email }}); headers are {{ __data.__headers.* }}; stored values are {{ __data.* }}.
  • continue + Sync run mode is the combination for request/response APIs.
  • Authenticate anything that returns real data.
  • Enable CORS only when a browser calls the endpoint directly.
  • Guard incoming values with default(...) or is defined, callers do not always send what you expect.

Back to