Skip to content

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

AbilityAllows
apps:readList and read apps, download QR codes
apps:writeCreate, update and delete apps, replace store links, import store metadata
analytics:readRead app analytics
webhooks:manageList, 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 by id, 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 data key.
  • Store keys are app_store, google_play, app_gallery, microsoft_store and web.
  • Platform keys are ios, android, huawei, windows, macos and other. Destination keys are the store keys plus landing.

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.

StatusWhen
401The token is missing, invalid or revoked. {"message": "Unauthenticated."}
402The workspace plan does not include API access. Code plan_required.
403The token does not have the ability this endpoint needs. {"message": "Invalid ability provided."}
404The app or subscription does not exist, or belongs to another workspace.
422Validation failed, or the request could not be completed. For example, code metadata_unavailable.
429Rate limit exceeded. Wait for the number of seconds in Retry-After.
5xxSomething 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 pathAbilityDescription
GET /workspaceanyWorkspace, plan, usage and the current token
GET /eventsanyWebhook event types
GET /appsapps:readList apps
POST /appsapps:writeCreate an app
GET /apps/{id}apps:readRetrieve an app
PATCH /apps/{id}apps:writeUpdate an app
DELETE /apps/{id}apps:writeDelete an app
PUT /apps/{id}/linksapps:writeSet or remove store links
POST /apps/{id}/metadataapps:writeImport details from the store listing
GET /apps/{id}/analyticsanalytics:readTotals, daily series and breakdowns
GET /apps/{id}/qrapps:readQR code image
GET /webhookswebhooks:manageList webhook subscriptions
POST /webhookswebhooks:manageCreate a webhook subscription
DELETE /webhooks/{id}webhooks:manageDelete a webhook subscription

The app object

FieldTypeDescription
idstringULID of the app
namestringApp name
slugstringThe path of the smart link: mapps.one/{slug}
statusstringlive, paused or suspended
tagline, descriptionstring or nullPage text
categorystring or nullA category key, such as health-fitness or productivity
page_themestringclean, midnight or vivid
accent_colorstring or nullHex colour, such as #16a34a
developerobjectname, website and email
social_linksobjectFilled social profiles, keyed by network: x, instagram, tiktok, youtube, facebook, linkedin, threads, github, discord, reddit
featuresarrayFeature items with icon, title and description
listed_in_explorebooleanWhether the app appears in Explore
utm_passthroughbooleanWhether UTM tags are forwarded to stores
redirect_rulesobjectDestination per platform, with defaults filled in
icon_urlstring or nullPublic URL of the icon
ratingobject or nullvalue and count from the store
urlsobjectPublic URLs: smart_link, landing_page, qr_svg, qr_png, privacy_policy, terms. Legal URLs return 404 until the document is published.
linksobjectStore links keyed by store, each with url, status (unverified, verified or invalid) and verified_at. Included on responses for a single app.
screenshotsarrayScreenshots with url, width and height. Included after a metadata import.
created_at, updated_atstringISO 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

FieldRequiredDescription
nameYesApp name, up to 120 characters
slugNo3 to 40 lowercase letters, numbers and single dashes. When omitted, a free slug is generated from the name.
linksNoStore 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.

FieldType
namestring
slugstring. Changing it breaks the old link immediately.
tagline, descriptionstring or null
categorycategory key or null
page_themeclean, midnight or vivid
accent_color#rrggbb or null
utm_passthrough, listed_in_exploreboolean
redirect_rulesobject 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.

PUT /apps/{id}/links takes a links object keyed by store:

  • a URL adds or replaces the link for that store,
  • null or 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.

FieldDescription
overwriteOptional, 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:

ParameterDescription
range7d, 30d, 90d, 12m or custom. Default 30d.
from, toDates as YYYY-MM-DD, both included, for range=custom. to cannot be later than today.
timezoneAn 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 }
    }
  }
}
  • from is included and to is excluded. Both are UTC instants.
  • clamped_to_plan is true when the requested range was shortened to the plan's history.
  • daily counts 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:

FieldRequiredDescription
urlYesA public https URL that receives the events
eventsYesAn array of event types, or ["*"] for all events
descriptionNoUp 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.