For developers

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.

Private beta · Base URL https://api.wandkit.app · OpenAPI 3.0.3 spec served at /api/openapi.yaml

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.

wk_

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.

Issued per app under Apps & environments, or with wandkit apikeys create --app
wks_

Server 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.

Issued per environment under Apps & environments
wkp_

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.

Created in the dashboard

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.

Read scope

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.

Tokens always expire

The choices are 30, 90 or 365 days. There is no non-expiring option.

Revocation is itself token-allowed

for write-scope tokens, on purpose, so cleaning up a leaked token can be scripted rather than clicked.

Optionally organization-scoped

, restricting a token to a single organization you belong to.

Terminal
# 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.

forms

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.

responses

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.

events

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.

issues · campaigns

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.

Terminal
# 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.

Terminal
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.

POST/api/v1/push/send

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.

Terminal
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"}'
POST/api/v1/referrals/conversions

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.

Terminal
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"}'
GET/api/v1/access-codes/{code}

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.

Terminal
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.

Spec

/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.

Errors

error, code, message

Branch on code, a stable snake_case string. Never branch on message, which is localized and free to change.

Pagination

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.

No backend event ingest

Events come from the SDK in your app. There is no public endpoint for posting events from your servers.

No analytics endpoints today

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.

No generic outbound webhooks

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.

No published rate limit and no SLA

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.

No live and test key prefixes

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.

Error messages ship in English and Croatian

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.