Skip to content

Add Domain Claims API support (claim + get + verify) - #133

Merged
felipefreitag merged 3 commits into
mainfrom
feat/domain-claims
Jul 24, 2026
Merged

Add Domain Claims API support (claim + get + verify)#133
felipefreitag merged 3 commits into
mainfrom
feat/domain-claims

Conversation

@felipefreitag

@felipefreitag felipefreitag commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Adds SDK support for the newly-released Domain Claims API, mirroring the Node.js SDK:

  • POST /domains/claimDomainClaimAsync(name, region?) and DomainClaimAsync(DomainClaimData)
  • GET /domains/{id}/claimDomainClaimRetrieveAsync(domainId)
  • POST /domains/{id}/claim/verifyDomainClaimVerifyAsync(domainId)

What's included

  • Models: DomainClaim, DomainClaimRecord, DomainClaimData, and the DomainClaimStatus enum (pending/verified/completed/blocked/expired/superseded/canceled/failed).
  • Client: ResendClient.DomainClaims.cs (partial class, matching the OAuth-grants convention) + three methods on IResend, documented in the existing Domains region.
  • CLI: a domain claim create|get|verify subcommand group.
  • Fake API server: three routes so integration tests exercise the wire path.
  • Tests: 5 serialization unit tests (documented payload, +00 space-separated datetimes, a blocked_reason value, an out-of-order / type-last payload, and an unknown blocked_reason) + 4 integration tests against the in-memory server.

Design note: blocked_reason

Modelled as a nullable string, not an enum. The Node SDK uses a string-union, but the API column is free-text on the server, so a JsonStringEnumValueConverter would throw on any unrecognized value and break deserialization of the whole response. A regression test covers an unknown value.

Verification

  • Monorepo (public-api): request/response schemas, nullability of domain_id/region, the 8-value status pgEnum, and the free-text blocked_reason column all cross-checked against origin/main server source.
  • Live staging API: all three endpoints exercised via raw curl and end-to-end through the SDK (CLI); every field mapped, +00 datetimes parsed, test placeholder domain cleaned up.
  • Tests: 131/131 pass on .NET 8 (matching CI); build clean, 0 warnings. No target-framework change (still net8.0).

Summary by cubic

Adds Domain Claims API support to the SDK and CLI. Create a claim, check its status, and trigger verification for domains already verified by another team; also bumps fallback version to 0.7.0.

  • New Features

    • SDK: DomainClaimAsync(name, region?), DomainClaimAsync(DomainClaimData), DomainClaimRetrieveAsync(domainId), DomainClaimVerifyAsync(domainId).
    • Models: DomainClaim, DomainClaimRecord, DomainClaimData, and DomainClaimStatus (pending, verified, completed, blocked, expired, superseded, canceled, failed). blocked_reason is a nullable string to tolerate unknown server values.
    • CLI: domain claim create, domain claim get, domain claim verify.
    • Tests: serialization edge cases and integration tests using in-memory API routes.
  • Bug Fixes

    • CLI renderer now guards null record when printing claims.
    • domain claim create reuses the shared renderer for consistent output.

Written for commit 39ac5f5. Summary will update on new commits.

Review in cubic

Implements the three Domain Claims endpoints:
- POST /domains/claim              -> DomainClaimAsync
- GET  /domains/{id}/claim         -> DomainClaimRetrieveAsync
- POST /domains/{id}/claim/verify  -> DomainClaimVerifyAsync

Adds DomainClaim / DomainClaimRecord / DomainClaimData models and a
DomainClaimStatus enum, a "domain claim create|get|verify" CLI command
group, fake API server routes, and unit + integration tests.

blocked_reason is modelled as a nullable string (not an enum): the API
column is free-text, so an unknown value must not break deserialization.

Verified against the monorepo public-api schema and the live staging API.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 15 files

Confidence score: 3/5

  • In src/Resend/DomainClaimStatus.cs, deserialization currently has no fallback for unknown enum values, so a newly introduced API status could throw JsonException and fail the entire DomainClaim payload for clients — add an unknown/default enum mapping or tolerant converter path to preserve forward compatibility.
  • In tools/Resend.ApiServer/Controllers/DomainController.cs, retrieve and verify handlers duplicate hard-coded claim construction, which raises drift risk where one fake response changes and the other silently diverges — extract a shared claim response factory/helper used by both routes.
  • In tools/Resend.Cli/Domain/Claim/DomainClaimGetCommand.cs and tools/Resend.Cli/Domain/Claim/DomainClaimVerifyCommand.cs, leftover "Placeholder" argument descriptions can confuse CLI users and create inconsistent help text — align both to the existing convention (e.g., "Domain identifier").
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tools/Resend.ApiServer/Controllers/DomainController.cs">

