---
title: "Records an identity's plan and its Stripe customer link (subscription activation, renewal, cancellation, or a sales/comp plan set by hand). Idempotent by `reason`, mirroring the quota endpoint."
method: POST
path: "/admin/identities/{email}/plan"
tags: ["admin"]
---

# Records an identity's plan and its Stripe customer link (subscription activation, renewal, cancellation, or a sales/comp plan set by hand). Idempotent by \`reason\`, mirroring the quota endpoint.

`POST /admin/identities/{email}/plan`

## Request

- Base URL: `https://api.mapmap.ai`
- URL: `https://api.mapmap.ai/admin/identities/{email}/plan`
- Auth: HTTP bearer

**The identity ↔ Stripe customer join.** The path email is the primary
key: it is the identity's own address, read out of the gateway by
checkout and mirrored into our Stripe metadata, and it resolves through
[`identity::identity_by_email`]'s alias-collapsed `canonical_key`; so a
subscription bought as `you+work@gmail.com` reaches the identity that
signed up as `you@gmail.com`. `stripe_customer_id` is *stamped onto*
that row, not joined on, because Checkout mints a fresh Stripe customer
per session: customer → identity is many-to-one, so it could never be
the primary join. When the email matches nothing but the supplied
customer id is already linked to an identity, that link is used as a
fallback (the customer edited their email in Stripe's billing portal),
rather than 404-ing for Stripe's whole retry window.

## Path parameters

- `email` string, required

## Request body

- SetPlanRequest: Body for `POST /admin/identities/{email}/plan`.
  - `plan` string, required: Coarse plan label to record on the identity: 1–[`MAX_PLAN_LEN`] characters of lowercase `a-z`, `0-9`, `-` or `_`. Cancellation writes [`FREE_PLAN`] back.
  - `reason` string, required: Idempotency reason, e.g. `stripe-sub:sub_…:1750000000`. Repeating a reason is a no-op returning the recorded plan. Share it with the quota / download-allowance / included-MAU calls for the same Stripe event so all four move together.
  - `stripe_customer_id` string, nullable: The Stripe customer that paid (`cus_…`), when the change came from Stripe. Optional so sales/comp plans can be set by hand; omitting it (or sending `null`) leaves any existing link untouched, it is never cleared, so a lapsed account stays traceable to its past invoices.

## Response `200`

Plan applied (or unchanged, when the reason was replayed)

- PlanResponse: Response for `POST /admin/identities/{email}/plan`.
  - `duplicate` boolean, required: `true` when the reason had already been applied (idempotent replay; nothing changed).
  - `email` string, required: Canonical (lowercased) identity email.
  - `plan` string, required: The plan now recorded for this reason.
  - `stripe_customer_id` string, nullable: The Stripe customer the identity is linked to after the change.

## Other responses

- `400`: Invalid plan, stripe_customer_id or reason
- `401`: Missing or wrong admin token
- `404`: Unknown identity email, and no identity linked to the supplied Stripe customer

---

[API](https://skmtc.dev/mapmap/apis/sn-gateway.md) · [All operations](https://skmtc.dev/mapmap/apis/sn-gateway/llms.txt) · [OpenAPI document](https://skmtc.dev/mapmap/apis/sn-gateway/revisions/2293be8b4c38?raw)
