{"openapi":"3.1.0","info":{"title":"Brabble.ai API","version":"1.0.0","summary":"Live feed of hackathons, case competitions and coding contests open to students in India, merged from sixteen platforms.","description":"Free, key-required JSON API. All responses are served from an hourly snapshot (see refreshedAt in each response), not fetched live per request. Attribution required: credit Brabble.ai and link back where a reader can see it. Registration always happens on the organiser's own site - this API never collects entries.","contact":{"url":"https://brabble.ai/developers"},"license":{"name":"Free to use, including commercially, with attribution"}},"servers":[{"url":"https://brabble.ai"}],"security":[{"ApiKeyHeader":[]},{"BearerAuth":[]}],"paths":{"/api/listings":{"get":{"operationId":"listListings","summary":"List open competitions and hackathons","description":"Returns a page of currently-open listings, sorted by deadline (soonest first). Expired listings are never included. Served from the same hourly-refreshed snapshot the website renders from.","parameters":[{"name":"hub","in":"query","required":false,"description":"Top-level grouping. school-students and college-students filter on stated eligibility text, not on type - the other three filter on type.","schema":{"type":"string","enum":["hackathons","case-competitions","coding-contests","school-students","college-students"]}},{"name":"city","in":"query","required":false,"description":"In-person listings in/around that city. Excludes ONLINE-mode listings entirely, since a city filter implies travel.","schema":{"type":"string","enum":["delhi-ncr","indore","hyderabad","pune","ahmedabad","bhopal","bengaluru","mumbai","chennai","kolkata","jaipur","lucknow"]}},{"name":"platform","in":"query","required":false,"description":"One source platform.","schema":{"type":"string","enum":["unstop","devpost","devfolio","mlh","dorahacks","hackerearth","hack-club","ethglobal","internshala","codeforces","codechef","leetcode"]}},{"name":"type","in":"query","required":false,"description":"Server-side exact-match filter on the listing's normalised type (case-insensitive on input, compared uppercase). A value outside the enum is a 400, not an empty result.","schema":{"type":"string","enum":["HACKATHON","CASE STUDY","CONTEST","CODING","INNOVATION","DESIGN","COMPETITION","QUIZ"]}},{"name":"mode","in":"query","required":false,"description":"Server-side exact-match filter (case-insensitive on input). A value outside the enum is a 400, not an empty result.","schema":{"type":"string","enum":["ONLINE","OFFLINE","HYBRID"]}},{"name":"free","in":"query","required":false,"description":"Set to true to return only listings where feeLabel is exactly \"Free\". Any value other than \"true\" or \"false\" is a 400.","schema":{"type":"string","enum":["true","false"]}},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match against title + organiser.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size. Absent -> 50. A non-integer value, or one outside 1-200, is a 400 - not silently clamped.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"offset","in":"query","required":false,"description":"Row offset for paging. Absent -> 0. A non-integer value, or one outside 0-100000, is a 400.","schema":{"type":"integer","minimum":0,"maximum":100000,"default":0}}],"responses":{"200":{"description":"A page of listings.","headers":{"x-ratelimit-limit":{"schema":{"type":"integer"},"description":"Daily request cap for this key (1000)."},"x-ratelimit-remaining":{"schema":{"type":"integer"},"description":"Requests left today for this key."},"x-ratelimit-reset":{"schema":{"type":"integer"},"description":"Seconds until the counter resets (the next 00:00 IST)."},"cache-control":{"schema":{"type":"string"},"description":"public, max-age=3600 - the snapshot refresh interval."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListingsResponse"}}}},"400":{"description":"A query param failed validation: hub/city/platform/type/mode outside its enum, free not \"true\"/\"false\", or limit/offset non-integer or out of range. Returned instead of an empty result so a typo doesn't silently ship as \"zero listings\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key. Checked before query params are parsed.","headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"NOTE: production does not currently send this header on 401 - only a JSON body. Documented here as the intended contract; treat its absence as a known gap, not a signal to guess auth some other way."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Daily limit (1000 requests) reached for this key. Resets at 00:00 IST.","headers":{"retry-after":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying (until 00:00 IST)."},"x-ratelimit-limit":{"schema":{"type":"integer"},"description":"1000."},"x-ratelimit-remaining":{"schema":{"type":"integer"},"description":"0 on a 429."},"x-ratelimit-reset":{"schema":{"type":"integer"},"description":"Seconds until the counter resets - same value as retry-after."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"securitySchemes":{"ApiKeyHeader":{"type":"apiKey","in":"header","name":"x-api-key","description":"Preferred header. Value is the raw key, e.g. x-api-key: brbl_<64 hex chars>."},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"brbl_<64 hex chars>","description":"Equivalent to x-api-key: send Authorization: Bearer brbl_<key>. Both header forms are checked on every request (x-api-key wins only if Authorization is absent or not a Bearer value) - this is intentional, not a bug or a migration in progress."}},"schemas":{"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable cause, safe to show a developer as-is."},"docs":{"type":"string","format":"uri","description":"Link to /developers. Present on every error."}}},"Listing":{"type":"object","description":"One competition/hackathon/contest row.","required":["id","title","organiser","type","kind","platform","url","shareUrl","deadline","mode","city","prize","team","fee","eligibility","registered"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"organiser":{"type":"string"},"type":{"type":"string","enum":["HACKATHON","CASE STUDY","CONTEST","CODING","INNOVATION","DESIGN","COMPETITION","QUIZ"],"description":"Normalised category from the upstream listing."},"kind":{"type":"string","enum":["competition","contest"],"description":"NOT redundant with type, despite looking it: this governs how `deadline` reads. kind=competition means deadline is when applications CLOSE. kind=contest means deadline is when it STARTS. Always check kind before displaying `deadline` to a user."},"platform":{"type":"string","description":"Display name of the source platform, e.g. \"Unstop\"."},"url":{"type":"string","format":"uri","description":"Outbound link to the organiser's own listing. Registration happens there, never on Brabble."},"shareUrl":{"type":"string","format":"uri","description":"Brabble's own share page for this listing (/s/<id>)."},"deadline":{"type":"string","format":"date-time","description":"ISO timestamp. Meaning depends on `kind` - see above. Never null: undated upstream rows are dropped rather than guessed at."},"mode":{"type":"string","enum":["ONLINE","OFFLINE","HYBRID"]},"city":{"type":"string","description":"Free text from the upstream source. Can be empty string or the literal \"See listing\" when the source has no real venue field - check for those two values before treating city as reliable, particularly for Unstop-sourced rows."},"prize":{"type":"object","required":["label","inr"],"properties":{"label":{"type":"string","description":"Exactly as the organiser wrote it, e.g. \"₹1L\" or \"$10,300\". KNOWN LIMITATION, not a bug: for a large share of listings this is literally the string \"See listing\" because the upstream source doesn't expose a real prize field. Do not treat a \"See listing\" label as a parsing failure on Brabble's side."},"inr":{"type":["number","null"],"description":"Best-effort parse of `label` into rupees. null whenever the label can't be read as an amount - including the \"See listing\" case above."}}},"team":{"type":"string","description":"Display string for team size, e.g. \"1-4\" or \"Individual\"."},"fee":{"type":"string","description":"Display string, e.g. \"Free\" or \"₹500\". Use the `free=true` query param rather than string-matching this yourself."},"eligibility":{"type":"array","items":{"type":"string"},"description":"Raw eligibility statements as the organiser wrote them. `hub=school-students`/`college-students` are computed by regex over this field."},"registered":{"type":["integer","null"],"description":"Registrant count if the source publishes one, else null. Not comparable across platforms."}}},"ListingsResponse":{"type":"object","required":["refreshedAt","origin","total","count","offset","limit","listings","attribution","docs"],"properties":{"refreshedAt":{"type":"string","format":"date-time","description":"When the underlying feed snapshot was last read - NOT when you called the API. Use this, not wall-clock time, to judge staleness. The snapshot refreshes hourly (cache-control max-age=3600), so treat data older than a few hours as suspect and anything past ~24h as stale; there is no separate longer SLA."},"origin":{"type":"string","enum":["store","live"],"description":"\"store\" = served from last-known-good database rows. \"live\" = read from upstream on demand (fallback path)."},"total":{"type":"integer","description":"Total matching rows across the whole filtered set, unpaged. Page through with offset/limit until count reaches total."},"count":{"type":"integer","description":"Number of rows in this page (listings.length)."},"offset":{"type":"integer"},"limit":{"type":"integer"},"listings":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}},"attribution":{"type":"string","description":"License text. Constant string, repeated on every response by design."},"docs":{"type":"string","format":"uri"}}}}},"x-versioning":{"note":"There is currently no /api/v1/ prefix - /api/listings is the only version that has ever existed. There is no published deprecation/changelog process today. The auth requirement documented here was added to a previously-unauthenticated endpoint without a version bump or advance notice; treat that as the operating precedent, not an exception, until a versioning policy is published. Pin to response fields you actually use and code defensively against new fields appearing, since additions won't be flagged in advance either."}}