---
title: "Claim Goal"
method: POST
path: "/goals/claim"
tags: ["goals"]
---

# Claim Goal

`POST /goals/claim`

## Request

- Base URL: `https://api.imperfect.co` (the document declares no server; this is the origin it was published from)
- URL: `https://api.imperfect.co/goals/claim`
- Auth: HTTP bearer

Take the goal this token names into the authenticated account (#2566).

**The one Firebase route on this surface**, and the split is the point.
Every other ``/goals`` route answers an anonymous browser holding a cookie,
because an athlete settles a goal before they have an account (#1851); this
one answers the app, which holds a bearer and none of the browser's three
signals — no cookie, no CSRF token (a browser concept), no ``Origin``. The
token proves *I was handed this goal's link*; the bearer proves *I am this
athlete*. Neither alone is enough, which is the channel-link security model
(#858) with the goal in place of the channel identity.

First claim wins while there is still an account to lose it to: a second
bearer gets 409 when the goal's holder can still be found — it holds a
provider link or an email — and a holder that can be found by neither is a
dead install, so the goal is handed back instead (#2624 D6,
:func:`_handed_back`). The same account claiming again is a success with
``claimed_at`` unmoved, because the app may resend a confirm whose answer it
never saw.

Answers with the owner's detail payload, so the app draws the goal from the
claim instead of following it with a read — and reads it in the shape
looking-glass already renders, plus ``recovered`` and the ``custom_token``
a re-hand has to sign in with.

**It is also the one route here that can create a ``User``**, because the
athlete it answers may not have one yet; :func:`_claiming_account` carries
that and the reasoning behind it. That is why the token is read first and
the row is minted second, which inverts what this handler used to do: a
claim that cannot land must not leave an account behind it, so an unknown
token and a goal already held both answer before anything is created. The
cost is one indexed read on a route an install runs once.

**The phone is written down when the claim lands** (#2637), for the same
reason the settle writes it: taking a goal into this account on this device
is the athlete committing to it here. After the claim, so a refused one
leaves no key behind either.

**A free goal on a phone we already know lands on the phone's account**
(#2952). An athlete who deletes the app, settles a new goal on the web and
installs from its button arrives on a fresh uid holding the link, and the
key is the only thing that says who they are. Claiming onto the fresh uid
minted a second account and then repointed the key at it, so the first
account and its goal were left where nothing would ever reach them. The claim
now makes the bootstrap's recognition (:func:`_recognized_phone`, the same
rule and the same refusals) before it stamps the goal, and answers
``recovered`` with the token the phone signs in with. The same phone asking
again, once the goal is on the key's account, gets the same answer, so a
lost response or a failed fold is a retry away. A goal someone else holds, a
bearer's own replay, a key nobody holds and a key the bearer already holds
all claim as before.

## Headers

- `x-device-timezone` string, nullable
- `x-device-hint` string, nullable
- `x-client-platform` string, nullable

## Request body

- GoalClaimRequest: The token the app read out of the link it was opened with. Only the token, the way ``POST /channel_associations`` takes only the token: who is claiming comes from the Firebase bearer, so a body that could name a user would be a body that could claim for somebody else.
  - `token` string, required

## Response `200`

Successful Response

- GoalClaimResponse: The claim's answer: the goal, plus how the phone reaches the account (#2636). The detail payload the owner's page reads, subclassed rather than widened, because these two fields are only ever true of a claim: ``GET /goals/{id}`` is a read and has no account to hand anyone. Both are the second hand-over (#2624 D6). A goal whose holder is a dead install — a row with no provider link and no email, so nothing reaches it — comes back to the phone that presented the token instead of being refused, and the account it lands on may not be the bearer's own row. ``recovered`` says that happened; ``custom_token`` is what the app signs in with, exactly as the body link's recovery already hands one back, and is ``null`` when the bearer already holds the surviving account's uid.
  - `name` string, required
  - `line` string, nullable
  - `locale` string, required
  - `kind` 'destination' | 'practice', required
  - `sport` string, nullable
  - `set_at` string, date-time, required
  - `target` GoalDate, required: A dated commitment at its honest resolution. ``precision`` is the band: the date is normalized to the first day of its period (month and season → the 1st of that month, quarter → the quarter's first month), so a coarse band can never license a specific day the source hasn't published — the same normalization the plan rim applies to its events. A season keeps its own start month rather than the quarter holding it (#2945). Seasons do not line up with calendar quarters, and which months one covers depends on the athlete's hemisphere: spring is March in Mexico and September in Chile. The model writes the month it means, so the band runs three months from there; folding it into a quarter kept January and February for a spring and dropped April and May.
    - `date` string, date, required
    - `precision` 'day' | 'month' | 'quarter' | 'season'
  - `anchor_kind` 'event' | 'route', nullable
  - `anchor` GoalAnchorPayload: The event a goal is pinned to, resolved into what a client draws. Additive and optional: a goal that anchors nowhere — most of them — carries ``null`` here and the page looks exactly as it did before this existed. This is the whole reason the coach never writes a URL. The reply is plain chat text that ships to any channel as-is, so a link in it would be a link the model chose and nothing validated; here the host resolves the id the conversation anchored to and hands over the official name, date, place and site. The surface renders the card, and the coach's words stay words. ``website_url`` points at the organizer's own site rather than an imperfect.co event page: the athlete is being handed the place where they actually register. It resolves server-side, so retargeting it later is one line here and no change to any client.
    - `kind` 'event' | 'route', required
    - `id` string, required
    - `name` string, required
    - `date` string, date, nullable
    - `date_month` string, date, nullable
    - `city` string, nullable
    - `country` string, nullable
    - `website_url` string, nullable
    - `short_link` string, nullable
    - `distance` string, nullable
    - `distance_km` number, nullable
  - `factors` object[]
  - `events` GoalEvent[]
    - `name` string, required: The event, the way the athlete named it.
    - `when` GoalDate: A dated commitment at its honest resolution. ``precision`` is the band: the date is normalized to the first day of its period (month and season → the 1st of that month, quarter → the quarter's first month), so a coarse band can never license a specific day the source hasn't published — the same normalization the plan rim applies to its events. A season keeps its own start month rather than the quarter holding it (#2945). Seasons do not line up with calendar quarters, and which months one covers depends on the athlete's hemisphere: spring is March in Mexico and September in Chile. The model writes the month it means, so the band runs three months from there; folding it into a quarter kept January and February for a spring and dropped April and May.
      - `date` string, date, required
      - `precision` 'day' | 'month' | 'quarter' | 'season'
    - `id` string, nullable
    - `role` 'main' | 'rehearsal' | 'training', nullable
  - `page` GoalPage: The prose sections of one settled goal's page. Every field is optional in the sense that matters: a section the document does not carry is absent rather than filled, because a slot is minting pressure (#1850) and this projection has no athlete left in the loop to correct an invention. The page reads fine short. **The field descriptions are the spec the model works from.** They are what the projection prompt deliberately does not restate — one place says what a section is, and it is the type the answer has to satisfy.
    - `why` string, nullable: Why this goal matters: the passage that carries their reason, not the loudest one in it. In the athlete's own words: a passage copied exactly as the document has it, and only where those exact words read as clean writing, with no pause marks, fillers, cut-off words or words said twice. Begin it with a capital letter even when the passage starts mid-sentence; that is the only change allowed. Otherwise leave it empty. The passage also has to stand on its own under the goal's title, for a reader who never heard the conversation. A word like it or this for the goal or the training is fine. It fails only when it points back at something the page does not show: it counts itself as one of several reasons whose others are missing, or it continues, answers or contradicts a remark the reader never heard. Then select a contiguous passage that does not lean on it, or leave why empty.
    - `why_summary` string, nullable: Why this goal matters, when why has no clean quote. Only when the athlete said it but no clean passage of theirs exists: what they meant, in one plain sentence in the page's own voice, the way the rest of the page describes them, starting with a capital letter. Never I or you, never a name, never in quotation marks, and nothing they did not say. Lead with the reason, the feeling or the lesson rather than the person: no pronoun for the athlete, and never they, the athlete or the couple. A sentence that seems to need one is reworded, never dropped, so the slot fills whenever the athlete said it. No gendered pronoun for anyone, and in every language keep the athlete one person, in the singular; a verb agreeing with them is not a pronoun.
    - `journey` string, nullable: One plain line on where the athlete is coming from and what this goal is the next step of: the races behind them, the step up ahead, where they are today.
    - `sport` string, nullable: The athlete's own word for the sport this goal is in, whenever the document makes it plain; when its events span several sports, each of them in their words, listed the way they would list them. Nothing only when the document genuinely does not say.
    - `week` GoalWeekCommitment[]: Everything the week keeps, one entry each: the week this goal asks for, with its training amounts and build; availability; and regular or occasional activities, including those outside the goal's own sport. Preserve the difference between time available, planned training and activities already done. Keep occasional activities occasional, with no new days or frequencies.
      - `name` string, required: The commitment, in two or three words.
      - `detail` string, required: What it is, as the document states it.
    - `life` string[]: One short line each for the life the work has to fit: the days and hours actually available, the travel, the sleep, the dates already fixed, whatever the athlete called non-negotiable. Their constraints, never advice, and always in the page's own words: where the document carries a constraint as something the athlete said, say what it means, keeping their numbers at the coarseness they gave them, rather than reproduce the saying.
    - `history` GoalComingFrom: What this goal is built on: the race behind them, or where they start. An athlete with a race behind them is coming from that race and what it taught. A first-timer is coming from a starting point instead — the longest they have run in one go, what their weeks already hold — and their document states it just as plainly (#2013). A card only a past race could fill left theirs blank, on the one section of the page that shows the athlete it meets them where their journey starts. **The quotes are optional, and that is what makes both shapes fit.** ``felt`` and ``lesson`` are each a slot: a clean exact quote, gate-checked against the document, else a summary beside it, else nothing (#2866). Nobody quotes how their current mileage felt, so a starting point stands on ``event`` alone, held to the document by the grounding judge like every other sentence the model writes. Bench round 1 (#1984) had already shown that requiring both quotes drops a real card whose body quoted the athlete once; requiring one drops every first-timer's.
      - `event` string, required: The race or attempt, named as the document names it — or, when the document tells of none, the athlete's starting point in the same handful of words: the longest they have gone in one go, what their weeks already hold. The thing itself, never a sentence introducing it.
      - `felt` string, nullable: How it felt. In the athlete's own words: a passage copied exactly as the document has it, and only where those exact words read as clean writing, with no pause marks, fillers, cut-off words or words said twice. Begin it with a capital letter even when the passage starts mid-sentence; that is the only change allowed. Otherwise leave it empty.
      - `felt_summary` string, nullable: How it felt, when felt has no clean quote. Only when the athlete said it but no clean passage of theirs exists: what they meant, in one plain sentence in the page's own voice, the way the rest of the page describes them, starting with a capital letter. Never I or you, never a name, never in quotation marks, and nothing they did not say. Lead with the reason, the feeling or the lesson rather than the person: no pronoun for the athlete, and never they, the athlete or the couple. A sentence that seems to need one is reworded, never dropped, so the slot fills whenever the athlete said it. No gendered pronoun for anyone, and in every language keep the athlete one person, in the singular; a verb agreeing with them is not a pronoun.
      - `lesson` string, nullable: What it taught. In the athlete's own words: a passage copied exactly as the document has it, and only where those exact words read as clean writing, with no pause marks, fillers, cut-off words or words said twice. Begin it with a capital letter even when the passage starts mid-sentence; that is the only change allowed. Otherwise leave it empty.
      - `lesson_summary` string, nullable: What it taught, when lesson has no clean quote. Only when the athlete said it but no clean passage of theirs exists: what they meant, in one plain sentence in the page's own voice, the way the rest of the page describes them, starting with a capital letter. Never I or you, never a name, never in quotation marks, and nothing they did not say. Lead with the reason, the feeling or the lesson rather than the person: no pronoun for the athlete, and never they, the athlete or the couple. A sentence that seems to need one is reworded, never dropped, so the slot fills whenever the athlete said it. No gendered pronoun for anyone, and in every language keep the athlete one person, in the singular; a verb agreeing with them is not a pronoun.
    - `honest_subtitle` string, nullable: One line on what keeps this goal honest: where the coach's read differs from the athlete's target, or what the goal is being held to. Lead with the goal or the choice, in the page's own voice: never I or you, no gendered pronoun, and never the athlete named as a person.
    - `factor_lines` GoalFactorLines[]: One entry for each factor on the rim that has a position, so each of its two bars can carry a short line.
      - `name` string, required: The factor's name, exactly as the rim writes it.
      - `asks_line` string, nullable: What the goal asks on this factor, in a few words beside its bar: this factor's own asks as one figure, said the way the athlete would say it, in the document's language and the athlete's units, a pace or a clock time in minutes and seconds. Never a range, a second number or anything else the document says about it. Nothing when the factor has no asks.
      - `standing_line` string, nullable: Where the athlete is today on this factor, said the same way: this factor's own standing as one figure and nothing more. For a factor read off the body, its position and never the readings behind it. Nothing when the factor has no standing.
  - `page_expected_until` string, date-time, nullable
  - `checkpoints` GoalCheckpointPayload[]
    - `name` string, nullable: One or two of the athlete's own words for this checkpoint.
    - `measure` string, required: The sentence the athlete reads: what has to be observably true.
    - `by` GoalDate, required: A dated commitment at its honest resolution. ``precision`` is the band: the date is normalized to the first day of its period (month and season → the 1st of that month, quarter → the quarter's first month), so a coarse band can never license a specific day the source hasn't published — the same normalization the plan rim applies to its events. A season keeps its own start month rather than the quarter holding it (#2945). Seasons do not line up with calendar quarters, and which months one covers depends on the athlete's hemisphere: spring is March in Mexico and September in Chile. The model writes the month it means, so the band runs three months from there; folding it into a quarter kept January and February for a spring and dropped April and May.
      - `date` string, date, required
      - `precision` 'day' | 'month' | 'quarter' | 'season'
    - `on_miss` string, required: The sentence the athlete reads: what the goal reverts to, stated openly.
    - `anchor` GoalAnchorPayload: The event a goal is pinned to, resolved into what a client draws. Additive and optional: a goal that anchors nowhere — most of them — carries ``null`` here and the page looks exactly as it did before this existed. This is the whole reason the coach never writes a URL. The reply is plain chat text that ships to any channel as-is, so a link in it would be a link the model chose and nothing validated; here the host resolves the id the conversation anchored to and hands over the official name, date, place and site. The surface renders the card, and the coach's words stay words. ``website_url`` points at the organizer's own site rather than an imperfect.co event page: the athlete is being handed the place where they actually register. It resolves server-side, so retargeting it later is one line here and no change to any client.
      - `kind` 'event' | 'route', required
      - `id` string, required
      - `name` string, required
      - `date` string, date, nullable
      - `date_month` string, date, nullable
      - `city` string, nullable
      - `country` string, nullable
      - `website_url` string, nullable
      - `short_link` string, nullable
      - `distance` string, nullable
      - `distance_km` number, nullable
  - `alternatives` GoalCandidate[]
    - `name` string, required
    - `demands` string, required: One honest line on what this framing asks.
    - `label` string, nullable
    - `tags` string[]
    - `target` GoalDate: A dated commitment at its honest resolution. ``precision`` is the band: the date is normalized to the first day of its period (month and season → the 1st of that month, quarter → the quarter's first month), so a coarse band can never license a specific day the source hasn't published — the same normalization the plan rim applies to its events. A season keeps its own start month rather than the quarter holding it (#2945). Seasons do not line up with calendar quarters, and which months one covers depends on the athlete's hemisphere: spring is March in Mexico and September in Chile. The model writes the month it means, so the band runs three months from there; folding it into a quarter kept January and February for a spring and dropped April and May.
      - `date` string, date, required
      - `precision` 'day' | 'month' | 'quarter' | 'season'
    - `recommended` boolean
    - `chosen` boolean
  - `offered_goals` GoalCandidate[], nullable
    - `name` string, required
    - `demands` string, required: One honest line on what this framing asks.
    - `label` string, nullable
    - `tags` string[]
    - `target` GoalDate: A dated commitment at its honest resolution. ``precision`` is the band: the date is normalized to the first day of its period (month and season → the 1st of that month, quarter → the quarter's first month), so a coarse band can never license a specific day the source hasn't published — the same normalization the plan rim applies to its events. A season keeps its own start month rather than the quarter holding it (#2945). Seasons do not line up with calendar quarters, and which months one covers depends on the athlete's hemisphere: spring is March in Mexico and September in Chile. The model writes the month it means, so the band runs three months from there; folding it into a quarter kept January and February for a spring and dropped April and May.
      - `date` string, date, required
      - `precision` 'day' | 'month' | 'quarter' | 'season'
    - `recommended` boolean
    - `chosen` boolean
  - `fallbacks` GoalFallbackPayload[]
    - `when` string, required: The athlete's named obstacle.
    - `then` string, required: The athlete's own replacement.
    - `athlete_quote` string, nullable
  - `revisions` GoalShareRevision[]
    - `published_at` string, date-time, required
    - `name` string, required
    - `target_date` string, date, required
    - `target_precision` 'day' | 'month' | 'quarter' | 'season', required
  - `goal_id` string, required
  - `status` 'exploring' | 'proposing' | 'settled', required
  - `share_id` string, nullable
  - `slug` string, nullable
  - `published_at` string, date-time, nullable
  - `claimed_at` string, date-time, nullable
  - `recovered` boolean
  - `custom_token` string, nullable

## Other responses

- `422`: Validation Error

## Changes

- **2026-10-27** `c009be4558a1`: 1 warning, 1 info
  - removed the optional property `events/items/first` from the response with the `200` status
  - added the optional property `events/items/role` to the response with the `200` status
- **2026-10-21** `c98bfec83142`: 1 info
  - added the optional property `offered_goals` to the response with the `200` status
- **2026-10-15** `c746f5978073`: 1 info
  - added the optional property `events` to the response with the `200` status
- **2026-10-06** `2022ce4f3c48`: 2 info
  - added the optional property `anchor/anyOf[subschema #1: GoalAnchorPayload]/date_month` to the response with the `200` status
  - added the optional property `checkpoints/items/anchor/anyOf[subschema #1: GoalAnchorPayload]/date_month` to the response with the `200` status
- **2026-10-05** `60efb6aabb43`: 3 breaking
  - the response property `fallbacks/items/athlete_quote` became optional for the status `200`
  - response property `fallbacks/items/athlete_quote` list-of-types was widened by adding types `null` to media type `application/json` of response `200`
  - the `fallbacks/items/athlete_quote` response property's minLength was decreased from `1` to `0` for the response status `200`

[Operation history](https://skmtc.dev/imperfect/apis/imperfect-api/changes/goals/claim/post.md)

---

[API](https://skmtc.dev/imperfect/apis/imperfect-api.md) - [All operations](https://skmtc.dev/imperfect/apis/imperfect-api/llms.txt) - [OpenAPI document](https://skmtc.dev/imperfect/apis/imperfect-api/revisions/33a8821ec7f8?raw)