<violation number="1" location="tools/Resend.ApiServer/Controllers/DomainController.cs:212">
P3: The retrieve and verify handlers duplicate the entire hard-coded claim construction, so changes to the fake claim response can easily drift between these routes. A shared response factory/helper would keep the two test endpoints consistent.</violation>
</file>

<file name="src/Resend/DomainClaimStatus.cs">

<violation number="1" location="src/Resend/DomainClaimStatus.cs:9">
P2: No fallback for unknown enum values: if the API introduces a new status, deserialization of the entire `DomainClaim` response throws a `JsonException` (SE002). The `blocked_reason` field on `DomainClaim` was consciously modeled as a nullable string to avoid exactly this failure mode, but the required `status` field uses an enum with no unknown-value handling.</violation>
</file>

<file name="tools/Resend.Cli/Domain/Claim/DomainClaimGetCommand.cs">

<violation number="1" location="tools/Resend.Cli/Domain/Claim/DomainClaimGetCommand.cs:16">
P3: Argument description contains leftover scaffolding text "Placeholder" and doesn't match the established convention. Other domain commands (e.g. `DomainRetrieveCommand`) use `"Domain identifier"`.</violation>
</file>

<file name="tools/Resend.Cli/Domain/Claim/DomainClaimVerifyCommand.cs">

<violation number="1" location="tools/Resend.Cli/Domain/Claim/DomainClaimVerifyCommand.cs:14">
P3: The argument description says "Placeholder domain identifier". "Placeholder" reads like a scaffolding placeholder that was left in. Consider updating it to "Domain identifier" (matching the convention in `DomainRetrieveCommand`).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tools/Resend.Cli/Domain/Claim/DomainClaimRender.cs Outdated
/// Status of a domain claim.
/// </summary>
/// <see href="https://resend.com/docs/dashboard/domains/claim" />
[JsonConverter( typeof( JsonStringEnumValueConverter<DomainClaimStatus> ) )]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: No fallback for unknown enum values: if the API introduces a new status, deserialization of the entire DomainClaim response throws a JsonException (SE002). The blocked_reason field on DomainClaim was consciously modeled as a nullable string to avoid exactly this failure mode, but the required status field uses an enum with no unknown-value handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Resend/DomainClaimStatus.cs, line 9:

<comment>No fallback for unknown enum values: if the API introduces a new status, deserialization of the entire `DomainClaim` response throws a `JsonException` (SE002). The `blocked_reason` field on `DomainClaim` was consciously modeled as a nullable string to avoid exactly this failure mode, but the required `status` field uses an enum with no unknown-value handling.</comment>

