Connect Loops to anything
Loops' public REST API is language-agnostic. Websites, mobile apps, WhatsApp/Telegram/Discord bots, Slack, GitHub Actions, even cron jobs — any HTTP client with a bearer token works.
3 minutes
Quick start
- 1Sign in to Loops and get admin access.
- 2Settings → API Keys page, create a secret or publishable key.
- 3Try the API with cURL:bash
curl -H "Authorization: Bearer loop_sk_..." \ https://your-loop.app/api/v1/posts
HTML
Embed widget
Drop a feedback board into any HTML page with a single line. Vanilla JS, ~8KB, no dependencies.
<div id="loop-board"></div>
<script src="https://your-loop.app/loop-widget.js"
data-key="loop_pk_..."
data-host="https://your-loop.app"
data-target="#loop-board"
data-theme="light"
data-locale="tr"></script>data-key: publishable key (loop_pk_…). Never put a secret key in the browser.
data-theme: light or dark.
data-locale: tr or en.
data-target: CSS selector where the widget mounts.
Bearer auth
REST API
All endpoints are JSON. CORS is open. Every request needs Authorization: Bearer <key>. Errors come back as { error: { code, message } }.
/api/v1/postsList all feedback items.
statustaglimitoffsetcurl -H "Authorization: Bearer loop_sk_..." \
"https://your-loop.app/api/v1/posts?status=planned&limit=10"/api/v1/postsCreate a new post (write scope).
titledescriptiontagexternal_user_idsourcecurl -X POST -H "Authorization: Bearer loop_sk_..." \
-H "Content-Type: application/json" \
-d '{"title":"Dark theme please","tag":"UI","source":"telegram"}' \
https://your-loop.app/api/v1/posts/api/v1/posts/:id/voteVote / unvote on a post (toggle).
external_user_idcurl -X POST -H "Authorization: Bearer loop_pk_..." \
-H "X-Loop-External-User: tg_user_12345" \
https://your-loop.app/api/v1/posts/POST_ID/vote/api/v1/posts/:idUpdate a post's status/tag (admin scope).
statustag/api/v1/posts/:idDelete a post (admin scope).
Outgoing
Webhooks
When a new post / vote / status change happens in Loops, we POST to your URL. Bridge it to Slack, Discord, Linear, GitHub Issues or anything else.
Every request carries an X-Loop-Signature header — your webhook secret signed with HMAC-SHA256(body). Node verification example below:
import crypto from "node:crypto";
function verify(req, secret) {
const sig = req.headers["x-loop-signature"];
const expected = crypto.createHmac("sha256", secret)
.update(req.rawBody).digest("hex");
return sig && crypto.timingSafeEqual(
Buffer.from(sig), Buffer.from(expected)
);
}Sample webhook payload: { event: "post.created", post: { id, title, ... }, timestamp }
Python
Telegram bot
Bot that posts to Loops with the /feedback <text> command. ~30 lines.
pip install python-telegram-bot requests
export TELEGRAM_TOKEN=...
export LOOP_KEY=loop_sk_...
export LOOP_HOST=https://your-loop.appimport os, requests
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
LOOP_HOST = os.environ["LOOP_HOST"]
LOOP_KEY = os.environ["LOOP_KEY"]
async def feedback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
text = " ".join(ctx.args).strip()
if len(text) < 3:
await update.message.reply_text("Usage: /feedback <your idea>")
return
user = update.effective_user
r = requests.post(
f"{LOOP_HOST}/api/v1/posts",
headers={"Authorization": f"Bearer {LOOP_KEY}"},
json={
"title": text[:140],
"source": "telegram",
"external_user_id": f"tg_{user.id}",
"tag": "telegram",
},
)
if r.ok:
await update.message.reply_text("✓ Got it, check the board.")
else:
await update.message.reply_text(f"Error: {r.text}")
app = Application.builder().token(os.environ["TELEGRAM_TOKEN"]).build()
app.add_handler(CommandHandler("feedback", feedback))
app.run_polling()Node + Twilio
WhatsApp bot
Convert messages received via Twilio's WhatsApp Sandbox into Loops posts. Point Twilio's webhook at this Express endpoint.
npm i express twilio body-parser
# Twilio Sandbox webhook URL: https://your-bot.com/waimport express from "express";
import twilio from "twilio";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post("/wa", async (req, res) => {
const body = (req.body.Body || "").trim();
const from = req.body.From; // "whatsapp:+90555..."
const r = await fetch(`${process.env.LOOP_HOST}/api/v1/posts`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LOOP_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: body.slice(0, 140),
source: "whatsapp",
external_user_id: from,
tag: "whatsapp",
}),
});
const twiml = new twilio.twiml.MessagingResponse();
twiml.message(r.ok ? "✓ Got it, posted to the board." : "❌ Something went wrong.");
res.type("text/xml").send(twiml.toString());
});
app.listen(3000);Node + discord.js
Discord bot
Minimal bot that posts to Loops via the /feedback slash command.
npm i discord.js
# Create a bot + applications.commands scope in the Discord Developer Portalimport {
Client, GatewayIntentBits, REST, Routes,
SlashCommandBuilder,
} from "discord.js";
const cmd = new SlashCommandBuilder()
.setName("feedback")
.setDescription("Send feedback to Loops")
.addStringOption(o => o.setName("text").setDescription("Your idea").setRequired(true));
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
await rest.put(
Routes.applicationCommands(process.env.DISCORD_APP_ID),
{ body: [cmd.toJSON()] }
);
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on("interactionCreate", async (i) => {
if (!i.isChatInputCommand() || i.commandName !== "feedback") return;
const text = i.options.getString("text", true);
const r = await fetch(`${process.env.LOOP_HOST}/api/v1/posts`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LOOP_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: text.slice(0, 140),
source: "discord",
external_user_id: `dc_${i.user.id}`,
tag: "discord",
}),
});
await i.reply({ content: r.ok ? "✓ Added." : "❌ Error.", ephemeral: true });
});
client.login(process.env.DISCORD_TOKEN);