You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(api): populate claim_url on every 201 anon provision response (DOG-21)
DOG-21 — marketing landing page L106-109 promises "the agent surfaces the
claim card directly in chat" with a clickable claim_url, but on the live
201 response /db/new (and every sibling) returned `claim_url: null`.
Agents had to hand-construct `https://api.instanode.dev/start?t=<upgrade_jwt>`
from the upgrade_jwt field — pushing composition responsibility into every
vendor integration and producing different broken links per integration.
Fix: emit `claim_url` alongside `upgrade` / `upgrade_jwt` at every 201
anonymous provision site. Same URL semantically (the /start?t=<jwt> page
handles both claim and upgrade flows); distinct field signals intent so
an agent can render a "Claim with email" CTA without parsing the upgrade
URL from upgrade_jwt.
Surfaces covered (rule 22):
- db.go dedup branch + fresh-201 (2 sites)
- cache.go dedup branch + fresh-201 (2 sites)
- nosql.go dedup branch + fresh-201 (2 sites)
- queue.go dedup branch + fresh-201 (2 sites)
- vector.go dedup branch + fresh-201 (2 sites)
- webhook.go dedup branch + fresh-201 (2 sites)
- storage.go dedup branch + fresh-201 (2 sites)
Total: 14 anon-provision emit sites — all carry claim_url.
- openapi.go all 7 response schemas + ErrorResponse description
widened to cover the 201-anon case (the pre-fix doc had it only on
402 recycle-gate, which is the documentation gap that produced the
original report).
What did NOT change:
- upgrade / upgrade_jwt fields unchanged — back-compat preserved
- Same URL as upgrade — no new endpoint, no new minting
- Authenticated paths unchanged (no anon claim flow there)
Coverage block:
Symptom: claim_url:null on 201 /db/new despite marketing promise
Enumeration: rg -n '"upgrade":[[:space:]]+upgradeURL,' internal/handlers/
Sites found: 14 (7 dedup + 7 fresh-201)
Sites touched: 14 / 14
Coverage test: TestDOG21_ClaimURLEmittedOnEveryAnonProvision iterates the
live anonProvisionHandlerFiles registry (rule 18 — registry-
iterating, not hand-typed) and asserts claim_url count >=
upgrade_jwt count per file; TestDOG21_OpenAPISchemaDocumentsClaimURL
asserts ErrorResponse.claim_url description reflects the new
201 emit (rule 22 surface checklist).
Live verified: pre-merge: see DOG-21 in personal-dogfood-log.md ("claim_url: null
in the response"). post-merge curl below.
Local gate:
- go build ./... PASS
- go vet ./... PASS
- go test -short -run TestOpenAPI ./internal/handlers/ PASS (all 18 pass)
- go test -short -run TestDOG21 ./internal/handlers/ PASS
- go test -short -run Recycle ./internal/handlers/ PASS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: internal/handlers/openapi.go
+15-8Lines changed: 15 additions & 8 deletions
Original file line number
Diff line number
Diff line change
@@ -2644,7 +2644,8 @@ const openAPISpec = `{
2644
2644
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
2645
2645
"note": { "type": "string" },
2646
2646
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email to convert the anonymous resource into a claimed (authenticated) one — no need to string-parse the upgrade URL. Absent on authenticated provisions." },
2647
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL the agent can hand to the user to drive the dashboard claim flow." }
2647
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL the agent can hand to the user to drive the dashboard claim flow." },
2648
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
2648
2649
}
2649
2650
},
2650
2651
"VectorProvisionRequest": {
@@ -2677,7 +2678,8 @@ const openAPISpec = `{
2677
2678
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
2678
2679
"note": { "type": "string" },
2679
2680
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2680
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2681
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2682
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
2681
2683
}
2682
2684
},
2683
2685
"CacheProvisionResponse": {
@@ -2699,7 +2701,8 @@ const openAPISpec = `{
2699
2701
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
2700
2702
"note": { "type": "string" },
2701
2703
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2702
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2704
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2705
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
2703
2706
}
2704
2707
},
2705
2708
"NoSQLProvisionResponse": {
@@ -2720,7 +2723,8 @@ const openAPISpec = `{
2720
2723
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
2721
2724
"note": { "type": "string" },
2722
2725
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2723
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2726
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2727
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
2724
2728
}
2725
2729
},
2726
2730
"QueueProvisionResponse": {
@@ -2754,7 +2758,8 @@ const openAPISpec = `{
2754
2758
"dedicated": { "type": "boolean", "description": "True when the resource was provisioned on dedicated (single-tenant) infrastructure rather than the shared pool." },
2755
2759
"note": { "type": "string" },
2756
2760
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2757
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2761
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2762
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2775
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2780
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2781
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
2776
2782
}
2777
2783
},
2778
2784
"StorageProvisionResponse": {
@@ -2802,7 +2808,8 @@ const openAPISpec = `{
2802
2808
"note": { "type": "string", "description": "Anonymous-tier upgrade hint emitted on the 201 happy path (T19 P1-5, BugHunt 2026-05-20). Was previously undocumented; the schema only listed credentials_note which only appears on the dedup path." },
2803
2809
"credentials_note": { "type": "string", "description": "Present only on the rate-limited anonymous dedup response, where access_key_id/secret_access_key are NOT re-emitted (the secret is minted once at provision time and never stored)." },
2804
2810
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2805
-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2811
+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2812
+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
2806
2813
}
2807
2814
},
2808
2815
"DeployItem": {
@@ -3193,7 +3200,7 @@ const openAPISpec = `{
3193
3200
"retry_after_seconds": { "type": ["integer", "null"], "description": "Seconds the agent should wait before retrying. null on 4xx (no retry — fix the request). int on transient 5xx: 30 for 503, 60 for 429, 10 for 502/504. For 429/502/503/504 the same value is also set in the Retry-After HTTP header." },
3194
3201
"agent_action": { "type": "string", "description": "Optional. A sentence the calling agent should surface verbatim to the human user — e.g. 'Tell the user they've hit the hobby tier storage limit (500MB). Have them upgrade at https://instanode.dev/pricing to provision more storage.' Present on quota walls, invalid-token errors, permission-denied errors, expired-resource errors, tier-gate errors, AND on plumbing 5xx (where it falls back to a generic 'email support with this request_id' sentence)." },
3195
3202
"upgrade_url": { "type": "string", "format": "uri", "description": "Optional. Where the user can resolve the error — typically the pricing/upgrade page for quota walls and the login page for token errors. Present whenever following the URL would clear the error." },
3196
-
"claim_url": { "type": "string", "format": "uri", "description": "Optional. Present specifically on error='free_tier_recycle_requires_claim' (402 from /db/new, /cache/new, /nosql/new, /queue/new, /storage/new, /webhook/new): the URL the anonymous caller should visit to claim their existing resources with email before they can provision again. Distinct from upgrade_url — claim_url is about identity (anonymous → claimed), upgrade_url is about tier (claimed → paid). Both may be present on the same envelope." }
3203
+
"claim_url": { "type": "string", "format": "uri", "description": "Optional. Present on error='free_tier_recycle_requires_claim' (402 from /db/new, /cache/new, /nosql/new, /queue/new, /storage/new, /webhook/new, /vector/new): the URL the anonymous caller should visit to claim their existing resources with email before they can provision again. DOG-21 (QA 2026-05-29): ALSO emitted on every successful 201 anonymous provision response under each service's response schema — agents can surface a claim CTA on first provision instead of waiting for the recycle gate. Distinct from upgrade_url — claim_url is about identity (anonymous → claimed), upgrade_url is about tier (claimed → paid). Both may be present on the same envelope." }
0 commit comments