tenure

Developers

One prompt, one tag, or no code at all.

Three ways to install, with readiness checks matched to the experiences you enable. Your coding agent can do it over MCP; you review and publish.
Connect your coding agent · Claude Code
claude mcp add --transport http tenure \
  "https://app.trytenure.co/api/mcp?profile=install" \
  --header "Authorization: Bearer $TENURE_TEST_KEY"

Needs a free Tenure account: create a test key in Settings › Developer after sign-up.

Agent install

Agents draft. People publish.

  1. 01Create a test keyThe Read, Runtime and Draft scopes, a 7-day expiry. It lives in the agent's environment only.
  2. 02Paste the promptFrom Settings › Install, into Claude Code, Cursor or any coding agent.
  3. 03The agent proposesFacts to track, where to mount cards, how users map to billing, each with a citation.
  4. 04You reviewAccept or decline each item. Publishing stays with a signed-in owner or admin.
$ Connected to tenure over MCP, with a test key

✓ get_app_definition · get_verification✓ propose_facts · 7 facts, each with file:line✓ widget wired · simulate_decision→ submit_for_review. A person decides.

Install

Example checks; your setup determines which are required.
  • Billing is talking to Tenure
  • We know who your users are
  • Your app is telling us what happens
  • Customers can see it
  • Optional: test billing change
  • Email sending is verified

Or let your AI coding agent do it

A prompt for Claude Code, Cursor or Copilot. The agent proposes and tests; you review and publish.

First create a test key and put it in the agent’s environment as TENURE_TEST_KEY. Then paste the prompt in your repository. The agent reads your code, proposes the signals your app should send, wires the widget, runs a test decision and sends you its proposals to review. It cannot publish.

Example prompt

Install Tenure in this app. Tenure runs the cancel flow, win-back and in-app offers for this subscription product. You prepare the install; a person reviews it and publishes. Never ask for, or attempt, publishing, confirming or enabling anything.

  1. 1.Read https://app.trytenure.co/llms.txt first, then the pages it lists under "Install in one sitting" (install.md, mcp.md, events.md).
  2. 2.Connect to Tenure's MCP server: POST https://app.trytenure.co/api/mcp?profile=install with the header Authorization: Bearer $TENURE_TEST_KEY. That is a test key (it starts ck_test_) with the Read, Runtime and Draft scopes, expiring in 7 days. It can read, send test events, propose and submit for review; it cannot publish.
  3. 3.Call ping, get_app_definition and get_verification: what Tenure already knows, and which checks your setup needs, what is missing and what each would unlock.
  4. 4.Read this codebase to find how a signed-in user maps to a billing customer, the product's meaningful actions, and where the billing page, the cancel button and any locked features live.
Static example: the first four steps of the prompt.
The prompt, as it appears in Settings › Install.

Ready means your required checks pass, each read from real evidence. Required checks depend on your setup and experiences; test billing changes are optional diagnostics, and simulations and previews never count as production evidence.

MCP server

One endpoint, scoped keys.

Streamable HTTP. Keys carry the Read, Runtime and Draft scopes (read, decide and draft_write in the API); there is no publish scope.

Endpoint
https://app.trytenure.co/api/mcp
Key
A test key (ck_test_…). Draft writes are capped at 120 an hour per key.

Inspect

pingget_app_definitionget_verificationget_review_status

Test

track_eventsimulate_decisionreport_slots

Propose

propose_factspropose_slotspropose_identity_mappingsubmit_for_review

Decide

recommend_retention_offerreport_offer_eventenable_retention_offerescalate_to_human

Widget

One tag, on the page you have.

The flow loads only when someone reaches for cancel.

  • About 5 KB · gzipped, on every page
  • Shadow DOM · its own, so your styles stay yours
  • Fails open · Tenure can’t load? Your cancel link runs
HTML
<script src="https://app.trytenure.co/widget.js" data-key="pk_..." data-customer="USER_ID"
        data-auth="SIGNED_HASH" data-mode="test"></script>

<!-- Mark the cancel button you already have. Its href is followed
     after the subscriber confirms cancelling. -->
<a href="/account/cancel/confirm" data-tenure-cancel>Cancel subscription</a>

data-auth is the lowercase hex HMAC-SHA256 of the subscriber id, keyed with your widget signing secret (Settings › Install), computed on your server. Remove data-mode when you switch to live.

Hosted pagehttps://app.trytenure.co/c/<public key>, from a signed link or an email code.It can’t load while Tenure is down; the widget falls back to your link.
StylingMatch the page, --tenure-* variables and named ::part() hooks.
React and headless<CancelFlowButton>, useCancelFlow() and createCancelFlow(), from an optional source SDK.Not on npm; the script tag works without a package.
MobileiOS, Android, React Native and Flutter packages present the hosted flow in your app. TV hands off by QR code.The flow's screens show in a web view.

REST and webhooks

Your own screens, the same rules.

Recommend, report what was shown, then enable. Cancel always exits.

Node
const rec = await tenure.recommend({ customerId: user.id, reason: "too_expensive",
  conversation: "It's too expensive for us now" });

if (rec.decision === "offer" && rec.offer) {
  await tenure.deliveries.report(rec.deliveryId!, "rendered");
  await tenure.enable({ sessionId: rec.sessionId!, action: "accept" });  // on an explicit yes
} else if (rec.sessionId) {
  await tenure.enable({ sessionId: rec.sessionId, action: "cancel" });   // cancel always exits
}

SDK-style example for teams with the source package. It is not published on npm; use the package-free REST examples in the docs. The decision runs your published Cancellations plan.

Webhooks · Node 18+, no package
// Node 18+, no SDK package required. Pass the ORIGINAL raw body.
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTenureWebhook(rawBody, signature, timestamp, secret) {
  if (!secret || typeof signature !== "string" ||
      !/^[a-f0-9]{64}$/i.test(signature) ||
      typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) return null;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return null;
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".").update(rawBody).digest();
  const received = Buffer.from(signature, "hex");
  if (!timingSafeEqual(expected, received)) return null;
  try {
    const event = JSON.parse(rawBody.toString());
    return event && typeof event.event === "string" ? event : null;
  } catch { return null; }
}

// In your handler, pass x-tenure-signature and x-tenure-timestamp
// headers plus your webhook signing secret. Reject a null result.
// Handle each eventId once: retries reuse it. Do not parse or
// re-serialize the body before verification.

Verify the original raw body with your signing secret, then deduplicate by eventId. HMAC-SHA256; retries reuse the event ID.

Start with a test key.

New workspaces start in test mode: test keys see test data only, and nothing reaches a real customer until you publish.

Drafting your plan starts with a free account, then you connect billing.

Free during early accessStripe, Chargebee or your own billingQuestions first? Talk to us