Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.qflowhub.io/llms.txt

Use this file to discover all available pages before exploring further.

Two patterns

You can send a templated email to a guest in two ways:

A. Atomic

POST /api/guest with commsTemplateId. Use on the ticket-purchase happy path — no window where the guest exists in our DB without their email queued.

B. Two-step

POST /api/comms/send. Use when the guest already exists (bulk-imported, “I lost my ticket” resends, or retries after transient API-side failures).
Same internal path, same webhooks, same idempotency. Pick whichever fits your flow.

A. Atomic — create + send

The send is triggered as part of guest creation — single round-trip:
curl -X POST 'https://api.qflowhub.io/checkin/v1/api/guest' \
  -H 'Authorization: Bearer <token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "id":              "<your guid for this attendee>",
    "firstName":       "Jane",
    "lastName":        "Smith",
    "email":           "jane@example.com",
    "eventId":         "<event guid>",
    "commsTemplateId": "<template guid>",
    "correlationId":   "order-2026-0042"
  }'
Response (200): the created attendee record.
The id field is yours to choose. It must be a globally-unique GUID. We recommend generating it on your side (one GUID per logical action), because retrying the same call with the same id is idempotent — see Idempotency below.
The send happens asynchronously. You’ll receive comms.sent (or comms.failed) at your webhook URL within seconds, followed by comms.delivered / comms.opened etc. as the email lifecycle progresses.

What happens internally

1

Attendee row created

Or returned as-is if your id is already in use — see Idempotency.
2

Synchronous validation

Template exists, belongs to your account, your account is verified for sending.
3

Email queued

Hangfire picks up the send, renders merge tokens, hands to SendGrid.
4

Lifecycle webhooks fire

comms.sent when SendGrid accepts. As lifecycle events come back from SendGrid (comms.delivered, comms.opened, comms.bounced…) we forward them.
All webhooks for one send share the same attendeeInvitationId so you can match them to the original send.

B. Two-step — send to an existing attendee

Use this whenever the attendee already exists in Qflow — for first sends to bulk-imported guests, for resends (“I lost my ticket” / customer requests duplicate), and for retries after transient failures.
curl -X POST 'https://api.qflowhub.io/comms/v1/api/comms/send' \
  -H 'Authorization: Bearer <token>' \
  -H 'Ocp-Apim-Subscription-Key: <subscription_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "attendeeId":    "<existing attendee guid>",
    "templateId":    "<template guid>",
    "correlationId": "resend-2026-0042"
  }'
Response (200) — send queued:
{
  "attendeeInvitationId": "<new tracking guid>",
  "status": "queued"
}
Watch for the usual lifecycle webhooks (comms.sent, comms.delivered, etc.) at your registered webhook URL.

When POST /api/comms/send rejects

The endpoint inspects the most recent AttendeeInvitation for this (attendee, template) pair. If the previous attempt is in a state where retrying won’t help, you get 422 with a structured body — sending again would just hit the same wall.
Previous stateSends?Why
No previous attemptFirst send
Delivered successfullyNew attempt — “I lost my ticket” use case
Transient API-side failure (we never reached SendGrid)Likely temporary; retry is sensible
bounced❌ 422Recipient mail server rejected the address — won’t change
dropped (after delivery to SendGrid)❌ 422SendGrid suppression / hard fail
spam_reported❌ 422Recipient marked as spam — anti-spam policy blocks resend
unsubscribed❌ 422Compliance — recipient has opted out
422 body shape:
{
  "error":             "previous_send_permanently_failed",
  "message":           "Previous send bounced — resending will not help.",
  "failureType":       "bounced",
  "lastFailureAt":     "2026-04-29T20:43:55Z",
  "lastFailureReason": "550 5.1.1 No such user"
}
failureType is one of: bounced, dropped, spam_reported, unsubscribed.

Idempotency

The caller-supplied id GUID is the idempotency key. Generate one per logical action (e.g. one per ticket purchase) and reuse it on any retry of the same logical action.
  • Same id → no duplicate send. The first call creates the attendee + queues the send. Subsequent calls with the same id return the existing attendee with no new send.
  • Different id → new send. If you need to re-send to the same person for a separate logical action, generate a new GUID.
Don’t reuse id across different people. Always generate a fresh GUID per logical action. The id is your idempotency token, not a customer reference (use correlationId for that).

correlationId

A free-form string (≤128 chars) you supply per send. We echo it back verbatim in every webhook event for that send (data.correlationId). Use it to correlate events to your own records (order ids, ticket numbers, etc.) without having to store our GUIDs. It’s optional. If you don’t supply one, correlationId is null in the webhook payload.