<file context>
@@ -0,0 +1,59 @@
+/// Status of a domain claim.
+/// </summary>
+/// <see href="https://resend.com/docs/dashboard/domains/claim" />
+[JsonConverter( typeof( JsonStringEnumValueConverter<DomainClaimStatus> ) )]
+public enum DomainClaimStatus
+{
</file context>

/// <summary />
[HttpPost]
[Route( "domains/{id}/claim/verify" )]
public DomainClaim DomainClaimVerify( [FromRoute] Guid id )

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The retrieve and verify handlers duplicate the entire hard-coded claim construction, so changes to the fake claim response can easily drift between these routes. A shared response factory/helper would keep the two test endpoints consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/Resend.ApiServer/Controllers/DomainController.cs, line 212:

<comment>The retrieve and verify handlers duplicate the entire hard-coded claim construction, so changes to the fake claim response can easily drift between these routes. A shared response factory/helper would keep the two test endpoints consistent.</comment>

<file context>
@@ -148,4 +148,88 @@ public ActionResult DomainDelete( [FromRoute] Guid id )
+    /// <summary />
+    [HttpPost]
+    [Route( "domains/{id}/claim/verify" )]
+    public DomainClaim DomainClaimVerify( [FromRoute] Guid id )
+    {
+        _logger.LogDebug( "DomainClaimVerify" );
</file context>

Comment thread tools/Resend.Cli/Domain/Claim/DomainClaimCreateCommand.cs Outdated


/// <summary />
[Argument( 0, Description = "Placeholder domain identifier" )]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Argument description contains leftover scaffolding text "Placeholder" and doesn't match the established convention. Other domain commands (e.g. DomainRetrieveCommand) use "Domain identifier".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/Resend.Cli/Domain/Claim/DomainClaimGetCommand.cs, line 16:

<comment>Argument description contains leftover scaffolding text "Placeholder" and doesn't match the established convention. Other domain commands (e.g. `DomainRetrieveCommand`) use `"Domain identifier"`.</comment>

<file context>
@@ -0,0 +1,41 @@
+
+
+    /// <summary />
+    [Argument( 0, Description = "Placeholder domain identifier" )]
+    [Required]
+    public Guid? DomainId { get; set; }
</file context>



/// <summary />
[Argument( 0, Description = "Placeholder domain identifier" )]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The argument description says "Placeholder domain identifier". "Placeholder" reads like a scaffolding placeholder that was left in. Consider updating it to "Domain identifier" (matching the convention in DomainRetrieveCommand).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/Resend.Cli/Domain/Claim/DomainClaimVerifyCommand.cs, line 14:

<comment>The argument description says "Placeholder domain identifier". "Placeholder" reads like a scaffolding placeholder that was left in. Consider updating it to "Domain identifier" (matching the convention in `DomainRetrieveCommand`).</comment>

<file context>
@@ -0,0 +1,39 @@
+
+
+    /// <summary />
+    [Argument( 0, Description = "Placeholder domain identifier" )]
+    [Required]
+    public Guid? DomainId { get; set; }
</file context>

@felipefreitag

Copy link
Copy Markdown
Member Author

Thanks @cubic-dev-ai. Triage:

Addressed (commit 39ac5f5):

  • P1 — guarded the record table in DomainClaimRender with a null check. Note: the API's toClaimResponse builds record unconditionally for every status, so it's never actually null on the wire, but the guard is cheap and mirrors DomainRetrieveCommand.
  • P3 (create JSON drift)create now reuses DomainClaimRender (with a --json flag) instead of its own serialization block, matching get/verify.

Declined, with reasoning:

  • P2 (status enum unknown-value handling) — intentional. blocked_reason is a free-text column on the API, so it's modeled as string. status is a closed pgEnum (verified against public-api), so a C# enum is the faithful mapping — and every enum in this SDK (DomainStatus, EmailStatus, …) uses the same throwing converter. Making only this one tolerant would break SDK-wide consistency.
  • P3 (fake controller duplication) — the existing DomainController already duplicates Domain construction across add/retrieve; the new claim handlers follow that established test-server style.
  • P3 ("Placeholder" text) — kept. The API docs call this "the placeholder Domain ID returned when the claim was created", so it's precise terminology rather than leftover scaffolding.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Thanks @cubic-dev-ai. Triage:

Addressed (commit 39ac5f5):

  • P1 — guarded the record table in DomainClaimRender with a null check. Note: the API's toClaimResponse builds record unconditionally for every status, so it's never actually null on the wire, but the guard is cheap and mirrors DomainRetrieveCommand.
  • P3 (create JSON drift)create now reuses DomainClaimRender (with a --json flag) instead of its own serialization block, matching get/verify.
    ...

@felipefreitag Triage looks solid. Quick notes on each:

P1 (null guard) — even if the API always returns record, defensive code in a presentation layer doesn't cost anything and prevents a hypothetical NRE. Good call mirroring DomainRetrieveCommand.

P3 (create JSON drift) — reusing DomainClaimRender is the right outcome; keeps the CLI consistent across all three subcommands.

P2 (status enum) — your reasoning is airtight. Making DomainClaimStatus tolerant while DomainStatus, EmailStatus, etc. all throw would be the kind of inconsistency that bites someone later. The string approach for blocked_reason is exactly right too — free-text columns and closed enums should be handled differently, and you've drawn that line cleanly.

P3 (fake controller duplication) — following the existing test-server style is pragmatic. Refactoring that out of scope is correct.

P3 ("Placeholder") — that's the API's own language, so it's precise, not scaffolding. No reason to swap it for something vague.

Nothing to push back on here. Everything looks consistent and intentional.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants