API reference
One endpoint: GET /api/listings. Machine-readable spec (OpenAPI 3.1) at /api/docs. Overview and quickstart at /developers.
Authentication
Every request needs a key. Send it either way - both are checked on every request and neither is deprecated:
Authorization: Bearer brbl_<64 hex chars> # or x-api-key: brbl_<64 hex chars>
If both headers are present, x-api-key is only used when Authorization is absent or isn't a Bearer value.
Get a key at /dashboard → Developer settings. One per account, free, shown once. Format is the literal prefix brbl_ followed by 64 lowercase hex characters (an HMAC-SHA256 digest) - treat the whole string as opaque, don't parse it.
Limit: 1,000 requests/day per key, resets at midnight IST. Every 200 response carries x-ratelimit-limit and x-ratelimit-remaining - back off before you hit zero rather than after.
401 response (missing or invalid key), checked before any query param is parsed:
HTTP/1.1 401
content-type: application/json
{
"error": "Missing API key. Pass it as \"Authorization: Bearer <key>\" or \"x-api-key: <key>\". Generate one from your dashboard: https://brabble.ai/dashboard",
"docs": "https://brabble.ai/developers"
}Known gap: production does not currently send a WWW-Authenticate header on 401 - only this JSON body. If you're scripting against this, match on the JSON, not the header.
GET /api/listings
curl -H "Authorization: Bearer brbl_<key>" \ "https://brabble.ai/api/listings?hub=hackathons&city=indore&limit=5"
Query parameters, all optional
hub · enum: hackathons | case-competitions | coding-contests | school-students | college-students
Top-level grouping. school-students and college-students match on eligibility text (regex over eligibility[]), not on type. The other three match on type.
city · enum: delhi-ncr, indore, hyderabad, pune, ahmedabad, bhopal, bengaluru, mumbai, chennai, kolkata, jaipur, lucknow
In-person listings only - mode=ONLINE rows are excluded outright, even if their city text would otherwise match.
platform · enum: unstop, devpost, devfolio, mlh, dorahacks, hackerearth, hack-club, ethglobal, internshala, codeforces, codechef, leetcode
One source platform, matched exactly against the internal platform name.
type · enum: HACKATHON, CASE STUDY, CONTEST, CODING, INNOVATION, DESIGN, COMPETITION, QUIZ
Server-side, exact match - this is a real filter applied before pagination, not just a client-side concern. Case-insensitive on input.
mode · enum: ONLINE | OFFLINE | HYBRID
Server-side, exact match.
free · literal: true
Only listings where fee is exactly "Free".
q · string
Case-insensitive substring match against title + organiser.
limit · integer, 1-200, default 50
Invalid/negative values silently fall back to default; values over 200 are clamped, not rejected.
offset · integer, default 0
Clamped to 10000 max. Page until count reaches total.
An unrecognised hub, city or platform value returns 400, not an empty list - a typo shouldn't silently ship as “zero results.”
Response schema
{
"refreshedAt": "2026-08-15T07:00:00.000Z",
"origin": "store",
"total": 214,
"count": 5,
"offset": 0,
"limit": 5,
"listings": [ { "...": "see below" } ],
"attribution": "Free to use, including commercially. Please credit Brabble.ai and link back where a reader can see it.",
"docs": "https://brabble.ai/developers"
}refreshedAt · string (ISO datetime)
When the underlying snapshot was last read - not when you called. Use this for staleness, not wall-clock.
origin · enum: "store" | "live"
"store" = last-known-good database rows (normal path). "live" = read from upstream on demand (fallback).
total · integer
Unpaged count of everything matching your filters. Page until count sums to total.
count · integer
listings.length for this page.
offset / limit · integer
Echoed back, post-clamping.
listings[].id · string
listings[].title · string
listings[].organiser · string
listings[].type · enum, see above
Normalised category.
listings[].kind · enum: "competition" | "contest"
Not redundant with type: this governs how deadline reads. "competition" → deadline is when applications close. "contest" → deadline is when it starts. Check kind before displaying deadline.
listings[].platform · string
Display name, e.g. "Unstop".
listings[].url · string (URL)
Outbound link to the organiser. Registration always happens there, never on Brabble.
listings[].shareUrl · string (URL)
Brabble's own share page for the listing.
listings[].deadline · string (ISO datetime), never null
Meaning depends on kind. Undated upstream rows are dropped entirely rather than guessed at.
listings[].mode · enum: ONLINE | OFFLINE | HYBRID
listings[].city · string
Free text from upstream. Can be "" or the literal "See listing" when the source has no real venue field - check for those before trusting it.
listings[].prize.label · string
Exactly as the organiser wrote it, e.g. "₹1L". Known limitation, not a bug: this is literally "See listing" for a large share of rows because the upstream source doesn't expose a real prize field.
listings[].prize.inr · number | null
Best-effort parse of label into rupees. null whenever unparseable, including the "See listing" case.
listings[].team · string
Display string, e.g. "1-4" or "Individual".
listings[].fee · string
Display string, e.g. "Free" or "₹500". Prefer the free=true query param over string-matching this.
listings[].eligibility · string[]
Raw eligibility statements as the organiser wrote them.
listings[].registered · integer | null
Registrant count if the source publishes one. Not comparable across platforms.
Freshness contract
The snapshot refreshes hourly (cache-control: public, max-age=3600 on every response). refreshedAt tells you exactly how old the data you got actually is - use that field, not a fixed assumption. There is no separate published SLA beyond the hourly cadence; if you need a hard staleness cutoff, treat anything more than a few hours old as worth a warning and anything past ~24h as stale enough to flag to your own users. A 36-hour cutoff is not the real cadence and will under-detect staleness - don't hardcode it.
Error responses
400 · Unknown hub/city/platform
{ "error": "Unknown hub: foo", "docs": "..." }
401 · Missing or invalid key
See Authentication above. No WWW-Authenticate header currently - JSON body only.
429 · Daily limit (1000) reached
{ "error": "Daily limit of 1000 requests reached. Resets at midnight IST.", "docs": "..." }. No Retry-After header currently - compute next-midnight-IST yourself.
5xx · Upstream/server failure
Not expected in normal operation; retry with backoff, same as any HTTP client would for a transient server error.
Versioning
There is no /api/v1/ prefix. /api/listings is the only version that has existed. There is currently no published changelog or deprecation process - the auth requirement on this endpoint was added to what was previously an open, unauthenticated route without a version bump or advance notice. Treat that as the operating precedent until a formal policy exists: pin only to fields you use, and code defensively against new fields appearing without warning.
Examples
Success
curl -H "x-api-key: brbl_<key>" "https://brabble.ai/api/listings?limit=1"
{
"refreshedAt": "2026-08-15T07:00:00.000Z",
"origin": "store",
"total": 214,
"count": 1,
"offset": 0,
"limit": 1,
"listings": [
{
"id": "unstop-123456",
"title": "Example Hackathon 2026",
"organiser": "Example Org",
"type": "HACKATHON",
"kind": "competition",
"platform": "Unstop",
"url": "https://unstop.com/...",
"shareUrl": "https://brabble.ai/s/unstop-123456",
"deadline": "2026-09-01T18:30:00.000Z",
"mode": "ONLINE",
"city": "",
"prize": { "label": "See listing", "inr": null },
"team": "1-4",
"fee": "Free",
"eligibility": ["Open to all college students"],
"registered": 312
}
],
"attribution": "Free to use, including commercially. Please credit Brabble.ai and link back where a reader can see it.",
"docs": "https://brabble.ai/developers"
}401 - no key
curl "https://brabble.ai/api/listings"
HTTP/1.1 401
{
"error": "Missing API key. Pass it as \"Authorization: Bearer <key>\" or \"x-api-key: <key>\". Generate one from your dashboard: https://brabble.ai/dashboard",
"docs": "https://brabble.ai/developers"
}Fair use
Cache what you fetch - the underlying data changes about once a day, hourly polling is generous.
Send registrants to the organiser's own URL. Never collect entries or fees on their behalf.
Reuse the rows freely, including commercially. Don't republish the whole set as your own without the attribution string, and don't drop the caveats attached to prize/count figures.