Webhook Payload & Signature Reference

Every FluoTest webhook integration sends an HTTP POST with a JSON body when a quiz is submitted. This page documents the request format, the default payload fields, the alternate payload shape used with field mapping, and the signature scheme.

Last updated

What does a FluoTest webhook payload look like?

By default it's a flat JSON object with the submission's identifiers, contact fields, score, tier or outcome, the full answers object, and a submission timestamp. If a signing secret is set, requests include an X-FluoTest-Signature header (HMAC-SHA256 of the raw body).

Configuration

A webhook integration is configured with a destination URL and an optional signing secret. No OAuth or third-party account connection is required.

Request format

Every webhook request is sent with the following method and headers:

HeaderValueNotes
MethodPOSTAlways a POST request.
Content-Typeapplication/jsonThe body is JSON-encoded.
User-AgentFluoTest-Webhook/1.0Identifies the request as coming from FluoTest.
X-FluoTest-Eventsubmission.createdThe only event type currently sent.
X-FluoTest-Signaturesha256=<hex>Present only when a signing secret is configured for the webhook.

Default payload fields

Without field mapping configured, the request body is a flat JSON object:

{
  "submission_id": "8f2b1c3a-...",
  "test_id": "3a9d7e10-...",
  "test_name": "Lead Fit Quiz",
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Doe",
  "company": "Acme Inc",
  "contact_type": "email",
  "respondent_name": "Jane Doe",
  "total_score": 82,
  "max_score": 100,
  "score_percent": 82,
  "tier": "High fit",
  "scores": null,
  "outcome": null,
  "outcomes": null,
  "disqualified": false,
  "answers": {
    "sections": [
      {
        "section_title": "Section 1",
        "questions": [
          {
            "question_id": "q1",
            "question_text": "What's your role?",
            "question_type": "multiple_choice",
            "answer": "Founder"
          }
        ]
      }
    ]
  },
  "submitted_at": "2026-07-10T14:32:00.000Z"
}
FieldTypeDescription
submission_idstringUnique ID of the submission.
test_idstringID of the quiz.
test_namestringName of the quiz.
emailstring | nullRespondent's email, if collected.
first_namestring | nullRespondent's first name, if collected.
last_namestring | nullRespondent's last name, if collected.
companystring | nullCompany name, if collected.
contact_typestring | nullWhich contact method was collected: "email", "phone", or null.
respondent_namestring | nullFull name, if a name question was answered.
total_scorenumberThe respondent's total score.
max_scorenumberThe maximum possible score.
score_percentnumberScore expressed as a percentage.
tierstring | nullLabel of the matched score tier (single-score quizzes).
scoresobject | nullPer-category scores for multi-score quizzes; null otherwise.
outcomestring | nullKey of the matched primary outcome (Pro outcome engine only).
outcomesstring[] | nullKeys of all matched outcomes.
disqualifiedbooleanWhether the respondent was disqualified.
answersobjectNested sections/questions/answers — see below.
submitted_atstringISO 8601 submission timestamp.

Sending a subset of fields

A webhook can be configured to send only a chosen subset of the fields above. submission_id and test_id are always included regardless of that selection, so every payload can be traced back to its source.

Mapped payload mode

When field mapping is configured for a webhook (see Field Mapping & Sync Retries), the payload shape changes: the mapped fields produced by the mapping rules replace the flat structure, alongside any tags, and the full default payload is included unmodified under raw.

{
  "mapped": { "email": "[email protected]", "score": 82 },
  "tags": ["High fit"],
  "raw": { "submission_id": "8f2b1c3a-...", "...": "..." }
}

Verifying the signature

When a signing secret is set, every request includes an X-FluoTest-Signature header: the HMAC-SHA256 hex digest of the exact request body, computed with the shared secret and prefixed with sha256=. Recompute the same digest over the raw request body on the receiving end and compare it to the header to confirm the request came from FluoTest and wasn't altered in transit.

const crypto = require("crypto");

function isValidSignature(rawBody, header, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return header === expected;
}

Delivery, retries, and failures

A webhook request times out after 10 seconds. A non-2xx response status is treated as a failed delivery. Failed deliveries are retried automatically — see Field Mapping & Sync Retries for the retry schedule and how failures are logged.

Frequently asked questions

What triggers a FluoTest webhook?

A webhook fires once per quiz submission, with the event type submission.created. There is currently no other event type.

How do I verify the X-FluoTest-Signature header?

Compute an HMAC-SHA256 digest of the raw request body using the signing secret configured for the webhook, hex-encode it, prefix it with sha256=, and compare it to the header value using a constant-time comparison.

What happens if my endpoint is unreachable or returns an error?

The delivery is marked failed and retried automatically with backoff, up to 3 additional attempts, unless the failure is considered permanent.

Can I choose which fields are sent in the payload?

Yes. A webhook can be configured with a specific list of fields; only those fields, plus submission_id and test_id, are included in each request.