Qflow Comms API - Integrator Guide

Send transactional emails (ticket confirmations, registration receipts, custom invitations) through Qflow’s enterprise email infrastructure, using your own HTML and our merge tokens. Get signed webhook callbacks for every event in the email lifecycle.

This guide takes you from zero to a working integration. The companion OpenAPI / Swagger docs are the source of truth for request/response field shapes — this article covers the journey and the things Swagger can't express (HMAC verification, idempotency semantics, lifecycle).

What this service provides

When you create attendees in Qflow via the API, you can optionally trigger a templated email send to that recipient as part of the same call. Qflow:

  • Substitutes merge tokens like {{firstname}}, {{event}}, {{barcode}} from real attendee/event data

  • Sends the HTML email from a verified domain (your sending address, our infrastructure)

  • Tracks delivery, opens, bounces and other lifecycle events via SendGrid

  • POSTs signed webhooks to your configured URL for every event

  • Supports retry-safe idempotency: same request twice = one email, one webhook

You manage the email templates and the receiving webhook endpoint. We handle delivery, signing, retries, and lifecycle event capture.

Quick start (5 steps)

  1. Get an OAuth bearer token
    Use the standard /connect/token flow. See Authentication.

  2. Confirm Comms API access
    Call GET /api/comms/templates?eventId=<eventId>.
    200 OK means Comms API access is enabled.

  3. Register your webhook endpoint
    Call POST /api/comms/webhook with your webhook URL.
    Store the returned secret immediately — it is shown once only.

  4. Create a comms template
    Call POST /api/comms/template with your subject, HTML, sender details, and event ID.
    Store the returned template id.

  5. Create a guest and trigger the send
    Call POST /api/guest with commsTemplateId.
    Qflow returns the attendee record immediately, queues the email asynchronously, and sends lifecycle events to your webhook.

Authentication

The Comms API uses the same bearer token authentication as the rest of Qflow's API.

POST https://identity.qflowhub.io/core/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=password
&client_id=<your-client-id>
&client_secret=<your-client-secret>
&scope=qflowapi
&username=<email>
&password=<password>

Response includes access_token.

Authorization: Bearer <access_token>

Tokens expire (typically 1 hour). Refresh via standard OAuth flow.

Comms API access (allow-list)

Comms API endpoints are restricted to enabled accounts.

Access levels:

  • Per-user

  • Per-enterprise

Check access:

curl -X GET 'https://api.qflowhub.io/api/comms/templates?eventId=<your-event>' \
-H 'Authorization: Bearer <token>'
  • 200 OK → enabled

  • 403 comms_api_not_enabled → request access

Templates

Templates = HTML + subject + merge tokens. Scoped to a single event.

{{firstname}}
Attendee’s first name

{{lastname}}
Attendee’s last name

{{fullname}}
Full name (first + last)

{{othernames}}
Middle/other names

{{email}}
Attendee email address

{{barcode}}
Barcode string (auto-generated if missing)

{{barcode_qr}}
QR code image URL (use in <img src="">)

{{plusones}}
Number of plus-ones

{{guestnotes}}
Additional attendee info

{{tags}} / {{tickets}} / {{sessions}}
Tag groups (one per line if multiple)

{{event}}
Event title

{{date}}
Event start date (dd MMMM yyyy)

{{starttime}} / {{endtime}}
Event times (HH:mm)

{{companyname}}
Sender’s company name

{{rsvplink_yes}} / {{rsvplink_no}}
RSVP confirmation links

{{V_1}}, {{V_2}}, …
Custom field values

Create template

POST /api/comms/template
{
"eventId": "guid",
"name": "Ticket confirmation v1",
"subject": "Your ticket for {{event}} — {{firstname}}",
"html": "<html>...</html>",
"fromAddress": "tickets@yourcompany.com",
"fromName": "Your Company Tickets",
"replyTo": "support@yourcompany.com"
}

Capture the id

URL safety scanning

Unsafe URLs → rejected with 422 spam_check_failed.

No previous attempt
Allowed — first send

Delivered successfully
Allowed — resend scenario

Transient failure
Allowed — retry likely to succeed

bounced
Rejected — address invalid

dropped
Rejected — SendGrid suppression or hard failure

spam_reported
Rejected — recipient marked as spam

unsubscribed
Rejected — recipient opted out

Webhooks

Webhooks notify you of email lifecycle events.

Register webhook

POST /api/comms/webhook

Response (shown once):

{
"url": "...",
"secret": "..."
}

Store the secret securely.

Signature verification (HMAC)

Headers:

X-Qflow-Signature: t=<timestamp>,v1=<hash>
X-Qflow-Webhook-Id: <uuid>
X-Qflow-Event: comms.delivered

Verify steps

  1. Read raw body

  2. Parse signature

  3. Check timestamp (±5 mins)

  4. Recompute HMAC

  5. Constant-time compare

Common mistakes

  • Re-stringifying JSON

  • Using == instead of constant-time compare

  • Skipping timestamp validation

  • Not deduping webhook IDs

Event types

comms.sent
Email successfully handed to SendGrid

comms.failed
Send failed before reaching SendGrid

comms.skipped
Idempotency triggered — email already sent

comms.delivered
Recipient mail server accepted the email

comms.opened
Email opened (can occur multiple times)

comms.bounced
Recipient mail server rejected the email

comms.dropped
SendGrid dropped the email (suppression list, invalid address, etc.)

comms.spam_reported
Recipient marked the email as spam

comms.unsubscribed
Recipient unsubscribed via email link

comms.test
Synthetic event triggered via webhook test endpoint

Payload example

{
"id": "...",
"type": "comms.delivered",
"createdAt": "...",
"data": {
"attendeeId": "...",
"templateId": "...",
"attendeeInvitationId": "...",
"status": "delivered",
"correlationId": "..."
}
}

Sending emails

Option A — Create + send

POST /api/guest

Includes:

{
"commsTemplateId": "...",
"correlationId": "order-123"
}

Option B — Send to existing attendee

POST /api/comms/send
{
"attendeeId": "...",
"templateId": "...",
"correlationId": "resend-123"
}

422 rejection cases

Idempotency

  • Use your own GUID id

  • Same request → no duplicate send

  • Different ID → new send

Errors

400 — Validation error

Missing required field, malformed GUID, or invalid JSON body

401 — Unauthorized
Bearer token is missing, invalid, or expired

403 — comms_api_not_enabled
Comms API access is not enabled for your account

403 — trust_required
Sender verification (KYC) has not been completed

404 — Not found
EventId, templateId, or webhook not found or not owned by you

422 — spam_check_failed
Template HTML contains URLs flagged as unsafe

422 — previous_send_permanently_failed
Recipient cannot be emailed (bounced, dropped, spam reported, or unsubscribed)

500 — Server error
Unexpected failure — retry the request or contact support if persistent

Rate limits

  • /api/guest → 500/min

  • /api/comms/send → 300/min

  • Bulk → 200 per request

Worked example

# Create template
POST /api/comms/template

# Register webhook
POST /api/comms/webhook

# Create guest + send
POST /api/guest

# Resend
POST /api/comms/send

Reference

Versioning

  • Breaking changes → 90 days notice

  • Additive changes → no notice

  • Ignore unknown fields