- Guides
- Ask for a decision
Guides
Ask for a decision
Send a Telegram confirm, select, or text-input interaction and read the first durable answer.
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.