1. Guides
  2. Ask for a decision

​
Ask a person and continue with their answer

An interaction turns a notification into a small, bounded decision. WhooshBang supports confirmation, selection from two to six choices, and short text input. The first terminal outcome wins, so a duplicate Telegram callback or a racing expiry cannot produce two answers.

Complete the quickstart first. For a real Telegram decision, use a live credential and a subscriber who has activated the subscription link.

​
Send a confirmation

Every interaction has an expiry between 30 seconds and 24 hours. Use an idempotency key derived from the decision your application is requesting, so a job retry returns the original message instead of asking twice.

export WHOOSHBANG_INTERACTION_EXPIRES_AT="$(node --print 'new Date(Date.now() + 15 * 60 * 1000).toISOString()')"

curl --silent --show-error --include \
  --request POST https://api.whooshbang.com/v1/messages \
  --header "Authorization: Bearer $WHOOSHBANG_LIVE_CREDENTIAL" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: deployment-987-approval' \
  --data "{\"to\":{\"subscriber_id\":\"operator_123\"},\"content\":{\"type\":\"text\",\"text\":\"A deployment is waiting for your decision.\"},\"interaction\":{\"type\":\"confirm\",\"prompt\":\"Approve this deployment?\",\"confirm_label\":\"Approve\",\"deny_label\":\"Deny\",\"expires_at\":\"$WHOOSHBANG_INTERACTION_EXPIRES_AT\",\"correlation_id\":\"deployment_987\"}}"

With the JavaScript SDK, the builder checks the interaction bounds before a request leaves your process:

import { WhooshBangClient, confirmInteraction } from "@whooshbang/sdk";

const whooshBang = new WhooshBangClient({
  baseUrl: "https://api.whooshbang.com",
  credential: process.env.WHOOSHBANG_LIVE_CREDENTIAL!,
  fetch,
});

const sent = await whooshBang.createMessage(
  {
    to: { subscriber_id: "operator_123" },
    content: {
      type: "text",
      text: "A deployment is waiting for your decision.",
    },
    interaction: confirmInteraction({
      prompt: "Approve this deployment?",
      confirmLabel: "Approve",
      denyLabel: "Deny",
      expiresInSeconds: 900,
      correlationId: "deployment_987",
    }),
  },
  { idempotencyKey: "deployment-987-approval" },
);
if (sent.outcome !== "accepted") {
  // Their channel state refused the send: revoked, never finished, or a
  // preferred channel that is paused. Nothing was sent; invite them again.
  throw new Error("Invite the person again before asking.");
}
const message = sent.message;

The accepted message contains interaction.id. Keep that public identifier; it is what you inspect for the answer.

​
Wait for the result

An interaction read can wait for up to 30 seconds. If nobody answers during that interval, the request returns the current open state normally, and the call consumes nothing, so reading again is always safe.

curl --silent --show-error \
  --header "Authorization: Bearer $WHOOSHBANG_LIVE_CREDENTIAL" \
  "https://api.whooshbang.com/v1/interactions/$INTERACTION_ID?wait=30"

A person can take longer than one hold to answer, so the SDK ships the loop rather than leaving you to write it. ask sends the question and waits for the answer in one call, and patienceMs says how long you are prepared to wait:

import { WhooshBangClient, confirmInteraction } from "@whooshbang/sdk";

const whooshBang = new WhooshBangClient({
  baseUrl: "https://api.whooshbang.com",
  credential: process.env.WHOOSHBANG_LIVE_CREDENTIAL!,
  fetch,
});

const asked = await whooshBang.ask(
  {
    to: { subscriber_id: "operator_123" },
    content: {
      type: "text",
      text: "A deployment is waiting for your decision.",
    },
    interaction: confirmInteraction({
      prompt: "Approve this deployment?",
      confirmLabel: "Approve",
      denyLabel: "Deny",
      expiresInSeconds: 900,
      correlationId: "deployment_987",
    }),
  },
  { idempotencyKey: "deployment-987-approval", patienceMs: 15 * 60_000 },
);

switch (asked.outcome) {
  case "answered":
    console.log(asked.message.id, asked.answer.value);
    break;
  case "answer_not_retained":
    // They answered; the typed answer has left its retention window.
    break;
  case "open":
    // Fifteen minutes passed and nobody answered. The interaction is still
    // open, so you can keep waiting or move on.
    break;
  case "expired":
  case "cancelled":
  case "retired":
    break;
  case "subscriber_unbound":
  case "preferred_binding_unavailable":
    // Their channel state refused the send: revoked, never finished, or a
    // preferred channel that is paused. Nothing was sent; invite them again.
    break;
}

ask resolves with an outcome you branch on, and the typed answer exists only on answered, so the compiler tells you about the case you forgot. Every interaction outcome also carries the accepted message. The terminal states are answered, expired, cancelled, and retired; open means your patience ran out first. An answered interaction keeps the fact and time of the answer after its typed answer ages out; that reads as answer_not_retained rather than as a missing answer.

Under the hood ask sends exactly once — a retry with the same idempotencyKey returns the original message rather than asking twice — and then repeats the bounded read while the interaction is open, re-issuing a read that a dropped connection or a rate limit interrupts. To keep waiting on a question you already sent, or one another process sent, wait on the interaction directly:

const read = await whooshBang.awaitInteraction(asked.message.interaction!.id, {
  patienceMs: 60 * 60_000,
});

It resolves with the same outcomes, without the message. Omit patienceMs to wait until the interaction itself ends; every interaction expires within 24 hours. To read the current state once without waiting, getInteraction and interactionAnswer are still there and make exactly one request.

​
Choose another interaction type

Use selectInteraction for two to six stable choices and inputInteraction for a short text response. They use the same send and inspection calls as the confirmation above. Keep option values meaningful to your code, keep labels clear to a person, and treat all free-text answers as untrusted input.

For automation that should receive answers without polling, use a signed customer endpoint or the hosted WhooshBang MCP connection. Those transports carry the same canonical first-terminal-writer outcome.