Skip to listings
API Docs

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.