AppMate for AI agents
AppMate is built so coding agents (Claude, Cursor, Codex, etc.) can integrate it end-to-end without a human staring at docs. Two things make this work today, with more on the roadmap.
llms.txt
Per the llmstxt.org proposal, AppMate publishes a discovery file at the root:
https://appmate.cloud/llms.txt
It lists every doc page + canonical URLs the agent should read before writing integration code. The full long-form version (every page concatenated) is at /llms-full.txt.
Integration prompt
Copy this into Claude, Cursor, or any agent that can read URLs and edit your iOS project. It contains everything the agent needs to wire AppMate into a SwiftUI or UIKit app:
You're helping integrate AppMate into an iOS app's cancel flow.
Context:
- AppMate is a hosted service at https://appmate.cloud.
- It runs a short, App Store–safe cancel flow when users tap "Cancel
Subscription" — collects a reason, optionally shows an offer or
feedback path, always lets users continue to Apple's manage-subscriptions UI.
Steps to complete:
1. Ask the user for: their AppMate app slug, their app's custom URL scheme.
If they don't have an AppMate account, point them to
https://flow.appmate.cloud/login (free tier covers one app).
2. Add the Swift Package "https://github.com/fil-technology/appmate-ios" to
the Xcode project (Package.swift or File → Add Package Dependencies).
Pin to from: "0.2.0".
3. Add the URL scheme to Info.plist under CFBundleURLTypes if not already
present.
4. At app launch, call RetentionFlow.configure(.init(appSlug:baseURL:urlScheme:))
with appSlug from step 1, baseURL "https://cancel.appmate.cloud",
urlScheme from step 1.
5. Replace the user's current "Cancel Subscription" button handler with
RetentionFlow.startCancelFlow(userId:attributes:onAction:) — pass the
user's id if available, and a switch on link.action handling at least
.manageSubscription (call RetentionFlow.presentManageSubscriptions()).
6. Add .onOpenURL { url in if let link = RetentionFlow.deepLink(from: url)
{ handle(link) } else { /* existing handler */ } } to the SwiftUI App or
application(_:open:options:).
Important:
- AppMate URLs are namespaced as {scheme}://retention-flow/action so they
never collide with existing deep links. Always chain AppMate parsing
BEFORE the existing handler.
- Never block the cancellation path. Always offer .manageSubscription as
a fallback.
Docs: https://docs.appmate.cloud
SDK: https://github.com/fil-technology/appmate-ios
Deep link contract: https://docs.appmate.cloud/deep-linksMCP server
Published on npm as @fil-technology/appmate-mcp. Install via npx, paste an API token, and your agent gets typed tools like list_apps, create_app, publish_cancel_flow, list_waitlist_signups.
// claude_desktop_config.json — or .mcp.json for Cursor / Codex
{
"mcpServers": {
"appmate": {
"command": "npx",
"args": ["-y", "@fil-technology/appmate-mcp"],
"env": {
"APPMATE_TOKEN": "amk_…",
"APPMATE_API_URL": "https://flow.appmate.cloud"
}
}
}
}Get a token at flow.appmate.cloud/admin/settings/api-tokens. Full REST docs at API reference. Source: github.com/fil-technology/appmate-mcp.
What the agent can do today
- Read the deep-link contract and write parser-compatible code.
- Add the Swift Package to your project + wire
RetentionFlow.configureat launch. - Replace the Cancel Subscription handler with
startCancelFlow. - Generate the embed iframe snippet for a waitlist on your marketing site. The embed at
signup.appmate.cloud/embed/waitlist/{appSlug}picks up the same theme + accentColor + titleFont + formLayout the standalone page uses. Append?transparent=1to drop the card chrome when the host page already wraps the iframe in its own container.
Authoring flow configs — do/don't
warnings array with the same checks at runtime — use them to self-correct.Pick the right action for each button
Every primaryButton / secondaryButton has an action that determines what actually happens when the user taps. Choose by what the user should EXPERIENCE, not by what feels semantically close:
return_to_app— close the cancel flow, send the user back to where they were in the app. No params.manage_subscription— open Apple's subscription management UI. The required escape hatch — never hide it.open_offer+offerId— fire a deep link that the iOS app interprets as “present this StoreKit promo offer.” The app does the actual presentation.open_premium+ optionalpaywallId— deep link to a specific paywall variant inside the app (e.g.cancel_save_monthly). SkippaywallIdfor the default paywall.open_support+ optionalsupportTopic+message— deep link the iOS app to open its support inbox prefilled. Topic is a string YOUR APP routes on (e.g.bug_report,billing).open_feature+featureId— deep link to a specific screen/feature inside the app (e.g.onboarding,settings). The app must recognize the id.external_url+url— open a public HTTPS URL in the browser. Use for docs, migration guides, refund policies. NEVER use for a URL that needs auth.none— record the click, do nothing. Rare; useshowThanksScreen: trueon the response instead when you just want feedback.
showThanksScreen is opt-in for feedback-only paths
Setting showThanksScreen: trueon a response tells the renderer to record the click and land on a generic “Got it — thanks” screen INSTEAD of firing the deep link. Use it ONLY when the click itself is the entire signal you want — typically the missing-feature, too-clunky, or found-another-appreasons where you're collecting a reason, not routing the user.
Pair the label with the behaviour. When showThanksScreen is on, the label should be feedback-shaped— “Send feedback”, “Tell us why”, “Submit”. When it's off, the label should be destination-shaped— “Contact support”, “Claim 20% off”, “Open tutorial”. Mismatches are the #1 mistake the warnings system flags.
Bad vs. good
Both configs parse. The first one silently breaks the user experience — “Contact support” promises navigation, but showThanksScreen suppresses the deep link, so the user taps and lands on a generic thanks screen wondering if anything happened.
// ✗ BAD — label promises a destination, showThanksScreen blocks it
{
"primaryButton": { "label": "Contact support", "action": "open_support" },
"showThanksScreen": true
}
// ✓ GOOD #1 — label matches the feedback-only behaviour
{
"primaryButton": { "label": "Send feedback", "action": "open_support" },
"showThanksScreen": true
}
// ✓ GOOD #2 — actually opens the in-app support inbox, no thanks screen
{
"primaryButton": {
"label": "Contact support",
"action": "open_support",
"supportTopic": "bug_report",
"message": "I'm seeing this issue:"
}
}Other patterns to follow
- Always have a secondary button on response screens. Almost always
manage_subscription— the App-Store-safe escape hatch. - Use
open_featurewith a real, app-known featureId. If the iOS app doesn't recognizefeatureId: "onboarding", the deep link fires but does nothing visible. Confirm the id with the iOS codebase before publishing. - Use
open_offerwith a stable offerId. Generic names like"offer"or"test"trigger a placeholder warning. The id should be unique per offer (e.g.ios_20_off_3_months). - Cover every reason in
responses. The warnings system flags reasons without a configured response — the renderer redirects back to the picker, which feels broken. - Multi-select skips response screens. When
reasonScreen.selectMode: "multi", the click goes straight to thanks. Don't bother authoring per-reason responses — they're dead config. Use multi for “rate which factors mattered”-style flows. - Test on a real device. URL schemes like
quakemate://only resolve when the iOS app is installed. A desktop browser may show a permission prompt or appear to do nothing — that's expected. Useexternal_urlfor desktop-testable validations.
The warnings response
Every PUT /api/v1/apps/{id}/flows/... returns:
{
"ok": true,
"warnings": [
{
"code": "thanks_screen_blocks_navigation",
"path": "/responses/technical_issue/primaryButton",
"message": "Button labelled \"Contact support\" with action \"open_support\" suggests the user will be navigated somewhere, but the response has `showThanksScreen: true` which suppresses the deep link. ..."
}
]
}Treat any non-empty warnings array as your cue to re-read the config and probably re-PUT a fixed version BEFORE publishing. The warnings are stable codes — pattern-match thanks_screen_blocks_navigation, missing_response, placeholder_offer_id, etc., and fix them in a single follow-up call.
Authoring waitlist landing pages
The public URL at signup.appmate.cloud/{slug} renders a FULL landing page — hero, eyebrow chip, value-prop bullets, social-proof pill, themed background — not just a form. The visual treatment lives in config.hero, all fields optional. Omit the hero block entirely for the minimal card-on-neutral look (backward-compatible with old configs).
Pick a theme by intent, not by feel
minimal— clean white card on neutral background. Default. Use when the user's brand lives elsewhere on the page (e.g. a marketing site frame) or for conservative B2B.gradient— pastel blobs + accent color. Marketing- launch energy. Use when this IS the brand surface and you want some emotional pull.dark— dark hero, accent glow. Product-reveal vibe. Use for premium / pro-tier waitlists.side_by_side— two-column desktop layout (story left, form right). Collapses to minimal on phone — that's the public renderer's honest responsive behaviour, not a bug. Use when you have 3+ bullets worth telling a story about.
Full waitlist config shape
{
"type": "waitlist",
"intro": {
"title": "Get AppMate Pro early",
"subtitle": "Unlimited apps, custom domains, webhooks…",
"emailPlaceholder": "you@company.com",
"submitLabel": "Notify me",
"legal": "One email when Pro launches."
},
"success": {
"title": "You're on the list.",
"body": "We'll email you the moment Pro is ready.",
"ctaLabel":"Back to the dashboard",
"ctaUrl": "https://flow.appmate.cloud/login"
},
"hero": {
"theme": "gradient",
"eyebrow": "Founder pricing",
"accentColor": "#7c3aed",
"showCount": true,
"bullets": [
{ "icon": "📈", "title": "Unlimited apps", "body": "Across your portfolio." },
{ "icon": "🪝", "title": "Webhooks + API", "body": "Pipe events into your stack." },
{ "icon": "🎨", "title": "Custom domains", "body": "Serve flows from your own brand." }
]
}
}Patterns to follow
- When you have a real launch date or beta-window phrase, put it in
hero.eyebrow(under 40 chars). It tightens the page's emotional hook without bloating the title. accentColortints THREE things: the submit button, the eyebrow chip, and the gradient blob. Pick a colour that reads well on white AND has enough saturation that the chip's 12%-alpha background is still visible.showCount: truerenders a “{N} on the waitlist” pill above the form. The renderer hides the pill when the count is < 3 — “1 person on the waitlist” looks worse than no pill. So you can ship this on every launch without orchestrating the cutover.- Cap bullets at 3 for the gradient and side_by_side themes — they look great with three, cluttered with five. The minimal theme tolerates 4–5 because they sit below the form, not next to it.
- Use the live /examples?kind=waitlist gallery as your reference set. Each card's “Copy config” emits the exact JSON you'd PUT — a fast way to seed.
Starter templates
Six waitlist starter templates exist server-side. The user can click any from /examples?kind=waitlist and land in the editor with the full config pre-filled. From the MCP / REST side, you can just emit the same shape directly:
minimal_email_only— single email field on a white card. The baseline.feature_tease— gradient, 3 bullets, social proof. Best when there's product personality to brag about.launching_soon— dark theme, date-shaped eyebrow. Good for product reveals where the date is the story.pro_upsell— side_by_side, premium bullets, founder- rate framing. The Pro/Plus tier waitlist.private_beta— minimal, “Invite-only” eyebrow, downplays count. Positions access as scarce.early_access_referral— gradient, “skip the line by inviting friends” framing.
Authoring feedback & report flows
Two more flow types live on the same dashboard + API surface as cancel + waitlist. Both are hosted, themeable forms with cursor- paginated submission lists. Same auth, same publish/draft model, same warnings response — just different schemas.
When to use which
- Feedback— open-ended “tell us what you think” collection. Optional 1–5 star rating + free-text message + optional reply email. Lives at
appmate.cloud/feedback/{appSlug}. - Report — categorised bug / abuse / spam channel. Required category picker (1–10 entries, snake_case ids) + free- text message + optional reply email. Lives at
appmate.cloud/report/{appSlug}. The submit endpoint validates the posted category against the flow's live category list — unknown ids get a 422.
Feedback config example
{
"type": "feedback",
"intro": {
"title": "Send us feedback",
"subtitle": "Tell us what's working, what's broken.",
"messagePlaceholder": "What's on your mind?",
"submitLabel": "Send feedback"
},
"rating": { "enabled": true, "prompt": "Rate your experience", "required": false },
"emailField": { "enabled": true, "placeholder": "you@example.com", "required": false },
"success": {
"title": "Thanks — we got it.",
"body": "Your feedback is in front of the team."
}
}Report config example
{
"type": "report",
"intro": {
"title": "Report an issue",
"subtitle": "Tell us what went wrong.",
"messagePlaceholder": "What happened?",
"submitLabel": "Submit report"
},
"categories": [
{ "id": "bug", "label": "Bug or crash", "emoji": "🐞" },
{ "id": "abuse", "label": "Harassment or abuse","emoji": "🚫" },
{ "id": "spam", "label": "Spam", "emoji": "🧹" },
{ "id": "privacy", "label": "Privacy concern", "emoji": "🔒" },
{ "id": "other", "label": "Something else", "emoji": "💬" }
],
"emailField": { "enabled": true, "required": false },
"success": {
"title": "Report received.",
"body": "We'll review and follow up if we need more details."
}
}REST endpoints
GET /api/v1/apps/{id}/flows/feedback+PUT+POST /flows/feedback/publish.GET /api/v1/apps/{id}/flows/report+PUT+POST /flows/report/publish.GET /api/v1/apps/{id}/feedback/submissions?limit=&cursor=GET /api/v1/apps/{id}/report/submissions?limit=&cursor=&category=
Both PUT endpoints return { ok: true, warnings: [] } — the warnings array is empty today (no rules wired) but the shape matches the cancel + waitlist endpoints so agents can use the same response handling code path.