{
  "ok": true,
  "name": "Spotka API",
  "base_url": "https://spotka.co/api/v1",
  "how_to_call": "Send your key and a `do` parameter. Any of these work: POST form fields, POST JSON, or a GET query string. The key can go in the `key` parameter or an X-API-Key header (the header is preferred — a key in a URL can leak into logs).",
  "get_a_key": "Spotka app → Settings → API access → Create key.",
  "example": "curl -X POST -H 'X-API-Key: spk_XXXXXXXXXXXX' -d 'do=ideas' https://spotka.co/api/v1  (or pass key=spk_… as a parameter if your client can only send a URL)",
  "verbs": [
    {
      "do": "ideas",
      "what": "Ideas picked for you.",
      "params": {
        "limit": "optional, 1-50, default 20"
      }
    },
    {
      "do": "events",
      "what": "Events other members are hosting that suit you. The API prefers YOUR ranked suggestions; when that deck is empty it falls back to the broader By Others browse pool the app uses. Filters narrow whichever pool was loaded, and the reply reports deck_size. When nothing comes back, `empty.reason` says which of still_ranking / no_inventory / filters_too_narrow / end_of_list it is, so you know whether to wait, widen, or stop. A cold first call may also return ranking_pending=true with retry_after_ms — wait that long and ask again rather than reporting an empty deck.",
      "params": {
        "limit": "optional, 1-50, default 20",
        "query": "optional, matches title, description, pitch, host and place",
        "location": "optional, matches the place",
        "starts_after": "optional ISO date — events with no time are excluded",
        "starts_before": "optional ISO date",
        "availability": "optional, open (hide full events) or any",
        "page": "optional, 1-based page number; cursor takes precedence when both are present",
        "cursor": "optional, the next_cursor from the previous reply"
      }
    },
    {
      "do": "my_events",
      "what": "Events you host, events you joined, and invitations waiting for you.",
      "params": {
        "limit": "optional, 1-50, default 20"
      }
    },
    {
      "do": "create_event",
      "what": "Add an event. when= accepts a weekday phrase such as 'next Saturday at 6pm' or an ISO instant such as 2026-08-22T18:00:00+02:00. If a supplied when= cannot be read, or publish=true cannot complete, the call fails (ok:false) with the unmet fields rather than leaving a silent draft.",
      "params": {
        "title": "required",
        "when": "optional weekday phrase, e.g. 'next Friday at 7pm', or an ISO 8601 instant. Unreadable when= fails.",
        "where": "optional plain text",
        "about": "optional description",
        "max_guests": "optional, 2-8",
        "visibility": "optional, public or private",
        "publish": "optional, true (default) or false",
        "raw": "test accounts only (a standard key belonging to one is enough) — skip the writing model and set the fields exactly as given",
        "starts_at": "test accounts only, with raw — an ISO 8601 start time"
      }
    },
    {
      "do": "update_event",
      "what": "Change an event you host. A partial update: anything you leave out stays exactly as it is. Nobody is notified — guests see the change next time they open it. Pass publish=true to publish a draft once when and where are set.",
      "params": {
        "event": "required, the event id",
        "title": "optional",
        "when": "optional plain text, e.g. 'Saturday 8pm', or an ISO 8601 instant to reschedule a published event",
        "where": "optional plain text",
        "about": "optional description",
        "max_guests": "optional, 2-8",
        "visibility": "optional, public or private",
        "publish": "optional, true publishes a draft (default false). Already published is a no-op",
        "expected_version": "optional — send the version you read and the call fails rather than overwrite a newer change"
      }
    },
    {
      "do": "invite",
      "what": "Invite people to an event. Give `event`, or give `title` instead to create the event and invite in one call.",
      "params": {
        "event": "event id, or omit and pass create_event params",
        "to": "one or more email addresses, comma separated"
      }
    },
    {
      "do": "join",
      "what": "Take a seat at somebody else's event. The reply's `via` says which path was used: 'deck' when the event was in that member's suggestions, or 'share_link' for a test key when it was not (with `deck_size` so you can see why).",
      "params": {
        "event": "event id",
        "note": "optional message to the host"
      }
    },
    {
      "do": "leave",
      "what": "Give up a seat you took.",
      "params": {
        "event": "event id"
      }
    },
    {
      "do": "respond",
      "what": "Answer something addressed to you: accept or decline an invitation, or (as host) approve a guest's pending time proposal. Host approval is only for proposed_time — an interested guest already holds a seat; do not approve them. Check do=event allowed_actions first.",
      "params": {
        "event": "event id",
        "decision": "accept / decline / interested — or approve / decline when answering as the host",
        "guest": "as host only: the guest's user id"
      }
    },
    {
      "do": "event",
      "what": "One event in full. For hosts: lifecycle (published / pending_proposal / calendared), held_guest_count, calendared_guest_count, guest_seats_remaining, guest_states (interested_with_held_seat / proposed_time / calendared), calendared_attendees (same meaning as confirmed — only calendared encounter guests), and allowed_actions. Empty confirmed/calendared_attendees does not mean the guest is unapproved.",
      "params": {
        "event": "event id"
      }
    },
    {
      "do": "archive",
      "what": "Retire an event you host.",
      "params": {
        "event": "event id"
      }
    },
    {
      "do": "delete",
      "what": "Delete an unpublished draft you host. A published event must be archived instead.",
      "params": {
        "event": "event id"
      }
    },
    {
      "do": "profile",
      "what": "This member's own profile, split into `public` (what other members see) and `private` (their own context — dietary needs, interests, what they are focused on). Use the private half to choose what to suggest; never quote it into an invitation or a message.",
      "params": {}
    },
    {
      "do": "update_profile",
      "what": "Change this member's own profile. Only the fields you name change; everything else is left alone. Anything not on the writable list is refused by name rather than ignored.",
      "params": {
        "any of": "display_name, first_name, last_name, bio, city, company, job_title, personal_url, instagram, telegram, linkedin, musical_preferences, community_goals, current_focus, interests, dietary_preferences, dietary_other, allergies, allergies_other, languages",
        "lists": "interests, dietary_preferences, allergies and languages take a list, or comma-separated text"
      }
    },
    {
      "do": "notification_settings",
      "what": "Which notifications this member gets, on both rails Spotka uses — push and email. Each one says whether it is on per channel, whether that is their own choice or Spotka's default, and which apps it applies to. Also reports how many devices are registered: with none, no push is delivered however these are set, so never promise a notification without checking.",
      "params": {
        "app": "optional, spotka or sponic_gardens. Defaults to every app.",
        "channel": "optional, push or email. Defaults to both."
      }
    },
    {
      "do": "update_notification_settings",
      "what": "Turn notifications on or off. Only the ones you name change. Naming a notification changes every channel it travels on, so \"turn off the weekly ideas\" stops both the push and the email; add channel=email to change one rail only. Ask the member first: a notification turned off is not noticed until something has already been missed.",
      "params": {
        "any notification key": "on, off, or default — for example new_message=off. `default` is a VALUE, not a parameter: it clears the member's choice so the setting follows whatever Spotka ships with, which is the only way back once an explicit value is stored.",
        "all": "optional, on / off / default, applied to everything in scope. channel=email with all=off is 'stop emailing me'. It skips anything already set that way rather than pinning it out of Spotka's defaults.",
        "type": "optional, one key. enabled=on|off|default must come with it.",
        "app": "optional, spotka or sponic_gardens. Defaults to every app.",
        "channel": "optional, push or email. Defaults to both."
      }
    },
    {
      "do": "calendar",
      "what": "What this member has coming up — events they host and events they joined, soonest first, with exact times. Events with no agreed time are returned separately in `undated`, never mixed into the schedule.",
      "params": {
        "limit": "optional, 1-50, default 20",
        "from": "optional ISO date. Defaults to now — an agenda is about what is ahead.",
        "to": "optional ISO date"
      }
    },
    {
      "do": "activity_history",
      "what": "What this API has changed on this account, newest first: which verb, what it touched, and whether it worked. A record of what happened, NOT an undo log — reverse things with the opposite verb.",
      "params": {
        "limit": "optional, 1-50, default 20",
        "verb": "optional, narrow to one action such as create_event",
        "since": "optional ISO date"
      }
    },
    {
      "do": "revoke_invite",
      "what": "Take back an invitation you sent. The link stops accepting a reply. Email that already went out cannot be unsent.",
      "params": {
        "invite": "required, the invitation id"
      }
    },
    {
      "do": "inbox",
      "what": "What is waiting: conversations with unread messages, and invitations waiting for an answer. Reading it marks nothing read.",
      "params": {
        "limit": "optional, 1-50, default 20"
      }
    },
    {
      "do": "conversation",
      "what": "One conversation's messages, newest first. Does NOT mark anything read.",
      "params": {
        "conversation": "required, the id from do=inbox",
        "limit": "optional, 1-50, default 30",
        "before": "optional, the `at` of the oldest message you have, to page back"
      }
    },
    {
      "do": "mark_read",
      "what": "Mark a conversation read. Only do this when the member has actually seen it.",
      "params": {
        "conversation": "required"
      }
    },
    {
      "do": "send_message",
      "what": "Send a message as the member. Needs a key with messaging enabled — it is off by default because it writes to other people in their name. Only replies in a conversation they already have, or writes to Fern.",
      "params": {
        "body": "required, the message",
        "conversation": "the id from do=inbox — or",
        "to": "fern"
      }
    },
    {
      "do": "report_bug",
      "what": "Tell the Spotka team something is broken. Use it when something you tried did not work, or the member says something is wrong — you saw the call and the reply, so you are the best-placed reporter. It goes into the same queue as a report typed in the app. Default is no auto-fix. autofix=true on a clear bug sends it to a coder. check_only=true on a test-reporter key runs duplicate detection and returns the verdict without filing. Pass product_url, app_version, platform, and a screenshot when you have them. Poll do=report_status; when waiting_for_retest is true, re-test on production and call do=verify_bug. Tell the member it has been passed on, never that it is already fixed.",
      "params": {
        "report": "required — what the member did, what happened, what they expected instead",
        "title": "optional, a short heading for the report",
        "screen": "optional, where in Spotka this was",
        "autofix": "optional, true/1/yes/on. Bugs only. A clear bug goes to a coder. The reply still has being_worked_on: false at file time — never tell the member it is already fixed.",
        "check_only": "optional, true/1/yes/on. Test-reporter keys only. Runs duplicate detection and returns the verdict without filing a task.",
        "product_url": "optional, the https://spotka.co/app/… page where it broke",
        "app_version": "optional, e.g. 0.6.375",
        "app_build": "optional",
        "source_commit": "optional",
        "platform": "optional, web / ios / android",
        "screenshot": "optional, one PNG/JPEG/WebP as a data URL or base64, up to 1 MiB. Oversized images are dropped; the report still files",
        "screenshots": "optional, extra screenshots, same rules as screenshot, up to 5 in total"
      }
    },
    {
      "do": "report_feature",
      "what": "Ask the Spotka team for something Spotka does not do yet. Same queue, same rule: a person reads it and decides. Tell the member the request was passed on, never that it will be built.",
      "params": {
        "report": "required — what the member wants, and what they are trying to achieve with it",
        "title": "optional, a short heading for the request",
        "screen": "optional, where in Spotka this was",
        "product_url": "optional, the https://spotka.co/app/… page where this came up",
        "app_version": "optional",
        "app_build": "optional",
        "source_commit": "optional",
        "platform": "optional",
        "screenshot": "optional, one PNG/JPEG/WebP as a data URL or base64, up to 1 MiB",
        "screenshots": "optional, extra screenshots, same rules as screenshot, up to 5 in total"
      }
    },
    {
      "do": "report_status",
      "what": "Poll a report this account filed. Returns status, gate, whether a fix has landed, and whether a re-test is waiting. Only reports this key's member filed are visible, except a QA fleet agent key (is_test + test_account_kind=agent) may open a report that is waiting for a re-test. Omit report to list recent ones. Pass waiting=true on a fleet agent key to list every waiting report, oldest first.",
      "params": {
        "report": "optional — task number or id from report_bug. Also accepted as number= or id=. Omit to list up to 20 recent reports this account filed.",
        "waiting": "optional, true/1/yes/on. Fleet agent keys only. Omit report and set this to list every capture report waiting for a re-test (up to 50, oldest first). Ignored on any other key."
      }
    },
    {
      "do": "verify_bug",
      "what": "Submit the retest verdict on a report this account filed, without the email token. Only when waiting_for_retest is true. A QA fleet agent key may also verify any other account's report that is waiting. still_broken reopens the report; unverified means the re-test could not run and leaves the report waiting.",
      "params": {
        "report": "required — task number or id. Also accepted as number= or id=",
        "verdict": "required — works, still_broken, or unverified when the re-test could not run",
        "note": "optional, up to 4000 characters"
      }
    },
    {
      "do": "capabilities",
      "what": "What your key may do, before you try it: which verbs are available to this key, what each one changes in the world (writes_data, publishes_publicly, sends_email, notifies_members, confirmation_recommended), and how much of each budget is left. Reading it does not spend your write budget.",
      "params": {}
    }
  ],
  "testing": {
    "what": "A key marked as a test key can act as any test account, write without the model, and undo what it made. Ask an admin for one.",
    "as": "Add as=<name or email> to any call to run it as that test account. Test accounts only — a test key can never act as a real member.",
    "extra_verbs": {
      "actors": "List the test accounts this key may act as.",
      "seed": "Top a test account up to the number of published events the app requires before a member may join anyone else's. Reports deck_size so you know whether the deck path is available.",
      "reset": "Archive every event this key created.",
      "key_list": "List keys for a test account named with for= (email or id). Returns prefix, label, created_at, last_used_at — never the secret.",
      "key_create": "Mint a standard key for a test account named with for=. The plaintext is returned once. A test key cannot set test_mode.",
      "key_revoke": "Revoke a key belonging to the test account named with for=. Send id= from key_list."
    },
    "create_event_raw": "Add raw=true (and starts_at=<ISO time>) on a test account to write the fields directly instead of through the model — a standard key belonging to that account is enough, not only an admin test key. Sub-second, and the copy is exactly what you asked for.",
    "invites": "A test key can only invite other test accounts, so a suite never sends real email.",
    "example": "do=create_event&as=host@example&raw=true&title=T1&starts_at=2026-09-10T17:00:00Z  ->  do=join&as=guest@example&event=<id>  ->  do=event&as=host@example&event=<id>"
  },
  "limits": {
    "requests": "60 per minute, 1000 per day",
    "writes": "20 per hour, 60 per day",
    "invites": "25 people per day"
  },
  "errors": "Every failure returns { ok: false, error, message, hint }.",
  "mcp": {
    "url": "https://spotka.co/api/v1/mcp",
    "what": "Every verb above, offered as MCP tools, for clients that speak MCP natively (Claude, Cursor, Windsurf).",
    "connect": "claude mcp add --transport http spotka https://spotka.co/api/v1/mcp --header \"X-API-Key: spk_XXXXXXXXXXXX\""
  }
}