Webhooks
Flashpoint.AI receives webhooks from panel providers (Prolific, Dynata) to update fielding status in real time, and from Stripe for billing events. When a respondent completes a study, a panel reaches its target, or a subscription state changes, the provider notifies Flashpoint.AI and the platform updates its records automatically.
These endpoints are inbound only — Flashpoint.AI is the receiver, not the sender. You do not subscribe to them; they are documented here for transparency about how the platform reacts to upstream events.
Prolific webhooks
POST /api/v1/webhooks/prolific
Prolific sends webhooks for respondent-level and study-level events.
Events
| Event | Trigger | What Flashpoint.AI does |
|---|---|---|
submission.completed | Respondent finished the study | Logs completion, links response to the panel integration |
submission.returned | Respondent returned their slot | Logs the return so the slot can be re-filled |
study.completed | Study reached target completions | Sets the panel integration status to completed, emits a PANEL_COMPLETED event |
Signature verification
Prolific signs every webhook with HMAC-SHA256. The signature is sent in the X-Prolific-Signature header.
Flashpoint.AI verifies the signature by computing the HMAC of the raw request body using the shared secret and comparing it to the header value. Requests with missing or invalid signatures are rejected with 401.
Payload shape
{
"event_type": "submission.completed",
"data": {
"participant_id": "60a1b2c3d4e5f6...",
"study_id": "61a2b3c4d5e6f7..."
}
}
Dynata webhooks
POST /api/v1/webhooks/dynata
Dynata sends webhooks for pricing changes, line-item state transitions, and project-level state changes.
Events
| Event | Trigger | What Flashpoint.AI does |
|---|---|---|
REPRICING | Dynata raised the per-complete cost mid-fielding | Auto-pauses the integration to protect the budget. Appends the repricing event to the audit log. Emits PANEL_PAUSED. |
LINE_ITEM_STATE_CHANGE | A line item transitioned state (e.g. LAUNCHED, PAUSED) | Records the new state in provider_config for audit trail. No status change on the Flashpoint.AI side. |
PROJECT_STATE_CHANGE | Project transitioned state (e.g. COMPLETED, CLOSED) | Records the new state. If terminal (COMPLETED or CLOSED), marks the integration as completed and emits PANEL_COMPLETED. |
Repricing auto-pause
Repricing is a financial safety event. When Dynata raises the cost-per-complete beyond what the customer originally approved, Flashpoint.AI automatically pauses the integration via the Dynata API. The customer must explicitly re-approve the new rate before fielding resumes. The old and new prices are recorded in the integration's provider_config.repricing_events array.
Signature verification
Dynata signs webhooks with the same algorithm as Prolific: HMAC-SHA256 over the raw body, sent in the X-Dynata-Signature header. Missing or invalid signatures are rejected with 401.
Payload shape
{
"type": "REPRICING",
"eventId": "evt_abc123",
"projectId": "proj_xyz789",
"lineItemId": "li_456",
"oldPrice": { "cpi": 4.50 },
"newPrice": { "cpi": 6.25 },
"reason": "Feasibility adjustment"
}
Stripe webhooks (billing)
POST /webhooks/stripe
Stripe drives subscription state and per-action payments. Every billing-related event is delivered here; the handler is idempotent (events are deduplicated by event.id server-side) and resilient to retries.
Events
| Event | Trigger | What Flashpoint.AI does |
|---|---|---|
customer.subscription.created | New seat-tier subscription | Mirrors purchased seats + Stripe IDs into the team's plan |
customer.subscription.updated | Seat count, plan, or status change | Re-mirrors the subscription state |
customer.subscription.deleted | Subscription cancelled | Clears the team's subscription state and revokes any active licenses beyond the free-seat cap (newest-first, so the longest-tenured holders keep access) |
checkout.session.completed | Hosted Checkout session paid | Flips the matching payment row to succeeded and publishes a fan-out event so downstream services (panel launches, etc.) react |
checkout.session.expired | Hosted Checkout session lapsed (24h unpaid) | Flips the payment row to expired and rolls back any pending action it was funding |
payment_intent.succeeded | Direct PaymentIntent confirmed | Marks the intent paid and dispatches the success callback to the originating service |
payment_intent.payment_failed | Direct PaymentIntent declined | Marks the intent failed and dispatches the failure callback |
charge.refunded | Refund settled on a charge | Appends an audit event to the corresponding payment intent |
Any other event types Stripe delivers are recorded and acknowledged with 200 but produce no state change.
Signature verification
Stripe signs every webhook. The signature is sent in the stripe-signature header and verified server-side using Stripe's standard constructEvent against the configured webhook secret. Missing or invalid signatures are rejected with 400 and Stripe retries with backoff.
Idempotency
Every received event is recorded in a stripe_events table on first delivery. Re-deliveries (Stripe retries, multi-region duplication) collapse to 200 {"received": true, "deduped": true} without re-running any side effects.
Response shape
| Status | Body | Meaning |
|---|---|---|
200 | {"received": true} | Event accepted and dispatched |
200 | {"received": true, "deduped": true} | Already processed; no-op |
400 | {"error": "missing stripe-signature header"} or "signature verification failed" | Stripe will retry |
500 | {"error": "handler error"} | Handler raised; Stripe will retry |
Verifying signatures
Both panel providers use the same HMAC-SHA256 algorithm. (Stripe uses its own scheme — see the Stripe section above.) Here is a reference implementation for the panel webhooks:
import hashlib
import hmac
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
"""Verify an HMAC-SHA256 webhook signature.
Args:
body: Raw request body bytes.
signature: Hex-encoded signature from the header.
secret: Shared secret configured for the provider.
Returns:
True if the signature is valid.
"""
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Use hmac.compare_digest (constant-time comparison) to prevent timing attacks.
Response behavior
Both endpoints return 200 {"status": "ok"} on success. Providers retry on non-2xx responses, so returning 200 even for internally unroutable events (e.g. unknown project_id) prevents retry storms. Actual processing failures are captured in the audit log for investigation.