Deck Docs

Headless submit

Submit feedback with your own UI via useSubmitFeedback or submitFeedback.

Headless submit

Use headless APIs when you own the form, modal, and styling. Deck only handles validation and HTTP transport to the widget ingest API.

Must run under DeckProvider (usually with headless).

import {
  DeckProvider,
  useSubmitFeedback,
  FEEDBACK_SENTIMENTS,
} from "@deck-io/feedback";

function App() {
  return (
    <DeckProvider
      headless
      publishableKey={pk}
      user={{ id: user.id, email: user.email }}
    >
      <MyFeedbackModal />
    </DeckProvider>
  );
}

function MyFeedbackModal() {
  const { submitFeedback, isSubmitting, lastError, reset } =
    useSubmitFeedback();
  const [sentiment, setSentiment] = useState<"positive" | "neutral" | "negative">(
    "positive",
  );
  const [message, setMessage] = useState("");

  async function onSubmit() {
    const result = await submitFeedback({
      widgetId: "wgt_your_widget",
      sentiment,
      message,
      context: { surface: "settings" },
    });

    if (result.ok) {
      // close modal, toast, etc.
      // result.submissionId may be present
    }
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void onSubmit();
      }}
    >
      <select
        value={sentiment}
        onChange={(e) =>
          setSentiment(e.target.value as typeof sentiment)
        }
      >
        {FEEDBACK_SENTIMENTS.map((s) => (
          <option key={s} value={s}>
            {s}
          </option>
        ))}
      </select>
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} />
      {lastError ? <p>{lastError}</p> : null}
      <button type="submit" disabled={isSubmitting}>
        Send
      </button>
    </form>
  );
}

Return value

FieldDescription
submitFeedback(input)Posts to Deck using provider key + user
isSubmittingtrue while a request is in flight
lastErrorLast error message, or null
lastResultFull last response (ok: true | ok: false)
reset()Clears error/result/submitting state
userUser from DeckProvider
apiUrlResolved ingest URL
resolvedColorScheme"light" | "dark" (theme-related)

Submit input

{
  widgetId: string;
  sentiment: "positive" | "neutral" | "negative";
  message?: string;
  context?: Record<string, string | number | boolean | null | undefined>;
}

user and meta (URL, user agent, SDK version, timestamp) are filled by the provider/client.

Low-level: submitFeedback

Works without React context when you pass key + user yourself (non-React hosts, advanced cases).

import { submitFeedback } from "@deck-io/feedback";

const result = await submitFeedback({
  publishableKey: "pk_...",
  user: { id: "user_1", email: "a@b.com" },
  urlCapture: "path",
  submission: {
    widgetId: "wgt_...",
    sentiment: "positive",
    message: "Loved the new onboarding",
    context: { surface: "onboarding" },
  },
  // optional metadata override
  meta: { source: "client" },
});

if (result.ok) {
  console.log(result.submissionId);
} else {
  console.error(result.error, result.status);
}

Prefer useSubmitFeedback in React apps under DeckProvider.

Server-side (no React)

For Node, Route Handlers, and Server Actions, import from @deck-io/feedback/server instead. Prefer createDeckFeedbackClient — it defaults URL capture to "none" and sets meta.source to "server".

import { createDeckFeedbackClient } from "@deck-io/feedback/server";

const deck = createDeckFeedbackClient({
  publishableKey: process.env.DECK_PUBLISHABLE_KEY!,
});

await deck.submit({
  user: { id: session.userId },
  submission: {
    widgetId: "wgt_...",
    sentiment: "negative",
    message: "API timeout on checkout",
  },
  meta: { url: request.headers.get("referer") ?? undefined },
});

Full guide: Server-side submit.

Response shape

// success
{ ok: true, submissionId?: string }

// failure
{ ok: false, error: string, status?: number }

Next