API and feeds
Everything we verify is available as structured data, free and without a key. If you build something with it, we would love to hear about it.
JSON API
GET https://hackalendar.com/api/events
Returns upcoming events as { data, meta }. CORS is open and responses are cached. Use meta.nextOffset to continue while meta.hasMore is true. meta.total is the number of rows in this page, not the total catalogue size.
Dates preserve what the organiser published: startAt and endAt use YYYY-MM-DD when no hour was supplied, or an ISO timestamp for a known time. The optional temporalPrecision field describes each boundary. An unknown end is null; timezone is also null for periods with calendar dates only. Their final day is included.
Editorial picks are a smaller human selection within the catalogue. Each event includes picked and pickedAt, an ISO timestamp or null. A pick does not require a written note.
| Parameter | Meaning |
|---|---|
| city | One city slug, e.g. paris. |
| theme | Theme key: ai, web3, climate, health, data, hardware, student, creative. |
| mode | `in_person`, `online` or `hybrid`. |
| from | ISO date. Narrows the window forward; earlier than today is clamped to today. |
| to | ISO date, the far end of the window. |
| limit | Maximum rows, 1–200. Defaults to 100. |
| offset | Pagination offset, 0–100000. Defaults to 0; continue with meta.nextOffset. |
Private subscriptions and contributions
Create private access without a required account or email. Keep the management link safe, then give your integration a revocable key with the scopes it needs. API keys use Authorization: Bearer YOUR_API_KEY. Never put the key in a query string or request body. These responses are private and not cached. MCP clients can use the corresponding tools and OAuth flow.
| Route | Scope | Purpose |
|---|---|---|
| POST /api/submissions | contributions:write | Propose an event URL and optional context; returns a private receipt. |
| POST /api/feedbacks | contributions:write | Report a correction or suggestion; no direct listing changes. |
| GET /api/submissions/:id | contributions:read | Read the status of your own contribution. |
| GET /api/subscriptions | subscriptions:read | List your subscriptions. |
| POST /api/subscriptions | subscriptions:write | Save criteria and receive current matches plus a cursor. |
| GET /api/subscriptions/:id | subscriptions:read | Read one subscription and its followed events; preserve the existing inbox cursor. |
| PATCH /api/subscriptions/:id | subscriptions:write | Replace name, criteria and active state. |
| DELETE /api/subscriptions/:id | subscriptions:write | Remove a subscription. |
| GET /api/notifications?cursor=… | notifications:read | Read your notification inbox; limit 1–100, default 30. |
| GET /api/notifications/state | notifications:read | Read all followed events and a fresh inbox cursor atomically; no parameters. |
| GET /api/webhooks | webhooks:read | List your endpoints and delivery health. |
| POST /api/webhooks | webhooks:write | Register an HTTPS endpoint; signing secret returned once. |
| PATCH /api/webhooks/:id | webhooks:write | action: verify, pause, resume or rotate_secret. |
| DELETE /api/webhooks/:id | webhooks:write | Delete an endpoint and its delivery history. |
| GET /api/webhooks/:id/deliveries | webhooks:read | Inspect the latest 50 delivery attempts. |
| POST /api/webhooks/:id/deliveries | webhooks:write | Retry a failed delivery with its deliveryId. |
Submitting an event
{
"url": "https://example.org/hackathon",
"context": "Student teams are welcome.",
"idempotencyKey": "your-unique-request-001"
}Send JSON to POST /api/submissions; contactEmail is optional. A successful deposit returns HTTP 202 with id, status: received, published: false, receivedAt and statusUrl. A curator reviews the proposal before publication. You need not be its organiser.
Feedback uses message, optional eventSlug, category (correction, broken_link, suggestion, other) and evidenceUrl. Both write endpoints require an idempotencyKey of 8–120 letters, digits or . _ : -. REST also accepts the same key as Idempotency-Key. Retry identical content with the same key; conflicting content returns 409. Retrying does not duplicate a message or use another quota slot.
Notes have a 4,000-character limit, URLs 2,000 and contact emails 254; feedback needs at least 10 characters. JSON bodies are bounded to 64 KiB. Contribution admission allows five per owner and network source per hour, plus a shared 200-per-hour service limit. HTTP 429 includes Retry-After. HTTP 401/403 means missing access or scope, 400 means invalid fields, 413/415 means body size/media type, and 503 means temporary unavailability. Errors contain error and message; no private moderation details are returned.
Following events
A subscription contains name, active and criteria.rules. Each rule accepts cities (city slugs), modes and themes. Values within each facet are OR; facets are AND; rules are OR. Hybrid matches both online and in-person. Use separate rules for Paris in-person and online worldwide. Existing matches form a starting snapshot, not an initial webhook burst. The cursors returned by subscription creation, edits and single-subscription reads cover that subscription only: preserve an existing inbox cursor to keep unread changes from other subscriptions reachable.
For a first sync, call /api/notifications/state and save its events and cursor. Poll /api/notifications with that cursor and follow hasMore. Publication, important changes, cancellation, withdrawal and restoration share versioned notification IDs. History is retained for 90 days; HTTP 410 means the cursor expired. Call /api/notifications/state again, replace local state with its authoritative snapshot of all active subscriptions, then poll with its new cursor. Delivery is asynchronous. Webhook receivers must verify signatures, deduplicate notification IDs and apply event revisions monotonically. Registering an endpoint requires verification before any event delivery. The agent guide explains the tool workflow.
Webhook receivers
Register an HTTPS endpoint on port 443 with a name and URL. Keep the returned signing secret; it is shown once. Verification sends a signed webhook.challenge object containing challenge. Return HTTP 2xx with JSON containing that exact challenge to activate delivery. Redirects and private network addresses are refused.
Requests carry x-hackalendar-id, x-hackalendar-timestamp (Unix seconds) and x-hackalendar-signature. Verify the signature against the raw request body, before parsing or reserializing it:
v1=HMAC-SHA256(signingSecret, timestamp + "." + rawBody).hex()
Compare signatures in constant time and reject stale timestamps within a tolerance suitable for your clock, such as five minutes. Each delivery attempt is signed with its current timestamp; the notification ID remains stable. Respond with 2xx only after durable acceptance, then process the event asynchronously. Transient failures retry with capped backoff; persistent failures are visible in delivery history and can pause an endpoint. Rotation requires verification again.
Event descriptions in webhook event and previousEvent snapshots are capped at 1,000 characters, with descriptionTruncated when shortened. The serialized webhook body is limited to 64 KiB. An oversized notification fails that delivery without pausing the endpoint; the complete notification remains readable from the private inbox.
Feeds
- /feed.xml — every upcoming hackathon, and
/{city}/feed.xmlper city. - /calendar.ics — subscribe in Google or Apple Calendar;
/{city}/calendar.icsper city. - /llms.txt — a summary of the site for language models.
Using it
Please link back to the organiser’s registration page rather than replacing it — the whole point of this project is sending people to the events. Attribution to Hackalendar is appreciated but not required. Data is provided as-is, and we correct mistakes quickly when someone tells us about one.