API

Spotka Agentic Interface Guide

Let your favorite agents connect into Spotka.

One short key. It works with ChatGPT, Grok and Claude without any code, and with plain curl if you prefer.

Just want to wire it into Claude, Cursor or another MCP app? That is a shorter page: spotka.co/mcp.

Start here

This works without a key and tells you everything the API can do right now:

curl https://spotka.co/api/v1

Point an assistant at that URL and it can work the rest out on its own.

Get your key

Open Spotka on the web, Android or iPhone and go to Settings → API access → Create a key, or jump straight there:

Open Spotka settings

You get a short key like spk_4KAS22M1JQFQ, and a Copy for your assistant button. That button copies a ready-made block of text. Paste it into ChatGPT, Grok or Claude and it explains the whole API to them. You do not have to.

The key is shown once. If you lose it, make a new one. It takes one tap, and you can revoke the old one on the same screen.

A key belongs to one person: yours. It can do only what you can do in the app yourself.

What it can do

do=What it doesOptions
ideasThings to do in the city, picked for youlimit
eventsEvents other members are hosting that suit you. Text search can find public events beyond your current suggestions; other filters narrow the loaded poollimit, query, search (alias of query), location, starts_after, starts_before, date_from (alias of starts_after), date_to (alias of starts_before), availability, page, cursor
my_eventsEvents you host, events you joined, invitations waiting for youlimit
create_eventAdd an event. A title-only create stays a draft: publishing needs a time and a place, the same as update_event. Leaving when and starts_at out leaves the draft with no time rather than inventing a starttitle (needed, up to 120 characters; longer is rejected), when, starts_at (ISO start), ends_at (ISO end), where, about (up to 1000 characters; longer is rejected; angle brackets are preserved, not stripped), max_guests (2–8; out of range is rejected; capacity is the same field, the name every event read returns), visibility, publish
update_eventChange an event you host. The event must be the full event id; a truncated id is invalid_event_id. Anything you leave out stays as it is, and nobody is notified. Pass publish=true to publish a draft. If the supplied time cannot be applied, or that publish cannot complete, the call fails and the other fields are not changed. On a published event, where= is place_not_applied (the place is fixed while it is live), not invitation_not_draftevent (needed; full id), title (up to 120 characters; longer is rejected), when, starts_at (ISO start), ends_at (ISO end), where, about (up to 1000 characters; longer is rejected; angle brackets are preserved, not stripped), max_guests (2–8; out of range is rejected), visibility, publish, expected_version
inviteInvite people to an event. A person is listed in invited only when a pending invitation they can see and answer was created; otherwise the call names why. A truncated event id is invalid_event_id, and one that does not exist is event_not_found. Neither sends anything, and neither uses any of your daily recipient allowance. When you create the event in the same call, the allowance is taken just before the event is made, so a call that runs out of allowance never leaves a published event behindevent and to, or the create_event options instead, to make the event and invite in one go
joinTake a seat at someone else's event, at the time the host set. An event with no time yet is time_not_setevent, note
leaveGive up a seat you took. A host cannot leave their own event (cannot_leave_own_event). Archive or delete it instead. A missing event is event_not_found, not candidate_stale. The rest of the member's event suggestions stayevent
respondAccept, decline, or interested. Accept takes a seat; interested is a soft RSVP that records the person on the host's interested list without taking a seat. Changing interested to accept takes a seat; it is not already_responded. Accepting an event with no time yet is time_not_set. A never-answered invite on an archived event is event_archived (the host retired it), not already_responded. Repeating the same decision is a no-op success, not already_responded. An id that is not an event is event_not_foundevent, decision
eventOne event in full: lifecycle, held seats vs calendared guests, guest states, seats remaining, allowed actions. The event must be the full event id; a truncated id is invalid_event_idevent (full id)
archiveRetire an event you host. The event must be the full event id; a truncated id is invalid_event_id. An event you cannot see is event_not_visible, not invitation_not_foundevent (full id)
deleteDelete an unpublished draft. The event must be the full event id; a truncated id is invalid_event_id. Already gone is event_not_found. An event you cannot see is event_not_visible, not invitation_not_foundevent (full id)
profileYour own profile, split into what other members see and your own private contextnone
update_profileChange your own profile. Only the fields you name change. display_name and first_name cannot be blank. Omit them to keep the current name. Names refuse line breaks and control characters. Overlong text is rejected, not shortened. Over-long lists are rejected, not cut short: interests up to 40, dietary_preferences and allergies 20, languages 12. Each interests / dietary_preferences / allergies item is up to 120 characters; longer is rejected, not sliced. languages must be on the app list (english, polish, ukrainian, belarusian, russian, german, french, spanish, italian); invented words and JSON junk are invalid_languages, not stored. personal_url must be a public-page slug (invalid_personal_url). instagram, linkedin and telegram must be a handle or profile URL (invalid_social_handle); obvious non-URLs and non-handles are refused, not stored. HTML tags on bio, city, company, job_title, community_goals, current_focus, allergies, linkedin and personal_url are stripped on save, the same way event titles do, rather than stored raw. List fields accept a list, a JSON array string, or comma-separated text; invalid JSON starting with [ is refused, not stored as garbled itemsbio (up to 2000 characters; HTML stripped on save), city, company (HTML stripped on save), job_title (HTML stripped on save), interests (up to 40), dietary_preferences, allergies, languages (up to 12, from the app list), personal_url, instagram, linkedin, telegram, current_focus (HTML stripped on save) and more
notification_settingsWhich notifications you get by push and by email, whether each is your own choice or the default, and how many devices are registered to receive themapp, channel
update_notification_settingsTurn notifications on or off. Only the ones you name change, on every rail they travel on unless you say channel. default puts one back to what Spotka ships withany notification key, plus all, type, enabled, channel, app
calendarWhat you have coming up: hosted and joined, soonest first. Events with no agreed time are kept separatelimit, from, to
activity_historyWhat this API changed on your account, including keys you minted or revoked. A record, not an undo log. succeeded=false lists only failed actions; an invalid value is an error, not the unfiltered list. verb narrows to one action; only actions that change something are recorded, so a read verb is refused and the error lists what you may filter bylimit, verb, since, succeeded
revoke_inviteTake back an invitation you sent. invite= from do=event is enough on its own. Email already sent cannot be unsentinvite
inboxConversations with unread messages, and invitations waiting for an answer. unread_total counts every unread conversation, not just this page. Marks nothing readlimit
conversationOne conversation, oldest first. Does not mark it read. oldest_at is the cursor for before=; there is no since=conversation, limit, before
mark_readMark a conversation read, on purposeconversation
send_messageSend a message as you. Off unless your key has messaging enabledbody, and conversation or to=fern
report_bugTell the Spotka team something is broken. Default is no auto-fix. Send autofix=true on a clear bug to send it to a coder. Duplicates fold onto the open report they match, so the same bug is not filed twice; send check_only=true to get that verdict without filing. Pass product_url, app_version, platform, and a screenshot when you have them. If you are an AI agent, send agent with your own name: the account is a shared seat and does not say who wrote the report. Poll report_status; when waiting_for_retest is true, re-test and call verify_bugreport (needed), title, screen, autofix, check_only, agent, product_url, app_version, app_build, source_commit, platform, screenshot, screenshots
report_featureAsk for something Spotka does not do yet. A person reads it. Nothing is built automaticallyreport (needed), title, screen, agent, product_url, app_version, app_build, source_commit, platform, screenshot, screenshots
report_statusPoll a report this account filed: whether a fix has landed, and whether a re-test is waiting. Omit report to list recent ones. A fleet agent may pass waiting=true to list actionable reports waiting for a re-test; each item carries verify_surface (the lane that owns it). Pass surface=api/web/ios/android/site/intranet/person alongside waiting=true to fetch only that lane's queue. Each item also carries verify_tier, the tester that owns it now; pass tier to fetch only that tester's workreport (number or id; optional), waiting (fleet agents only), surface and tier (fleet agents only, use with waiting=true)
verify_bugUse probe=true with note and dataset_version to save an own-report observation at any time or contest a closure: still_broken reopens it; works outside the queue saves evidence without verifying a fix. Normally, submit the retest verdict on a report this account filed, without the email token. Only when waiting for a re-test. A fleet agent may also verify any waiting report. A tester in the verification cascade sends tier: an unverified answer then passes the report to the next tester, and after the last one it waits for a personreport (needed), verdict (works, still_broken or unverified), note, tier, reason, probe, dataset_version
test_sessionGet a sign-in session (access and refresh token) for a QA agent test account, so a device or browser tester can open the app as it. Only a test key (any agent test account) or a QA fleet agent key (its own account). Every other key and every other account is refused. Rate limited and loggedfor (email or id; needed on a test key)
capabilitiesWhat your key may do, what each verb changes in the world, and how much budget is leftnone
Times and places are ordinary words, an ISO instant, or a dated zone. Write when=next Friday at 7pm, 2026-08-22T18:00:00+02:00, 2026-09-26 18:00 (18:00 Europe/Warsaw, the same as typing 6pm), or 2026-09-10 12:00 Europe/Warsaw. An unreadable when= fails rather than guessing a different date, and so does a bare number (a Unix timestamp is not stored as a date), a date that is not a real day (29 February 2027 is not stored as 1 March) or a UTC offset no zone uses (+15:00 is not stored as 15 hours earlier). Spotka works it out. An idea from do=ideas carries when_time_known: when it is false the when value gives only the day: the organiser has not announced a start time, so do not present its 00:00 as one.

How to call it

Four ways, all the same underneath. Use whichever your tool finds easiest.

A form POST: the one an assistant will use

curl -X POST https://spotka.co/api/v1 \
  -H 'X-API-Key: spk_XXXXXXXXXXXX' \
  -d 'do=my_events'

JSON

curl -X POST https://spotka.co/api/v1 \
  -H 'X-API-Key: spk_XXXXXXXXXXXX' \
  -H 'Content-Type: application/json' \
  -d '{"do":"ideas"}'

A plain link (for tools that can only send a URL)

curl 'https://spotka.co/api/v1?key=spk_XXXXXXXXXXXX&do=ideas'

Ordinary REST paths

curl -H 'X-API-Key: spk_XXXXXXXXXXXX' https://spotka.co/api/v1/events/mine
curl -X POST -H 'X-API-Key: spk_XXXXXXXXXXXX' \
  -d '[email protected]' https://spotka.co/api/v1/events/<id>/invite

A header (X-API-Key or Authorization: Bearer) is the preferred way to send your key. A key in a link or body field can end up in server logs, so treat any such URL like a password. The key parameter still works for tools that can only send a URL.

Making an event

curl -X POST https://spotka.co/api/v1 \
  -H 'X-API-Key: spk_XXXXXXXXXXXX' \
  -d 'do=create_event' \
  -d 'title=Sunday board games' \
  -d 'when=Sunday at 6pm' \
  -d 'where=Kawiarnia Relaks'

The reply tells you the time that was actually saved. If the time you wrote was not understood, or publish=true could not complete, the call fails (ok: false) and names the unmet fields. A title-only create stays a draft: publishing needs a time and a place, the same as update_event publish=true. Leaving when out leaves the draft with no time rather than inventing a start; it cannot be published until a time is set. It does not leave a silent published leftover that looks like success. where is a known public-place name, JSON {"name":"Plac Zamkowy","lat":52.2477,"lng":21.0143} for a map pin, or where=later to decide the place with guests.

Add publish=false to leave it as a draft. To publish that draft later, send do=update_event with the event id and publish=true. Publishing a draft that still has no time keeps it as time-not-set rather than inventing a date.

Using it with Claude, Cursor or another MCP app

Everything above is also available as MCP tools, so apps that speak MCP can use Spotka directly, with no copy-pasting of instructions.

claude mcp add --transport http spotka https://spotka.co/api/v1/mcp \
  --header "X-API-Key: spk_XXXXXXXXXXXX"

The MCP endpoint is https://spotka.co/api/v1/mcp. Same key, same limits. Setup for Claude Desktop, Cursor and the rest is on spotka.co/mcp.

Writing test suites against it

Every interesting flow in Spotka needs two people: someone hosting and someone joining. A normal key is one person, so for testing there is a second kind: a test key, which an admin can create for you.

It can act as test accounts

Add as=<name or email> to any call and it runs as that account. do=actors lists the ones you can use. These are practice accounts only. A test key can never act as a real member, and it can never send email to anyone outside them.

It can write without the AI

Normally making an event goes through the same writing help the app uses, which takes a few seconds and words things differently each time. Pass starts_at as an ISO time like 2026-10-05T17:00:00Z, with optional ends_at, and the time is saved exactly as you wrote it, straight away. Test accounts can also add raw=true to skip the writing model for the other fields.

It still goes through the app's real creation path, so the event ends up properly formed and can be found by matching. Only the writing model is skipped.

It can set an account up to pass the real rules

Spotka asks a member to publish some of their own events before they can join anyone else's. do=seed tops a test account up to that number, so the rule is met rather than worked around.

The reply includes deck_size, how many suggestions that member currently has, which tells you whether a join will go through the normal suggestions list or fall back to the share-link route.

It cleans up after itself

do=reset retires every event that key made. Run it at the end and a test leaves nothing behind.

It can mint keys for agent testers without the GUI

A test key can also mint, list and revoke standard keys for agent testers (and consim accounts) so a four-actor invite suite does not need four Chrome profiles. Name the owner with for= as an email or id, never a display name. The verbs are key_list, key_create and key_revoke. The secret from key_create is shown once; listing returns only the prefix and dates. A test key cannot set test_mode on what it mints (that stays an admin sign-in action). A standard key cannot call these at all.

# list grokbot2's keys (prefix only)
curl -X POST https://spotka.co/api/v1 \
  -d 'key=YOUR_TEST_KEY' -d 'do=key_list' -d '[email protected]'

# mint a standard key for that account; the secret is in the reply once
curl -X POST https://spotka.co/api/v1 \
  -d 'key=YOUR_TEST_KEY' -d 'do=key_create' -d '[email protected]'

# revoke one
curl -X POST https://spotka.co/api/v1 \
  -d 'key=YOUR_TEST_KEY' -d 'do=key_revoke' -d '[email protected]' \
  -d 'id=<key-id>'

A whole test, start to finish

# the host makes an event and publishes it
curl -X POST https://spotka.co/api/v1 \
  -d 'key=YOUR_TEST_KEY' -d 'as=bartek@…' -d 'do=create_event' \
  -d 'raw=true' -d 'title=Board games' -d 'starts_at=2026-09-12T17:00:00Z'

# somebody else joins
curl -X POST https://spotka.co/api/v1 \
  -d 'key=YOUR_TEST_KEY' -d 'as=kacper@…' -d 'do=join' -d 'event=<id>'
#  -> "via": "deck"        joined from their suggestions, the normal way
#  -> "via": "share_link"  matching did not suggest it, so the link was used

# the host checks it worked
curl -X POST https://spotka.co/api/v1 \
  -d 'key=YOUR_TEST_KEY' -d 'as=bartek@…' -d 'do=event' -d 'event=<id>'
#  -> "guests": 1

# tidy up
curl -X POST https://spotka.co/api/v1 -d 'key=YOUR_TEST_KEY' -d 'do=reset'

The same thing works through MCP, so you can also just ask Claude to try things and tell you what breaks.

Reporting a problem while testing

report_bug and report_feature are the same verbs a member's assistant uses. On a test account they file immediately: no confirmation, and they land in the review queue tagged as a test report. Default is no auto-fix. Send autofix=true on a clear bug to send it to a coder. Feature requests still wait for a person. The in-app Report a bug / Request a feature screens do the same thing when you are signed in as that account.

A bot cannot click the verification email. Close the loop from a spk_ key. Do not read the mailbox, and do not wait on that mail. A regular member key still only sees reports it filed. A QA fleet agent key can also re-test a report another person filed: poll report_status with waiting=true, then call verify_bug on that number after a real check. Do not wait for the original reporter.

  1. File with report_bug. Keep report.number from the reply. Pass product_url, app_version, platform, and a screenshot when you have them.
  2. Poll report_status (that number, or omit it to list recent filings). When waiting_for_retest is true, re-test the original repro on production.
  3. Call verify_bug with verdict=works if it is fixed, or still_broken to reopen the report. If you could not run the re-test at all (the screen would not open, or the thing the repro needs was not there), send verdict=unverified instead. That changes nothing and leaves the report waiting, which is the honest answer. Never guess still_broken for a repro you could not run.
# file (keep report.number from the JSON). Default is no auto-fix.
curl -X POST https://spotka.co/api/v1 \
  -H 'X-API-Key: YOUR_KEY' \
  -d 'do=report_bug' \
  -d 'title=People from events is not available yet' \
  -d 'report=Opening People from an event card shows not available yet. I expected the guest list.' \
  -d 'product_url=https://spotka.co/app/events' \
  -d 'platform=web'

# poll that filing (REPORT_NUMBER is report.number from the file reply)
curl -X POST https://spotka.co/api/v1 \
  -H 'X-API-Key: YOUR_KEY' \
  -d 'do=report_status' -d 'report=REPORT_NUMBER'

# omit report= to list this account's recent filings instead
# fleet agent keys: omit report= and pass waiting=true to list every report waiting for a re-test

# after re-testing production, when waiting_for_retest is true
curl -X POST https://spotka.co/api/v1 \
  -H 'X-API-Key: YOUR_KEY' \
  -d 'do=verify_bug' -d 'report=REPORT_NUMBER' -d 'verdict=works'

Good to know

When something goes wrong

Every failure comes back in the same shape, written to be read rather than decoded:

{
  "ok": false,
  "error": "event_required",
  "message": "Name the event to invite people to.",
  "hint": "Send event=<event id>, or title=… to create one and invite in the same call."
}

A name this action does not take is unknown_parameter: the body names it, and hint offers the right name when the guess is close (q → query on events). The call does not run.

How fast it is

Reading takes about three to five seconds. Asking for other members' events can take about seven the first time, because it waits for your matches to be worked out rather than handing back an empty list. Making an event takes about fifteen: it goes through the same writing help the app uses.

Limits

  • 60 calls a minute, 1,000 a day
  • 20 writes an hour, 60 a day. Any change you make counts: creating, updating, joining, inviting, messaging, or reporting
  • 25 people invited a day

What you have left is in the X-RateLimit-Remaining header of every reply, and X-RateLimit-Bucket names which of the budgets above it is counting. The reply always shows the one you are closest to using up, usually the per-minute one. If you need more, write to support@spotka.co.

Invitations send real email

They go out in your name, so it is worth telling your assistant to check with you before it invites anyone. The block the app copies for you already asks it to.

Hosted assistant connections

Hosted assistants can use the member-only OAuth endpoint https://spotka.co/api/v1/connector/mcp. Sign in and choose permissions when your assistant asks to connect. Disconnect through Spotka Settings → API access. Connector writes require an operation ID for safe retries.

Meta Muse and OpenAI Dots platform review is pending. The API-key connection above remains available.

Report status separates fix_merged (claimed fix) from fix_verified (successful re-probe). fix_landed is a compatibility alias for verified. API-only reporters do not need an email address to enter the retest queue. The default fleet queue excludes person-only, postponed and closed reports. Fetch waiting=true with your tier; request surface=person or tier=person explicitly to inspect work assigned to a person.