API and CLI
The Marvey developer surface is three things: an iOS SDK inside your app, a command line tool for your repo and your CI, and one HTTP API that both the CLI and the dashboard speak. This page describes what is actually served today.
Three credentials, three jobs
Marvey issues three kinds of key and they are never interchangeable. A server key sent to an SDK endpoint is rejected, and an SDK key sent to a server endpoint is rejected. There is no live and test split: staging and production are environments of one project, and each has its own keys. Keys are named, revocable, and the plaintext is shown once at creation and never again.
SDK key
Embedded in your iOS app and used by the SDK. Sent as X-API-Key. It is the only key that belongs in a shipped binary. It belongs to one app in one environment. Do not call SDK endpoints from your own backend with it.
wandkit apikeys create --appServer key
For your backend. Sent as X-API-Key. It belongs to one environment. Today it authenticates five endpoints, all listed further down this page. That is the whole server-to-server surface, and we would rather say so than imply more.
Personal access token
For you, the CLI, scripts and agents. Sent as Authorization: Bearer. Scoped read or write, expires in 30, 90 or 365 days, revocable at any time.
Personal access tokens
A token reaches the same API the dashboard uses, minus the operations that should stay tied to a signed-in session.
The spec classifies every operation that accepts a Bearer credential, and a test fails the build if a new endpoint arrives unclassified. Of the 171 such operations, 149 accept a personal access token and 22 are session-only. The session-only set is the account-critical work: creating further tokens, editing or deleting your account, deleting an organization, changing organization membership, installing the Slack app, and storing integration secrets for APNs, RevenueCat, App Store Connect and Google Play. A token cannot mint another token.
is enforced by method. A read token may issue GET, HEAD and OPTIONS, and gets a 403 with the code token_read_only on anything else.
The choices are 30, 90 or 365 days. There is no non-expiring option.
for write-scope tokens, on purpose, so cleaning up a leaked token can be scripted rather than clicked.
, restricting a token to a single organization you belong to.
# read the last 50 checkout_completed events in production curl -H "Authorization: Bearer $WANDKIT_TOKEN" \ "https://api.wandkit.app/api/v1/projects/$PROJECT/events?event_name=checkout_completed&environment=production&limit=50"
The CLI
The wandkit command, for humans, scripts and agents. Authenticates with a personal access token from WANDKIT_TOKEN or a stored login.
Feedback forms as code
Forms live in your repo as forms/<key>.json, one file per form. The key is the only identity, so files carry no id or version. A form is one definition for the project, live in the environments you choose: push it live in staging, then publish it to production. pull, push, diff, validate, list, publish, unpublish.
Response export
list for one page, export for all of them, auto-paginated, filtered with --since, --until and --env. NDJSON by default, one object per line, so it pipes straight into whatever you already use. --format json if you want an array.
Integration check
list and tail. This is the tool for answering "is my SDK actually sending anything", and for discovering the event names you will trigger surveys on. It is a raw log today, not analytics.
Day to day work
Issues: list, create, update, close. Campaigns: list, get, create, update, delete, metrics. Plus wiki, projects, environments, apps, apikeys for SDK and server keys, auth and config.
# authenticate once, then bind this directory to a project wandkit auth login --with-token < token.txt wandkit projects use <project-id> # writes .wandkit.yml wandkit projects use --env staging # default environment for this repo # forms as code: edit locally, try it in staging, ship it wandkit forms pull # every form to forms/<key>.json wandkit forms diff forms/nps.json wandkit forms push forms/nps.json --env staging wandkit forms publish nps --env production # responses out as NDJSON wandkit responses export --form <form-id> --since 30d > responses.ndjson # watch events land while you wire up the SDK wandkit events tail
The escape hatch
No workflow should be blocked on a missing wrapper command, so the CLI carries a raw authenticated request. Paths may omit the /api/v1 prefix. --paginate follows the standard envelope and merges every page; --input takes a file path or - for stdin.
wandkit api GET /me/tokens wandkit api GET /projects/<id>/issues --paginate wandkit api POST /projects/<id>/issues --input issue.json
.wandkit.yml is committed alongside your code and pins the project, its default environment and the forms directory, so CI jobs and agents need no flags. WANDKIT_TOKEN always takes precedence over a stored login, so a CI runner never needs a credentials file on disk. The CLI is not yet published for download. Write to info@wandkit.app and we will get you a build.
Server to server
Five endpoints take a wks_ server key, and each acts in the environment that key belongs to. This is the complete list, not a selection.
Send a push to one user
POST /api/v1/push/send queues one notification for every device that user has registered. Delivery is asynchronous. Requires an APNs integration on the project. A repeated idempotency_key returns the original notification rather than queueing a second one, so retries are safe.
One caution we would rather state than bury: a device binds itself to a user id using the SDK key embedded in your app, so a determined client can bind to somebody else's id. Treat notification content as readable by anyone, and never put secrets or account data in it.
curl -X POST https://api.wandkit.app/api/v1/push/send \ -H "X-API-Key: wks_..." \ -H "Content-Type: application/json" \ -d '{"external_user_id":"user_123", "title":"Your report is live", "body":"Thanks for the bug report.", "idempotency_key":"report-live-8811"}'
Report a referral conversion
POST /api/v1/referrals/conversions. The app claims a referral first, then hands its install id to your backend, which reports the signup here. This call is deliberately yours to make: only your backend knows what a real signup is.
It always returns 200. A signup on an install that never claimed a referral is an ordinary organic signup, not an error, and comes back as converted: false with reason: no_claim. Reporting the same external_user_id twice is a no-op that returns the original outcome. When a conversion reaches the campaign's threshold, the inviter's reward is enqueued in the same transaction.
curl -X POST https://api.wandkit.app/api/v1/referrals/conversions \ -H "X-API-Key: wks_..." \ -H "Content-Type: application/json" \ -d '{"install_id":"...","external_user_id":"user_123"}'
Check an invite code
New, and in beta with the rest of the invite gate. When someone signs up with an invite code, your backend looks it up here: its status, how many times it has been claimed and from which installs, its expiry, and the email it was sent to, which helps when someone signs up with a different address. POST /api/v1/access-codes creates a code, generated or your own, and can email it. POST /api/v1/access-codes/{code}/revoke revokes one, and revoking it twice is harmless.
A code can be claimed any number of times. Marvey counts the claims and never refuses a code for having been used, so if one code should admit one person, that check belongs in your backend.
curl https://api.wandkit.app/api/v1/access-codes/K7QM-2XFT \ -H "X-API-Key: wks_..."
Reference and conventions
The spec is the contract. These conventions hold across the API.
/api/openapi.yaml
The OpenAPI 3.0.3 document is served by the API itself, and every request under /api/v1 is validated against it at runtime, so the request contract cannot drift from the running server. Swagger UI is at /swagger.
error, code, message
Branch on code, a stable snake_case string. Never branch on message, which is localized and free to change.
items, total, limit, offset
One envelope across list endpoints. limit defaults to 20 and caps at 100. wandkit api --paginate walks it for you.
What this API does not do
Worth knowing before you plan around it. This is the surface as served today, not a statement about what is planned.
Events come from the SDK in your app. There is no public endpoint for posting events from your servers.
No aggregation, no funnels, no cohorts, no retention over HTTP. Behavioral event analytics is coming, and this section will change when it ships. Event data is a raw log plus a per-user occurrence counter, which is what drives the Nth-occurrence survey trigger. A user's profile also groups their recent events into sessions, worked out when you ask and never stored.
Notifications go to Slack, through the Slack app you install from the dashboard, with 16 individually toggleable event types. If you need events pushed somewhere else, tell us.
Magic-link sends, team invitation resends and invite-code emails have a cooldown window, but there is no general documented limit and no uptime commitment yet. Marvey is in private beta and we are not going to pretend otherwise.
There is no wk_live_ or wk_test_. Staging and production are environments of one project, each with its own end users, events, posts and keys, so the key a call carries decides where it lands. List endpoints take an environment parameter, and the CLI takes --env.
That covers API error strings only. Server-driven localization is coming soon and is not part of the API surface yet. The code field is language-independent.
Something you need that is not here? Write to info@wandkit.app and tell us what you were trying to build. During beta, that is how the API gets decided.