Define a form on the Feedback tab. Your game shows it whenever you want. Answers come back grouped as one submission per call, tagged to the session and the build. Free for every studio.
A Player Feedback form is a stable id (ff_…) plus one or more fields. Each field has its own id (q1, q2, …), a player-facing label, and a kind:
rating-1-5: a 1 to 5 scale (most studios render 5 stars).short-text: single-line string.long-text: multi-line string.yes-no: binary choice.Each call to feedback.submit() lands as ONE submission server-side (shared submissionId) with N answer rows. The dashboard reassembles them into one card per submission on the tester detail page. Field labels and kinds are denormalized at write time, so editing the form later doesn't rewrite history.
Every SDK exposes feedback.submit(...) on the top-level client. The TypeScript shape:
await client.feedback.submit({
formId: 'ff_xyz_from_dashboard',
sessionId: currentSessionId,
responses: [
{ fieldId: 'q1', value: rating }, // "1".."5"
{ fieldId: 'q2', value: whatWorked }, // short-text
{ fieldId: 'q3', value: whatDidnt }, // long-text (skipped if empty)
],
askedAtSec: Math.floor(askedAt / 1000),
answeredAtSec: Math.floor(Date.now() / 1000),
})All five SDKs (TypeScript, Python, Unity, Godot, Unreal) share the same wire format under the hood — the shape above is what every client.Feedback.Submit(...) call sends. Unity, Godot, and Unreal additionally ship drop-in feedback widgets on top of that library API (see Default in-engine UIbelow); TypeScript and Python expose just the wire-format method. Each SDK's README has the engine-native code snippet.
On the dashboard the submission shows up under the tester's detail page (/games/[slug]/users/[deviceId]), grouped by submissionId, with the per-field labels denormalized at write time.
Unity, Godot, and Unreal each ship a default form that builds programmatically at runtime, no prefab or scene to maintain. One line opens it; the SDK awaits the player, submits, then removes the form. TypeScript and Python are logic-only by design (no UI runtime), so you bring your own form there.
var result = await client.FeedbackForm.OpenAsync(
formId: "ff_xyz",
sessionId: currentSessionId);
// result.Outcome is Submitted / Dismissed / Failedvar result: Dictionary = await playloop.feedback_form.open({
"form_id": "ff_xyz",
"session_id": current_session_id,
"parent": self,
})
# result["outcome"] is "submitted" / "dismissed" / "failed"auto* Widget = UPlayloopFeedbackWidget::CreateDefault(
GetWorld(), Client, TEXT("ff_xyz"),
UPlayloopClient::GetDefaultFeedbackFields());
Widget->OnFeedbackResolved.AddDynamic(this, &AMyHUD::HandleFeedback);All three accept a theme / field override so you can restyle without authoring a full UMG widget / UGUI prefab / Control scene from scratch.
Every feedback submission inherits whatever identity the session already has. Three tiers, surfaced as a badge on the dashboard:
deviceId. Grey badge with a short fingerprint of the device GUID.setTesterHandle(...). No badge, just the handle in monospace.See tester keys for the redemption flow that promotes a tester from anonymous to linked.
Two server-side limits, hardcoded, no opt-out. Both keep the dashboard clean and keep Playloop's brand off any "the feedback form keeps spamming me" perception:
(session, form): the same form can't be submitted twice in one session. Reject reason already_submitted, HTTP 409.session_submission_limit, HTTP 429.Both reasons surface through the SDK as errors carrying a usable reasonfield (PlayloopApiError / PlayloopPlaytestException / PlayloopError, depending on engine). Your code can branch and show a friendly "Thanks, we got your feedback" UI without retrying.
If you want to ask the player after every level, design your form around it (multi-field, single submission) instead of chaining N forms. The 3-per-session cap exists specifically to make chaining painful.
Submitted feedback lands in the session's raw event stream just like any tracked event. The same AI extraction pipeline that surfaces friction quotes and behavioral patterns reads feedback as high-signal player voice. No extra wiring on the studio side. Linked-tester submissions weight higher than anonymous ones in per-tester rollups.
On the dashboard:
submissionId with per-field labels and timestamps.Don't call feedback.submit from a quit handler.
The HTTP request may not flush before the process dies. Capture answers in your Back to menu or Pause handler, then call endSession() after the submission completes. Every engine SDK handles offline / retry behavior through the same queue as track().
These docs are evolving. Playloop is in active development ahead of launch, so APIs and details may change as we polish.