1. Authentication
The API uses Bearer tokens. Generate one for your account in Account settings → API. The token is shown only once — store it somewhere safe.
Send it in every request:
Authorization: Bearer <token>
Accept: application/json
Base URL:
https://taptime.fun/api/v1
The API is available to accounts with an active Premium plan, verified accounts and admins: only they can create tokens, and the check runs on every request — when Premium expires, existing tokens get 403 premium_required until it is renewed. Every endpoint requires a token — there is no anonymous access. Writing works only on resources owned by the token's user; other people's data is read-only and limited to public events (see Discover).
- Each token has an access level and an optional expiry, chosen when you create it: "Read only" (read) allows only GET requests, "Read and write" (read + write) allows everything; an expired token gets 401. A read-only token trying to write gets 403 with code insufficient_scope. Tokens created before this option have full access and no expiry. You can have at most 10 tokens; revoke them any time in the same settings tab. A banned account's tokens stop working (403, code banned).
- Rate limit: 60 requests per minute per token (300 per IP). Over the limit you get 429 with a Retry-After header; every response carries X-RateLimit-Limit / X-RateLimit-Remaining.
- Daily creation limit: at most 2 new events per account per day (Europe/Prague calendar day) — POST /events and POST /series (a series counts once; its generated occurrences do not) share it, as does the web app. Over the limit you get 429 with code daily_event_limit and Retry-After (seconds until midnight). Deleting an event does not give the slot back; requests that fail validation do not use one.
- Responses are always JSON (errors too), even without the Accept header — sending it is still recommended.
- Versioning: v1 only gets backwards-compatible changes (new fields, new endpoints) — clients must ignore unknown fields. Breaking changes will ship as /api/v2 and v1 will keep working for an announced transition period.
- Lists are paginated (?page=, ?per_page=1–100, default 20); the response has data, links and meta. Event ids are short hashids (the same as in web URLs); the numeric id is accepted too.
| GET | /user | Basic details of the token owner (id, name, nickname, slug, locale, timezone) plus a token object with abilities and expires_at |
2. Events
| GET | /events | My events by relation (?relation=, ?scope=; paginated) — your own by default |
| POST | /events | Create a new event |
| GET | /events/{id} | Show an event |
| PATCH | /events/{id} | Update an event |
| DELETE | /events/{id} | Delete an event |
| POST | /events/{id}/images | Upload photos (multipart, images[] field) |
| DELETE | /events/{id}/images/{imageId} | Delete a photo |
| POST | /events/{id}/images/{imageId}/primary | Set the primary photo |
- GET /events?relation= organizing (default: events you organize) | joined (you are a confirmed participant) | invited (a pending invitation) | waitlisted | requested (a join request awaiting the organizer). ?scope= upcoming (active and not over yet, soonest first) | past (the rest, newest first). Without scope you get everything for organizing/joined, and only upcoming for invited/waitlisted (an invitation to a finished event is not actionable). organizing returns the fields above; the other relations return the same shape as /discover/events (organizer, viewer.participation, can_join…) so you can accept an invitation or leave with the join endpoints. Events you were blocked from are not listed.
- Times: send an ISO 8601 datetime with an offset or Z (recommended). A value without an offset is read as Europe/Prague local time. Responses are always UTC (Z / +00:00).
- DELETE returns 409 when the event has confirmed participants — cancel it instead (PATCH status=cancelled).
- Read-only in responses: ticket_price_credits, photos_enabled, current_participants, group (hashid), is_recurring, parent_event, boosted_until, is_boosted, url.
Fields (create/update)
| Field | Type / allowed values |
|---|---|
| title | string, 3–100 chars, required |
| description | string, max 5000 (HTML is sanitized: only b, i, u, s, em, strong, ul, ol, li, p, br, a, h1–h4, blockquote) |
| type | drink | food | sport | culture | party | walk | work | game | other |
| starts_at | ISO 8601 datetime, required; on create must not be in the past |
| ends_at | ISO 8601 datetime, after starts_at (on update also checked against the stored start) |
| location_name | string, max 100 |
| location_address | string, max 200 |
| latitude / longitude | float |
| is_public | boolean |
| join_mode | open | password | tickets | request (people send a request the organizer approves; no paid tickets, no waitlist) |
| join_password | string, required if join_mode=password (ignored for other modes) |
| max_participants | integer, 2–100 |
| seeking_company | boolean — the event shows up in the "looking for company" card deck on the web. Only for a public event, not imported, join_mode open or request, without a paid ticket and with 2–10 max_participants (422 otherwise) |
| age_min / age_max | integer, 1–120 |
| gender_restriction | male | female |
| price_czk | numeric, 0–10000 |
| price_note | string, max 50 |
| ticket_url | URL, max 500 chars |
| status (update only) | active | cancelled — cancelling refunds paid credit tickets and notifies participants; a cancelled event cannot be reactivated |
Example — create an event
curl -X POST https://taptime.fun/api/v1/events \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"title": "Pivo na terase",
"type": "drink",
"starts_at": "2027-06-01T18:00:00+02:00",
"join_mode": "open"
}'
Example response
{
"data": {
"id": "eoQ1wr",
"title": "Pivo na terase",
"type": "drink",
"status": "active",
"starts_at": "2027-06-01T16:00:00+00:00",
"join_mode": "open",
"is_public": true,
"current_participants": 1,
"images": [],
"url": "https://taptime.fun/events/eoQ1wr"
}
}
Uploading photos
Multipart upload, one or more files under the images[] field (max 5 MB each, max 10 files per request). The first photo uploaded to an event with no photos becomes the primary one automatically.
curl -X POST https://taptime.fun/api/v1/events/eoQ1wr/images \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
-F "images[]=@photo1.jpg" \
-F "images[]=@photo2.jpg"
3. Recurring events
A series is a template plus a recurrence rule. Its occurrences are ordinary events (managed through /events, marked by series in the event response); the first ~8 weeks are generated on creation and a daily job adds the rest.
| GET | /series | List your own recurring events (paginated) |
| POST | /series | Create a recurring event (template + recurrence rule) |
| GET | /series/{id} | Show a series |
| PATCH | /series/{id} | Update the template — applied to all upcoming occurrences |
| POST | /series/{id}/cancel | Cancel the series: stops generation and cancels upcoming occurrences |
| GET | /series/{id}/events | Occurrences of the series (?scope=upcoming for active future ones only) |
Fields on create
| starts_at | start of the FIRST occurrence (ISO 8601; without an offset = Europe/Prague), not in the past, required |
| frequency | daily | weekly | monthly, required |
| interval | integer 1–52, default 1 (every N days/weeks/months) |
| until | YYYY-MM-DD after the first occurrence — OR |
| occurrences_limit | integer 2–104 (cannot be combined with until; neither = repeats until cancelled) |
| duration_minutes | integer 15–1440 (sets ends_at of every occurrence) |
| title, description, type, location_*, latitude, longitude, is_public, photos_enabled, join_mode, join_password, max_participants, age_*, gender_restriction, price_czk, price_note, ticket_url | template — same rules as for events |
- PATCH edits only the template (the recurrence rule cannot be changed after creation, same as on the web) and returns meta.updated_events. Times and status of occurrences and their participants are not touched.
- POST …/cancel cancels every upcoming occurrence like cancelling an event does (credit tickets refunded, participants notified) and returns meta.cancelled_events. A finite series whose occurrences are all generated is already active=false but can still be cancelled while occurrences remain in the future; otherwise 409.
- Limit: at most 5 live series per account (still generating, or with occurrences still in the future); creating another returns 409 until you cancel one. The web form has the same limit.
- Series are addressed by hashid, like events. price_czk is in CZK.
curl -X POST https://taptime.fun/api/v1/series \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"title": "Středeční běh",
"type": "sport",
"starts_at": "2027-06-02T18:00:00+02:00",
"frequency": "weekly",
"occurrences_limit": 10,
"duration_minutes": 90,
"join_mode": "open"
}'
4. Discover public events
Read-only access to other people's public events, with the same rules as the web list: only active events that have not ended yet, boosted ones first, then by start time.
| GET | /discover/events | List other people's public events (paginated, filters below) |
| GET | /discover/events/{id} | Show a public event (a private one is visible only to its organizer and participants/invitees, otherwise 404) |
Query parameters (all optional)
| Parameter | Meaning |
|---|---|
| q | text search in title, description and location (max 100 chars). Every word must match, by word prefix and ignoring diacritics ("pivo" finds "pivovar"); queries with words shorter than 3 characters fall back to a plain substring search |
| type | drink | food | sport | culture | party | walk | work | game | other |
| source | community | editorial (user-created vs. imported from external sources) |
| date_from / date_to | YYYY-MM-DD, by event start; days are Europe/Prague calendar days |
| min_participants | integer, 0–100 |
| seeking_company | boolean, 1 = only events whose organizer is looking for company (join them with POST /events/{id}/join) |
| lat + lng | search around a point (both required together); results then include distance_km |
| radius_km | 1–200, default 10, used with lat/lng |
| page, per_page | pagination, per_page 1–100 |
What the response adds compared to your own events
- organizer — name, nickname, slug, avatar_url
- source — {name, url} for imported events, null for user-created ones
- viewer — your relation to the event: is_organizer, participation (confirmed | pending | waitlisted | requested | declined | null) and can_join
- distance_km — only when lat/lng are sent
- Owner-only fields (is_public, join_password…) are never included.
curl "https://taptime.fun/api/v1/discover/events?lat=50.0755&lng=14.4378&radius_km=25&type=culture&per_page=5" \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json"
5. Joining events and managing participants
For participants (any event you can see)
| POST | /events/{id}/join | Join someone else's event (or accept an invitation); with join_mode=request it sends a request (viewer.participation = requested). Body: password / ticket_code depending on join_mode |
| DELETE | /events/{id}/join | Leave the event / decline the invitation / leave the waitlist / cancel a request (depending on your state) |
| POST | /events/{id}/waitlist | Join the waitlist of a full event |
Join and waitlist return the event (as in Discover) with your fresh viewer block; leaving returns 204.
For organizers (your own events only)
| GET | /events/{id}/participants | List participants of your event (?status=confirmed|pending|waitlisted|requested|declined|banned) |
| POST | /events/{id}/participants | Invite a user — body {"user": "<slug>"} |
| DELETE | /events/{id}/participants/{slug} | Remove a participant / cancel an invitation / remove from the waitlist / unban; declines a request (the requester stays as declined), deleting a declined request frees it; ?ban=1 blocks a confirmed one |
| POST | /events/{id}/participants/{slug}/promote | Promote from the waitlist to confirmed / approve a join request |
| PUT | /events/{id}/participants/{slug}/check-in | Check a participant in manually |
| DELETE | /events/{id}/participants/{slug}/check-in | Undo a check-in |
| POST | /events/{id}/check-in | Check-in by scanned code — body {"code": "..."} (participant QR or ticket code) |
| GET | /events/{id}/tickets | Ticket codes of an event with join_mode=tickets |
| POST | /events/{id}/tickets | Generate codes — body {"count": 1–100}, capped by max_participants and 500 per event in total |
| DELETE | /events/{id}/tickets/{code} | Delete a not yet used code |
Users are addressed by slug (the same as in profile URLs; it is in the participant list). A participant is {user:{name,nickname,slug,avatar_url}, status, checked_in_at, joined_at, …}, status = confirmed | pending | waitlisted | requested | declined | banned. Checking in a participant of a credit-priced event releases the held credits to you and cannot be reversed.
Error codes
Refusals in this section carry a machine-readable code next to the message: {"message": "...", "code": "event_full"}.
| 422 | paid_ticket_unsupported | The event has a credit-priced ticket — finish joining in the app |
| 422 | wrong_password | Missing/wrong password (join_mode=password) |
| 422 | invalid_ticket_code | Missing/invalid/used code (join_mode=tickets) |
| 409 | event_full | The event is full — try the waitlist |
| 409 | cannot_join | Joining is not possible (private, cancelled, age/gender, blocked, own event…) |
| 409 | already_participant / already_waitlisted / already_invited | Already joined / on the waitlist / user already invited |
| 409 | not_participant | You are neither joined nor invited |
| 403 | banned | You are blocked from the event |
| 409 | ticket_limit_reached / ticket_used | Ticket limit / deleting a used ticket |
| 409 | already_requested | You already sent a join request |
| 403 | request_declined | The organizer declined your join request |
| 409 | request_limit_reached | Too many pending requests (100 per event, 20 per user) |
| 409 | invite_limit_reached | The event already has 100 pending invitations |
| 422 | user_not_invitable | The user cannot be invited (search visibility turned off in their profile) |
curl -X POST https://taptime.fun/api/v1/events/eoQ1wr/join \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"password": "tajne"}'
6. Groups
Groups are addressed by hashid, users by slug. Only the founder manages a group (invites, removes members, approves requests, sets images). Group chat is not part of the API.
| GET | /groups | Your groups (?relation=member default | invited | requested) |
| GET | /groups/discover | Public groups you are not in yet (optionally ?lat=&lng=&radius_km=) |
| POST | /groups | Create a group |
| GET | /groups/{id} | Show a group (a private one is visible only to members, invitees and requesters, otherwise 404) |
| PATCH | /groups/{id} | Edit name, description, visibility and location (owner) |
| DELETE | /groups/{id} | Delete the group (owner) |
| POST | /groups/{id}/join | Accept an invitation; otherwise request to join a public group |
| DELETE | /groups/{id}/join | Leave / decline an invitation / cancel a request (depending on state) |
| GET | /groups/{id}/members | Members (?status=member — a member; invited | requested — the owner) |
| POST | /groups/{id}/members | Invite a user — body {"user": "<slug>"} (owner) |
| DELETE | /groups/{id}/members/{slug} | Remove a member / cancel an invitation / decline a request (owner) |
| POST | /groups/{id}/members/{slug}/approve | Approve a join request (owner) |
| POST | /groups/{id}/invite-link | Permanent invite link {"data": {"url": …}} (owner) |
| POST | /groups/{id}/avatar | Upload the avatar (multipart, avatar field, max 2 MB) |
| DELETE | /groups/{id}/avatar | Remove the avatar |
| POST | /groups/{id}/cover | Upload the cover image (multipart, cover field, max 5 MB) |
| DELETE | /groups/{id}/cover | Remove the cover image |
| GET | /groups/{id}/events | Group events (?scope=upcoming | past; a member) |
| PUT | /groups/{id}/events/{eventId}/interest | Add someone else's event as the group's "we're going" (a member) |
| DELETE | /groups/{id}/events/{eventId}/interest | Remove an event from the group's "we're going" (a member) |
- Create body: name (2–60), description (max 500), is_public (default false), location_name, latitude + longitude. Response viewer.role = owner | member | invited | requested | null; viewer.can_request = you can request to join.
- PATCH takes the same fields as create, all optional (latitude and longitude only together; both null clears the location); renaming also renames the group chat. DELETE removes the memberships and the "we're going" links; the group's events and the chat history are kept, they just no longer belong to a group, and the invite link stops working.
- Limits: at most 5 groups founded per account and 30 pending invitations per group. Only users who can be found in the app's search (searchable) can be invited.
- POST /events accepts group (hashid of a group you belong to): the event is then private unless you send is_public, and all confirmed group members are invited — like "Create an event with the group" on the web. The event response has group.
- GET /groups/{id}/events items are the same as in Discover plus group_link: own (created by the group) | interest (added as "we're going").
Error codes
| 409 | group_limit_reached | You have already created the maximum of 5 groups |
| 409 | invite_limit_reached | The group has 30 pending invitations |
| 422 | invalid_invitee / user_not_invitable | The owner or a user who cannot be found in search cannot be invited |
| 409 | already_related / has_pending_request / already_requested | The user is already in the group, invited, or has requested to join |
| 403 | owner_cannot_leave / cannot_remove_owner | The owner can neither leave nor be removed |
| 404 | not_requested / not_related | The user has no join request / does not belong to the group |
7. Blog posts
| GET | /posts | List your own posts (paginated) |
| POST | /posts | Create a post |
| GET | /posts/{slug} | Show a post |
| PATCH | /posts/{slug} | Update a post |
| DELETE | /posts/{slug} | Delete a post |
| POST | /posts/{slug}/cover | Upload/replace the cover image (multipart, cover field) |
| DELETE | /posts/{slug}/cover | Delete the cover image |
Posts are addressed by slug (not numeric id) in the URL, matching the public blog URLs.
Fields (create/update)
| Field | Type / allowed values |
|---|---|
| title | string, 3–150 chars, required |
| categories | array, 1–3 items, required — see below |
| event_id | event id (hashid as returned by /events, or numeric) — an event you organize or attend; response also has event = hashid |
| location | string, max 150 |
| excerpt | string, max 300 |
| body | HTML string, max 20000, required |
| status | draft | published (default draft) |
Allowed category values
Example — create a post
curl -X POST https://taptime.fun/api/v1/posts \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"title": "Jak jsme objevili Kutnou Horu",
"categories": ["mista"],
"body": "<p>Text příspěvku…</p>",
"status": "published"
}'
Cover image
curl -X POST https://taptime.fun/api/v1/posts/jak-jsme-objevili-kutnou-horu/cover \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
-F "cover=@cover.jpg"
8. Comments
| GET | /events/{id}/comments | Comments of an event (root threads with replies, paginated) |
| POST | /events/{id}/comments | Add a comment to an event |
| GET | /posts/{slug}/comments | Comments of a post |
| POST | /posts/{slug}/comments | Add a comment to a post |
| DELETE | /comments/{id} | Delete a comment (author or moderator) |
- Who can read: whoever can see the event/post (a private event only its organizer, participants and invitees, a draft only its author — otherwise 404). Who can write: to an event only the organizer and confirmed participants (403 comment_forbidden), to a published post any user.
- Body (JSON or multipart): body (text, max 2000, HTML tags are stripped), image (file, max 15 MB) — at least one of them; parent = id of the comment you reply to (must belong to the same event/post, otherwise 422 parent_invalid; a reply to a reply is moved under the thread root, threads are two levels deep).
- At most 10 comments per minute per user (shared with the web app) — over it 429 rate_limited with Retry-After. An event keeps its newest 100 threads; older ones are deleted together with their replies and images.
- Comment: id, body, image_url, author, parent, replies, viewer.can_delete, created_at. GIFs and translations are web-only.
9. Event expenses
The organizer of an event records who paid what and among whom it is split; the API computes balances and who should send whom how much (the fewest payments). The organizer and confirmed participants can read it (the split concerns everyone); only the organizer, on their own events, can add and delete expenses. Response viewer.can_manage tells which you are.
| GET | /events/{id}/expenses | Expenses + a summary per currency (total, balances, settlement) and who may take part |
| POST | /events/{id}/expenses | Add an expense |
| DELETE | /events/{id}/expenses/{expenseId} | Delete an expense |
- Create body: title (max 255), amount (0.01–999999, in currency units), currency (CZK default | EUR | USD), paid_by (slug, default the organizer), shared_with (array of slugs, default the organizer + all confirmed participants). Only the organizer and confirmed participants can pay or share, otherwise 422 participant_not_eligible. An event holds at most 50 expenses (409 expense_limit_reached).
- Currencies are never added together — every currency has its own total, balances and settlement (summary.CZK, summary.EUR…). The share of each person is amount / number of sharers.
- Each settlement {from, to, amount} carries payment {iban, spd} when the receiving user has a bank account in their profile (spd = the content of a Czech QR Payment code, render it as a QR yourself), otherwise payment is null. The organizer gets it for every settlement, a participant only for the payment they are supposed to send (other people's bank accounts are not shown to everyone).
- Expenses cannot be edited (as on the web) — delete and re-add.
10. Puls (local posts)
Puls is a short post tied to a place that disappears after 24 hours. You read what is happening around a point and can post your own.
| GET | /puls?lat=&lng= | Posts around a point, nearest first (?radius_km= 1–50, default 10; ?limit= 1–100, default 50) |
| GET | /puls/mine | Your active posts |
| POST | /puls | Add a post (multipart because of the photo/video) |
| DELETE | /puls/{id} | Delete your own post together with the uploaded files |
- Create body: latitude + longitude (required), location_name (max 100), body (max 500; only inline formatting b, i, u, s, em, strong, br is kept), and at most one of image (max 5 MB) or video (max 20 MB; the server re-encodes it and trims it to 6 s). At least text, a photo or a video is required.
- Limits: one free active post per account. The second active post costs credits in the app and cannot be added through the API (422 paid_post_unsupported). At most 3 new posts per account per day (Europe/Prague day, shared with the web app; only successfully created posts count, deleting one does not give the slot back) — over it 429 daily_puls_limit with Retry-After.
- Post: id, body, image_url, image_thumb_url, video_url, video_poster_url, latitude, longitude, location_name, distance_km (only in the feed), likes_count, author, viewer.is_mine, expires_at, created_at. Likes and reporting are not in the API yet.
- Other codes: 409 active_limit_reached (two active posts), 422 post_empty, 403 not_owner on deleting someone else's post.
11. Event photos and plan
| GET | /events/{id}/photos | The event's photo gallery (paginated) + meta: can_upload, my_photos, max_per_user |
| POST | /events/{id}/photos | Upload photos (multipart, photos[] field, 1–10 files, each an image up to 15 MB) |
| DELETE | /events/{id}/photos/{photoId} | Delete a photo (its author or the organizer) |
| GET | /events/{id}/itinerary | The event plan (programme) with its stops |
| PUT | /events/{id}/itinerary | Create or replace the plan (organizer) |
| DELETE | /events/{id}/itinerary | Delete the plan (organizer) |
- Photos are the guests' gallery, not the event's own cover images (/events/{id}/images). Everyone who can see the event can read it (a private event is 404 for strangers). Upload: the organizer and confirmed, not blocked participants, and only while the event has the gallery on (photos_enabled — set it on create/update); at most 10 photos per user per event (409 photo_limit_reached, the whole request is refused). Errors: 403 upload_forbidden | photo_forbidden, 404 photo_not_found.
- The plan is one per event: title (3–120), description (max 2000), is_public, and stops (1–50; each title max 120, notes max 500, location_name max 120, latitude + longitude together, duration_minutes 1–1440). PUT replaces the whole plan and the stops in the given order (201 when created, 200 when replaced). Anyone who can see the event can read the plan; is_public only decides whether the plan also has a public page (url).
12. Errors
| Status | Meaning |
|---|---|
| 401 | Missing or invalid token |
| 403 | The resource belongs to another user, the account is banned (code banned), neither Premium nor verified status applies (code premium_required) or the token lacks the needed access (code insufficient_scope) |
| 404 | Resource not found |
| 409 | Conflict — e.g. deleting an event that has confirmed participants, or a refused join (see the code field in section 5) |
| 422 | Validation failed — see the errors object |
| 429 | Rate limit or the daily event creation limit exceeded — wait Retry-After seconds |
Every error, whatever its status, has the same envelope: message (human readable, may be localized — do not parse it) and code (machine readable, stable — branch on this). Validation errors add errors. Generic codes: unauthenticated (401), forbidden (403), not_found (404), method_not_allowed (405), conflict (409), validation_failed (422), too_many_requests (429), server_error (5xx). Rule refusals carry a specific code, listed in the sections above (event_full, paid_ticket_unsupported, daily_event_limit, …).
422 responses look like:
{
"message": "Toto pole je povinné. (and 1 more error)",
"code": "validation_failed",
"errors": {
"title": ["Toto pole je povinné."],
"type": ["Toto pole je povinné."]
}
}
A refusal by a rule looks like:
{
"message": "Akce je plná.",
"code": "event_full"
}