Developers
API reference
The mapps.one REST API lets you create and update apps, manage store links, import store metadata, read analytics, generate QR codes and subscribe to webhooks from your own code, CI pipeline or integration. The API is available on the Studio plan.
Base URL
https://mapps.one/api/v1
All requests use HTTPS. Send request bodies as JSON with Content-Type: application/json, and always send Accept: application/json so errors are returned as JSON too.
Authentication
Create a token in the dashboard under Developers. Workspace owners and admins can do this. Give the token a name, such as "GitHub Actions", and choose its abilities. The token is shown only once, so store it in your secret manager straight away.
Tokens belong to the workspace, not to a person, so they keep working when someone leaves the team. They do not expire. Revoke a token from the same page when you no longer need it, or if it may have leaked.
Send the token as a bearer token:
curl https://mapps.one/api/v1/workspace \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json"
Abilities
| Ability | Allows |
|---|---|
apps:read | List and read apps, download QR codes |
apps:write | Create, update and delete apps, replace store links, import store metadata |
analytics:read | Read app analytics |
webhooks:manage | List, create and delete webhook subscriptions |
GET /workspace and GET /events work with any valid token. Give each integration only the abilities it needs.
Conventions
- IDs are 26-character lowercase ULIDs, such as
01j8b2k6r3xq9v7m4n5p0s1t2w. Apps are addressed byid, not by slug, so renaming a slug never breaks your integration. - Timestamps are ISO 8601 in UTC, such as
2026-09-14T08:12:44+00:00. - Responses wrap the result in a
datakey. - Store keys are
app_store,google_play,app_gallery,microsoft_storeandweb. - Platform keys are
ios,android,huawei,windows,macosandother. Destination keys are the store keys pluslanding.
Rate limits
Each workspace can make 120 requests per minute, shared by all of its tokens. Every response includes X-RateLimit-Limit and X-RateLimit-Remaining headers. When you go over the limit, the API responds with 429 Too Many Requests and a Retry-After header with the number of seconds to wait.
Errors
Errors return a JSON body with a human-readable message. Some errors also include a machine-readable code, and validation errors list problems per field in errors.
| Status | When |
|---|---|
401 | The token is missing, invalid or revoked. {"message": "Unauthenticated."} |
402 | The workspace plan does not include API access. Code plan_required. |
403 | The token does not have the ability this endpoint needs. {"message": "Invalid ability provided."} |
404 | The app or subscription does not exist, or belongs to another workspace. |
422 | Validation failed, or the request could not be completed. For example, code metadata_unavailable. |
429 | Rate limit exceeded. Wait for the number of seconds in Retry-After. |
5xx | Something went wrong on our side. Retry with exponential backoff. |
HTTP/2 402
{
"message": "API access requires the Studio plan.",
"code": "plan_required"
}
HTTP/2 422
{
"message": "This link is already taken.",
"errors": {
"slug": ["This link is already taken."]
}
}
Endpoints
| Method and path | Ability | Description |
|---|---|---|
GET /workspace | any | Workspace, plan, usage and the current token |
GET /events | any | Webhook event types |
GET /apps | apps:read | List apps |
POST /apps | apps:write | Create an app |
GET /apps/{id} | apps:read | Retrieve an app |
PATCH /apps/{id} | apps:write | Update an app |
DELETE /apps/{id} | apps:write | Delete an app |
PUT /apps/{id}/links | apps:write | Set or remove store links |
POST /apps/{id}/metadata | apps:write | Import details from the store listing |
GET /apps/{id}/analytics | analytics:read | Totals, daily series and breakdowns |
GET /apps/{id}/qr | apps:read | QR code image |
GET /webhooks | webhooks:manage | List webhook subscriptions |
POST /webhooks | webhooks:manage | Create a webhook subscription |
DELETE /webhooks/{id} | webhooks:manage | Delete a webhook subscription |
The app object
| Field | Type | Description |
|---|---|---|
id | string | ULID of the app |
name | string | App name |
slug | string | The path of the smart link: mapps.one/{slug} |
status | string | live, paused or suspended |
tagline, description | string or null | Page text |
category | string or null | A category key, such as health-fitness or productivity |
page_theme | string | clean, midnight or vivid |
accent_color | string or null | Hex colour, such as #16a34a |
developer | object | name, website and email |
social_links | object | Filled social profiles, keyed by network: x, instagram, tiktok, youtube, facebook, linkedin, threads, github, discord, reddit |
features | array | Feature items with icon, title and description |
listed_in_explore | boolean | Whether the app appears in Explore |
utm_passthrough | boolean | Whether UTM tags are forwarded to stores |
redirect_rules | object | Destination per platform, with defaults filled in |
icon_url | string or null | Public URL of the icon |
rating | object or null | value and count from the store |
urls | object | Public URLs: smart_link, landing_page, qr_svg, qr_png, privacy_policy, terms. Legal URLs return 404 until the document is published. |
links | object | Store links keyed by store, each with url, status (unverified, verified or invalid) and verified_at. Included on responses for a single app. |
screenshots | array | Screenshots with url, width and height. Included after a metadata import. |
created_at, updated_at | string | ISO 8601 timestamps |
Retrieve an app
curl https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json"
{
"data": {
"id": "01j8b2k6r3xq9v7m4n5p0s1t2w",
"name": "Habitly",
"slug": "habitly",
"status": "live",
"tagline": "Build habits that stick",
"description": "Habitly helps you build small daily habits with gentle reminders and streaks.",
"category": "health-fitness",
"page_theme": "clean",
"accent_color": "#16a34a",
"developer": {
"name": "Habitly Labs",
"website": "https://habitly.app",
"email": "hello@habitly.app"
},
"social_links": {
"instagram": "https://instagram.com/habitly.app"
},
"features": [
{ "icon": "bell", "title": "Gentle reminders", "description": "Nudges at the time you choose." }
],
"listed_in_explore": true,
"utm_passthrough": true,
"redirect_rules": {
"ios": "app_store",
"android": "google_play",
"huawei": "app_gallery",
"windows": "microsoft_store",
"macos": "landing",
"other": "landing"
},
"icon_url": "https://mapps.one/storage/apps/01j8b2k6r3xq9v7m4n5p0s1t2w/icon/icon.png",
"rating": { "value": 4.8, "count": 12450 },
"urls": {
"smart_link": "https://mapps.one/habitly",
"landing_page": "https://mapps.one/habitly/page",
"qr_svg": "https://mapps.one/habitly/qr.svg",
"qr_png": "https://mapps.one/habitly/qr.png",
"privacy_policy": "https://mapps.one/habitly/privacy",
"terms": "https://mapps.one/habitly/terms"
},
"links": {
"app_store": {
"url": "https://apps.apple.com/app/id1612345678",
"status": "verified",
"verified_at": "2026-09-14T08:12:44+00:00"
},
"google_play": {
"url": "https://play.google.com/store/apps/details?id=com.habitly.app",
"status": "verified",
"verified_at": "2026-09-14T08:12:46+00:00"
}
},
"created_at": "2026-09-01T11:20:03+00:00",
"updated_at": "2026-09-14T08:12:46+00:00"
}
}
List apps
Returns the apps in the workspace. Results are paginated: follow links.next, or pass ?page=2, until it is null.
curl "https://mapps.one/api/v1/apps?page=1" \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json"
{
"data": [
{ "id": "01j8b2k6r3xq9v7m4n5p0s1t2w", "name": "Habitly", "slug": "habitly", "status": "live" },
{ "id": "01j8d5r2w9y6a3c0e7g4j1m8p5", "name": "Habitly Focus", "slug": "habitly-focus", "status": "paused" }
],
"links": {
"first": "https://mapps.one/api/v1/apps?page=1",
"last": "https://mapps.one/api/v1/apps?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 2
}
}
App objects are shortened in this example. Each item has the full set of fields.
Create an app
| Field | Required | Description |
|---|---|---|
name | Yes | App name, up to 120 characters |
slug | No | 3 to 40 lowercase letters, numbers and single dashes. When omitted, a free slug is generated from the name. |
links | No | Store links keyed by store, in the same format as Set store links |
You can also send any of the fields listed under Update an app.
curl https://mapps.one/api/v1/apps \
-X POST \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "Habitly",
"slug": "habitly",
"links": {
"app_store": "https://apps.apple.com/us/app/habitly/id1612345678",
"google_play": "https://play.google.com/store/apps/details?id=com.habitly.app"
}
}'
The response is 201 Created with the new app object. Store links are saved in canonical form with the status unverified until the background check finishes. To fill in the icon, screenshots and description, call Import store metadata next.
Update an app
Send only the fields you want to change.
| Field | Type |
|---|---|
name | string |
slug | string. Changing it breaks the old link immediately. |
tagline, description | string or null |
category | category key or null |
page_theme | clean, midnight or vivid |
accent_color | #rrggbb or null |
utm_passthrough, listed_in_explore | boolean |
redirect_rules | object of platform key to destination key |
curl https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w \
-X PATCH \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"tagline": "Tiny habits, big changes",
"redirect_rules": { "macos": "landing" }
}'
The response is the updated app object.
Delete an app
curl https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w \
-X DELETE \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json"
The response is 204 No Content. Deleting is permanent: the smart link, landing page, legal documents, media and analytics of the app are removed.
Set store links
PUT /apps/{id}/links takes a links object keyed by store:
- a URL adds or replaces the link for that store,
nullor an empty string removes it,- stores you leave out are not changed.
Each URL must be a valid listing for its store: an apps.apple.com link with an app ID, a play.google.com link with a package ID, and so on. The web link can be any http or https address. URLs can be up to 1024 characters. Links are saved in canonical form and checked in the background.
curl https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w/links \
-X PUT \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"links": {
"app_gallery": "https://appgallery.huawei.com/app/C109876543",
"web": null
}
}'
The response is the app object, including links. An invalid link returns 422:
{
"message": "This is not a valid Google Play link.",
"errors": {
"links.google_play": ["This is not a valid Google Play link."]
}
}
Import store metadata
POST /apps/{id}/metadata reads the App Store listing, or the Google Play listing when there is no readable App Store link, and imports its details.
| Field | Description |
|---|---|
overwrite | Optional, default false. When false, only empty fields are filled, and screenshots are added only if the app has none. When true, the name, icon, text and screenshots are replaced. Rating and price are always updated. |
curl https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w/metadata \
-X POST \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{ "overwrite": true }'
The response is the app object, including links and screenshots, and an app.metadata_fetched webhook is sent. If no listing can be read:
HTTP/2 422
{
"message": "The store listing could not be read. Add a valid App Store or Google Play link first.",
"code": "metadata_unavailable"
}
Analytics
GET /apps/{id}/analytics accepts these query parameters:
| Parameter | Description |
|---|---|
range | 7d, 30d, 90d, 12m or custom. Default 30d. |
from, to | Dates as YYYY-MM-DD, both included, for range=custom. to cannot be later than today. |
timezone | An IANA time zone, such as Europe/Istanbul, used for day boundaries. Default UTC. |
curl "https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w/analytics?range=7d&timezone=Europe/Istanbul" \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json"
{
"data": {
"app": { "id": "01j8b2k6r3xq9v7m4n5p0s1t2w", "slug": "habitly" },
"range": {
"key": "7d",
"from": "2026-09-07T21:00:00+00:00",
"to": "2026-09-14T21:00:00+00:00",
"timezone": "Europe/Istanbul",
"clamped_to_plan": false
},
"summary": {
"clicks": 1284,
"visitors": 902,
"page_views": 356,
"store_clicks": 141
},
"daily": {
"2026-09-08": 152,
"2026-09-09": 171,
"2026-09-10": 168,
"2026-09-11": 205,
"2026-09-12": 214,
"2026-09-13": 190,
"2026-09-14": 184
},
"breakdowns": {
"os": { "iOS": 702, "Android": 431, "Windows": 88, "macOS": 63 },
"country": { "TR": 512, "US": 298, "": 236, "DE": 141, "GB": 97 },
"source": { "instagram": 488, "direct": 402, "qr": 190, "google": 121, "x": 83 },
"destination": { "app_store": 640, "google_play": 399, "landing": 214, "deep_link": 31 },
"utm_campaign": { "": 796, "launch": 402, "newsletter-sep": 86 }
}
}
}
fromis included andtois excluded. Both are UTC instants.clamped_to_planistruewhen the requested range was shortened to the plan's history.dailycounts clicks per local date, including days with no clicks.- Breakdowns list up to 10 values, most frequent first. An empty key
""means unknown or not set. - Metric definitions are on the Analytics page.
QR code
GET /apps/{id}/qr returns an image. Use format=svg (default) or format=png, plus the same options as the public QR URLs: fg, bg, margin, ecc, size, logo and download. See QR codes and press kit.
curl "https://mapps.one/api/v1/apps/01j8b2k6r3xq9v7m4n5p0s1t2w/qr?format=png&size=2048&ecc=Q" \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-o habitly-qr.png
Workspace
GET /workspace returns the workspace, its plan limits and features, current usage and the token in use. It is a good first call to check that a token works.
{
"data": {
"id": "01j8a0f4c7d2e9h6k3m5n8q1r4",
"name": "Habitly Labs",
"plan": {
"key": "studio",
"name": "Studio",
"limits": {
"apps": null,
"analytics_days": 730,
"members": 10,
"custom_domains": 25,
"legal_locales": null,
"screenshots": 10,
"webhooks": 10
},
"features": ["remove_badge", "all_themes", "accent_color", "qr_logo", "legal_full", "deep_links", "press_kit", "analytics_export", "custom_domains", "team", "folders", "ab_testing", "api", "webhooks", "white_label"]
},
"usage": { "apps": 12, "custom_domains": 3, "members": 4 },
"token": { "name": "GitHub Actions", "abilities": ["apps:read", "apps:write"] }
}
}
A null limit means unlimited.
Event types
GET /events lists the webhook event types with a short description. Integrations can use it to build an event picker.
{
"data": [
{ "type": "app.created", "description": "An app was added to the workspace." },
{ "type": "app.updated", "description": "App details, links or page settings changed." },
{ "type": "app.deleted", "description": "An app was deleted." },
{ "type": "app.status_changed", "description": "An app went live, was paused or was suspended." },
{ "type": "app.metadata_fetched", "description": "Store metadata was imported for an app." },
{ "type": "legal.published", "description": "A privacy policy or terms document was published." },
{ "type": "domain.verified", "description": "A custom domain was verified." },
{ "type": "link.clicked", "description": "Every smart link click. High volume." },
{ "type": "analytics.daily_summary", "description": "Yesterday's click totals for each app, sent once a day." }
]
}
Webhook subscriptions
Create webhook subscriptions from code, for example when a user connects your integration, and delete them when they disconnect. POST /webhooks takes:
| Field | Required | Description |
|---|---|---|
url | Yes | A public https URL that receives the events |
events | Yes | An array of event types, or ["*"] for all events |
description | No | Up to 160 characters, shown in the dashboard |
curl https://mapps.one/api/v1/webhooks \
-X POST \
-H "Authorization: Bearer $MAPPS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.zapier.com/hooks/catch/1234567/abcde12/",
"events": ["link.clicked"],
"description": "Zapier: clicks to Google Sheets"
}'
HTTP/2 201
{
"data": {
"id": "01j8c1p4s7v0y3b6e9h2k5n8q1",
"url": "https://hooks.zapier.com/hooks/catch/1234567/abcde12/",
"events": ["link.clicked"],
"description": "Zapier: clicks to Google Sheets",
"is_active": true,
"secret": "9f2c4e7a1b8d3f6e0a5c2b9d7e4f1a3c6b8d0e2f4a6c8e0b2d4f6a8c0e2b4d61",
"created_at": "2026-09-14T10:05:12+00:00"
}
}
The signing secret is only returned when the subscription is created. Use it to verify signatures. A Studio workspace can have up to 10 webhook endpoints. Delete a subscription with DELETE /webhooks/{id}, which returns 204 No Content.
Example: publish a beta from CI
This script creates an app, adds its Google Play link, imports the listing and prints the smart link. It needs curl, jq and a token with apps:write.
#!/usr/bin/env bash
set -euo pipefail
API="https://mapps.one/api/v1"
HEADERS=(-H "Authorization: Bearer $MAPPS_TOKEN" -H "Accept: application/json" -H "Content-Type: application/json")
APP_ID=$(curl -sf "${HEADERS[@]}" -X POST "$API/apps" \
-d '{"name": "Habitly Beta", "slug": "habitly-beta"}' | jq -r '.data.id')
curl -sf "${HEADERS[@]}" -X PUT "$API/apps/$APP_ID/links" \
-d '{"links": {"google_play": "https://play.google.com/store/apps/details?id=com.habitly.beta"}}' > /dev/null
curl -sf "${HEADERS[@]}" -X POST "$API/apps/$APP_ID/metadata" \
-d '{"overwrite": true}' | jq -r '.data.urls.smart_link'
Versioning
This is version 1 of the API. We may add endpoints, fields and event types at any time, so ignore fields you do not recognise. Breaking changes will ship as a new version with advance notice.