---
title: "Confirm an email change (dashboard session only)"
method: POST
path: "/v1/customers/me/email/confirm"
tags: ["Account"]
---

# Confirm an email change (dashboard session only)

`POST /v1/customers/me/email/confirm`

## Request

- Base URL: `https://api.placementapi.com`
- URL: `https://api.placementapi.com/v1/customers/me/email/confirm`
- Auth: HTTP bearer

Session only and cross-origin protected. Send the token in the JSON POST BODY, never the URL query; the emailed link carries it in the URL fragment, which browsers do not transmit. The live session must belong to the SAME account the token is bound to (not necessarily the browser that began the change); the token is single-use and expires after 30 minutes, and the account must still carry the address it had when the change was requested. The account update, the single-use burn, the end of every browser session on the account and the burn of unconsumed sign-in links for the PREVIOUS address all happen in one SQL statement, so an invalid, replayed or conflicting confirmation changes none of them. Because every session ends, the buyer is signed out and must sign in again with the new address; API keys are left alone. On success the previous inbox is notified, with no link and no token. The committed email is best-effort mirrored onto the account's Stripe customer; that outcome is reported as `billing_email_sync` and never rolls the change back. NOT available to API keys (403 `dashboard_only`).

## Request body

- object
  - `token` string, required: The single-use token from the confirmation email. In the POST body, not the query string.

## Response `200`

OK

- object
  - `customer_id` string, required
  - `name` string, required
  - `email` string, required: The newly verified address. All sessions were ended, so the client must sign in again with this address.
  - `billing_email_sync` 'not_needed' | 'updated' | 'pending', required: Whether the buyer's Stripe billing customer was moved to the new address. `not_needed`: the account has no Stripe customer, so no call was made. `updated`: Stripe accepted the new address. `pending`: the account email DID change, but the Stripe mirror did not; billing mail may still reach the previous address and the failure is recorded for an operator. Pending never means the email change failed.
  - `billing_email_sync_reason` 'not_configured' | 'stripe_error' | 'lookup_failed': Present only when `billing_email_sync` is `pending`. `not_configured`: no Stripe key on this deployment. `stripe_error`: Stripe rejected the customer update. `lookup_failed`: the account's Stripe customer id could not be read.

## Other responses

- `400`: `invalid_token`: missing, malformed, expired, already used, or not for this account.
- `401`: Missing, malformed or revoked Bearer credential: `{ error: "unauthorized" }`.
- `403`: `dashboard_only` for an API key; `forbidden` for a cross-origin request.
- `409`: `email_taken`: another account claimed the address before this completed; no change was made.
- `503`: `server_misconfigured` when the store is not configured; `upstream_unfunded` when the measurement provider's prepaid balance is exhausted (fresh profiles, gap maps and SERPs pause; cached results and catalogue are unaffected); `upstream_unavailable` on a provider error; `payments_unavailable` when card payments are not configured.

---

[API](https://skmtc.dev/placementapi/apis/placement-api.md) · [All operations](https://skmtc.dev/placementapi/apis/placement-api/llms.txt) · [OpenAPI document](https://skmtc.dev/placementapi/apis/placement-api/revisions/fc4e7866eaf7?raw)
