Skip to content

Add TeamAPI and resource classes for team-level management#10

Merged
delano merged 4 commits intomainfrom
feature/9-complete-team-api-resources
Apr 2, 2026
Merged

Add TeamAPI and resource classes for team-level management#10
delano merged 4 commits intomainfrom
feature/9-complete-team-api-resources

Conversation

@delano
Copy link
Copy Markdown
Member

@delano delano commented Apr 2, 2026

Summary

This PR introduces a major architectural enhancement that splits the SDK into two specialized API clients with distinct authentication schemes:

  • SendingAPI: Project-level email sending (uses x-lettermint-token header)
  • TeamAPI: Team-level management operations (uses Authorization: Bearer with lm_team_* tokens)

The existing Client class becomes an alias to SendingAPI, maintaining full backward compatibility.

Changes

New API Clients:

  • SendingAPI extracted from the original Client implementation
  • TeamAPI for team management with strict lm_team_* token validation
  • HttpClient now supports configurable auth_scheme parameter

Resource Classes (all inherit from Resources::Base):

  • Team - team settings, usage stats, member management
  • Domains - CRUD, DNS verification, project association
  • Projects - CRUD, token rotation, nested routes access
  • Routes - transactional/broadcast route management
  • Webhooks - endpoint management with test/regenerate-secret
  • Messages - history search with events/source/html/text sub-resources
  • Suppressions - bounce/unsubscribe list management
  • Stats - team and project-level statistics

Infrastructure:

  • Resources::Base with shared pagination (page[size], page[cursor]) and filter helpers
  • Consistent list/find/create/update/delete patterns across resources

Test Coverage

253 examples, 0 failures across 5 new spec files:

  • sending_api_spec.rb - token validation, auth headers, HTTP methods, config, Client alias
  • team_api_spec.rb - strict lm_team_* validation, Bearer auth, resource accessors
  • resources/base_spec.rb - pagination/sort/filter parameter building
  • resources/team_spec.rb - all endpoint methods
  • resources/domains_spec.rb - full CRUD, DNS verification, project association

Backward Compatibility

  • Lettermint::Client remains functional (alias to SendingAPI)
  • All existing method signatures unchanged
  • No breaking changes for current users

Usage

# Existing email sending (unchanged)
client = Lettermint::Client.new(api_token: 'proj_token')
client.email.from('a@b.com').to('c@d.com').subject('Hi').html('<p>Hello</p>').deliver

# New team management
team = Lettermint::TeamAPI.new(team_token: 'lm_team_xxx')
team.domains.list(status: 'verified')
team.projects.find('uuid').routes.list
team.team.usage

Closes #9

delano added 2 commits April 1, 2026 21:09
- Add auth_scheme parameter to HttpClient for project/team token auth
- Extract SendingAPI from Client for project-level email sending
- Add TeamAPI for team-level management operations with Bearer auth
- Add Resources::Base with shared pagination/filter helpers
- Add Resources::Team for /team endpoints
- Add Resources::Domains with full CRUD and DNS verification
- Add stub files for remaining resources (projects, routes, etc.)
- Simplify Client to alias SendingAPI for backward compatibility
- Update require chain in lettermint.rb
- SendingAPI specs: token validation, x-lettermint-token auth, HTTP
  methods, configuration, error propagation, Client alias backward compat
- TeamAPI specs: lm_team_* token format validation, Bearer auth header,
  resource accessor methods
- Resources::Base specs: build_params helper for pagination, sort, filters
- Resources::Team specs: get, update, usage, members endpoints
- Resources::Domains specs: full CRUD, DNS verification, project association

253 examples, 0 failures
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request significantly expands the Lettermint Ruby SDK by introducing a comprehensive suite of resource classes to support the Team API, including Domains, Messages, Projects, Routes, Stats, Suppressions, Team, and Webhooks. The architecture is refactored to distinguish between SendingAPI and TeamAPI, accommodating different authentication schemes while maintaining backward compatibility for the existing client. Review feedback highlights the need for explicit Accept headers on non-JSON endpoints in the Messages resource, suggests more robust handling of authentication symbols, and recommends refactoring the parameter-building logic to be more idiomatic.

# Get raw message source (RFC822 format).
# @param id [String] Message ID
# @return [String] Raw message source (message/rfc822)
def source(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.

high

The HttpClient defaults to Accept: application/json. Since this endpoint returns raw RFC822 message source, you should explicitly set the Accept header to message/rfc822 to ensure the server provides the correct content type and the client handles it as a raw string.

      def source(id)
        @http_client.get(path: "/messages/#{id}/source", headers: { 'Accept' => 'message/rfc822' })
      end

# Get message HTML body.
# @param id [String] Message ID
# @return [String] HTML content (text/html)
def html(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.

high

The HttpClient defaults to Accept: application/json. Since this endpoint returns HTML content, you should explicitly set the Accept header to text/html.

      def html(id)
        @http_client.get(path: "/messages/#{id}/html", headers: { 'Accept' => 'text/html' })
      end

# Get message plain text body.
# @param id [String] Message ID
# @return [String] Plain text content (text/plain)
def text(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.

high

The HttpClient defaults to Accept: application/json. Since this endpoint returns plain text content, you should explicitly set the Accept header to text/plain.

      def text(id)
        @http_client.get(path: "/messages/#{id}/text", headers: { 'Accept' => 'text/plain' })
      end

Comment on lines +60 to +66
def auth_headers(token, scheme)
case scheme
when :project then { 'x-lettermint-token' => token }
when :team then { 'Authorization' => "Bearer #{token}" }
else raise ArgumentError, "Unknown auth_scheme: #{scheme}"
end
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The scheme parameter is compared directly in the case statement. To make the client more robust against callers passing strings instead of symbols, it is safer to convert the scheme to a symbol before comparison.

    def auth_headers(token, scheme)
      case scheme.to_sym
      when :project then { 'x-lettermint-token' => token }
      when :team    then { 'Authorization' => "Bearer #{token}" }
      else raise ArgumentError, "Unknown auth_scheme: #{scheme}"
      end
    end

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a split-client SDK architecture by extracting the existing project-scoped sending client into Lettermint::SendingAPI and adding a new Lettermint::TeamAPI for team-scoped management endpoints, with Lettermint::Client retained as a backward-compatible alias. It also adds a set of TeamAPI resource classes with shared pagination/filter helpers and updates HttpClient to support configurable authentication schemes.

Changes:

  • Add SendingAPI (project token via x-lettermint-token) and TeamAPI (team token via Authorization: Bearer), with Client aliased to SendingAPI.
  • Add Resources::Base and multiple TeamAPI resource classes (Team, Domains, Projects, Routes, Webhooks, Messages, Suppressions, Stats).
  • Extend HttpClient with auth_scheme to switch header behavior, and add RSpec coverage for the new clients + some resources.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
spec/lettermint/team_api_spec.rb Adds TeamAPI initialization/auth header/accessor/error specs
spec/lettermint/sending_api_spec.rb Adds SendingAPI initialization/auth header/http delegation + Client alias spec
spec/lettermint/resources/team_spec.rb Adds Team resource endpoint specs
spec/lettermint/resources/domains_spec.rb Adds Domains resource CRUD + verification/association specs
spec/lettermint/resources/base_spec.rb Adds specs for shared pagination/sort/filter param builder
lib/lettermint/team_api.rb Introduces TeamAPI client and resource accessors
lib/lettermint/sending_api.rb Introduces SendingAPI client extracted from prior Client
lib/lettermint/resources/base.rb Adds shared query param building helper for resources
lib/lettermint/resources/team.rb Adds Team endpoints (get/update/usage/members)
lib/lettermint/resources/domains.rb Adds Domains endpoints (list/create/find/delete/verify/update_projects)
lib/lettermint/resources/projects.rb Adds Projects endpoints + nested routes accessor
lib/lettermint/resources/routes.rb Adds Routes endpoints (scoped list/create + direct CRUD/verify)
lib/lettermint/resources/webhooks.rb Adds Webhooks endpoints + deliveries access
lib/lettermint/resources/messages.rb Adds Messages endpoints + sub-resources (events/source/html/text)
lib/lettermint/resources/suppressions.rb Adds Suppressions list/create/delete
lib/lettermint/resources/stats.rb Adds Stats endpoint wrapper
lib/lettermint/http_client.rb Adds auth_scheme + auth header selection helper
lib/lettermint/client.rb Replaces old Client implementation with alias to SendingAPI
lib/lettermint.rb Updates require chain to load new clients/resources

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +37
# Get project details.
# @param id [String] Project ID
# @param include [String, nil] Related data: routes, domains, teamMembers, messageStats (+ Count/Exists variants)
# @return [Hash] Project data with optional includes
def find(id, include: nil)
params = include ? { 'include' => include } : nil
@http_client.get(path: "/projects/#{id}", params: params)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

PR description shows chaining team.projects.find('uuid').routes.list, but Projects#find returns a parsed Hash, so that call chain will fail. If the intended fluent API is projects.find(id).routes, consider returning a resource object from find (or adding a project(id) wrapper that exposes nested resources); otherwise update docs/examples to use team.projects.routes('uuid').list.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
# @return [Hash] Stats data with totals and daily breakdown
def get(from:, to:, project_id: nil)
params = { 'from' => from, 'to' => to }
params['project_id'] = project_id if project_id
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Resources::Stats#get only supports project_id, but the PR description / Issue #9 call out stats filters for date + project + route. If the API supports route-level stats, consider adding an optional route_id (and/or route_ids) parameter and including it in the query params so consumers can retrieve route-scoped stats as advertised.

Suggested change
# @return [Hash] Stats data with totals and daily breakdown
def get(from:, to:, project_id: nil)
params = { 'from' => from, 'to' => to }
params['project_id'] = project_id if project_id
# @param route_id [String, nil] Filter by a single route ID
# @param route_ids [Array<String>, String, nil] Filter by multiple route IDs
# @return [Hash] Stats data with totals and daily breakdown
def get(from:, to:, project_id: nil, route_id: nil, route_ids: nil)
params = { 'from' => from, 'to' => to }
params['project_id'] = project_id if project_id
params['route_id'] = route_id if route_id
params['route_ids'] = route_ids if route_ids

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +29
class Webhooks < Base
# List all webhooks.
# @param page_size [Integer, nil] Number of items per page (default: 30)
# @param page_cursor [String, nil] Cursor for pagination
# @param sort [String, nil] Sort field: name, url, created_at (prefix - for desc)
# @param enabled [Boolean, nil] Filter by enabled status
# @param event [String, nil] Filter by event type
# @param route_id [String, nil] Filter by route ID
# @param search [String, nil] Search filter
# @return [Hash] Paginated list of webhooks
# rubocop:disable Metrics/ParameterLists
def list(page_size: nil, page_cursor: nil, sort: nil, enabled: nil,
event: nil, route_id: nil, search: nil)
params = build_params(
page_size: page_size,
page_cursor: page_cursor,
sort: sort,
enabled: enabled,
event: event,
route_id: route_id,
search: search
)
@http_client.get(path: '/webhooks', params: params)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

This resource introduces multiple endpoints (list/create/find/update/delete/test/regenerate_secret/deliveries) but there are no dedicated specs exercising these methods (only an accessor check in team_api_spec). Add a spec/lettermint/resources/webhooks_spec.rb similar to domains_spec.rb/team_spec.rb to verify paths, query params, and error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +40
class Messages < Base
# List messages.
# @param page_size [Integer, nil] Number of items per page (default: 30)
# @param page_cursor [String, nil] Cursor for pagination
# @param sort [String, nil] Sort field: type, status, from_email, subject, created_at, status_changed_at
# @param type [String, nil] Filter: inbound, outbound
# @param status [String, nil] Filter by status
# @param route_id [String, nil] Filter by route ID
# @param domain_id [String, nil] Filter by domain ID
# @param tag [String, nil] Filter by tag
# @param from_email [String, nil] Filter by sender email
# @param subject [String, nil] Filter by subject
# @param from_date [String, nil] Filter from date (Y-m-d)
# @param to_date [String, nil] Filter to date (Y-m-d)
# @return [Hash] Paginated list of messages
# rubocop:disable Metrics/ParameterLists
def list(page_size: nil, page_cursor: nil, sort: nil, type: nil, status: nil,
route_id: nil, domain_id: nil, tag: nil, from_email: nil, subject: nil,
from_date: nil, to_date: nil)
params = build_params(
page_size: page_size,
page_cursor: page_cursor,
sort: sort,
type: type,
status: status,
route_id: route_id,
domain_id: domain_id,
tag: tag,
from_email: from_email,
subject: subject,
from_date: from_date,
to_date: to_date
)
@http_client.get(path: '/messages', params: params)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

This resource adds several endpoints (list, find, events, source, html, text) but there are no specs covering the request paths/params or the expected response parsing. Add a spec/lettermint/resources/messages_spec.rb to validate each endpoint and content-type handling (JSON vs plain text).

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +28
class Projects < Base
# List all projects.
# @param page_size [Integer, nil] Number of items per page (default: 30)
# @param page_cursor [String, nil] Cursor for pagination
# @param sort [String, nil] Sort field: name, created_at (prefix - for desc)
# @param search [String, nil] Search filter
# @return [Hash] Paginated list of projects
def list(page_size: nil, page_cursor: nil, sort: nil, search: nil)
params = build_params(page_size: page_size, page_cursor: page_cursor, sort: sort, search: search)
@http_client.get(path: '/projects', params: params)
end

# Create a new project.
# @param name [String] Project name (max 255 chars)
# @param smtp_enabled [Boolean, nil] Enable SMTP (default: false)
# @param initial_routes [String, nil] Initial routes: both, transactional, broadcast (default: both)
# @return [Hash] Created project data including api_token
def create(name:, smtp_enabled: nil, initial_routes: nil)
data = { name: name }
data[:smtp_enabled] = smtp_enabled unless smtp_enabled.nil?
data[:initial_routes] = initial_routes if initial_routes
@http_client.post(path: '/projects', data: data)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Resources::Projects adds CRUD, token rotation, member management, and a nested routes(project_id) accessor, but there are no resource-level specs validating the HTTP calls. Add a spec/lettermint/resources/projects_spec.rb to cover each method (paths, request bodies, and error propagation).

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +38
# List routes for a project.
# Requires project_id to be set (via constructor or projects.routes(id)).
# @param page_size [Integer, nil] Number of items per page (default: 30)
# @param page_cursor [String, nil] Cursor for pagination
# @param sort [String, nil] Sort field: name, slug, created_at (prefix - for desc)
# @param route_type [String, nil] Filter: transactional, broadcast, inbound
# @param is_default [Boolean, nil] Filter by default route status
# @param search [String, nil] Search filter
# @return [Hash] Paginated list of routes
# rubocop:disable Metrics/ParameterLists
def list(page_size: nil, page_cursor: nil, sort: nil, route_type: nil,
is_default: nil, search: nil)
raise ArgumentError, 'project_id required for listing routes' unless @project_id

params = build_params(
page_size: page_size,
page_cursor: page_cursor,
sort: sort,
route_type: route_type,
is_default: is_default,
search: search
)
@http_client.get(path: "/projects/#{@project_id}/routes", params: params)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Resources::Routes includes project-scoped methods (list, create) plus direct route methods (find, update, delete, verify_inbound_domain), but there are no specs confirming the correct endpoints and the project_id required guard behavior. Add a spec/lettermint/resources/routes_spec.rb to cover both scoped and unscoped usage.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +31
class Suppressions < Base
# List suppressions.
# @param page_size [Integer, nil] Number of items per page (default: 30)
# @param page_cursor [String, nil] Cursor for pagination
# @param sort [String, nil] Sort field: value, created_at, reason
# @param scope [String, nil] Filter: team, project, route
# @param route_id [String, nil] Filter by route ID
# @param project_id [String, nil] Filter by project ID
# @param value [String, nil] Filter by suppression value (email/domain/extension)
# @param reason [String, nil] Filter: spam_complaint, hard_bounce, unsubscribe, manual
# @return [Hash] Paginated list of suppressions
# rubocop:disable Metrics/ParameterLists
def list(page_size: nil, page_cursor: nil, sort: nil, scope: nil,
route_id: nil, project_id: nil, value: nil, reason: nil)
params = build_params(
page_size: page_size,
page_cursor: page_cursor,
sort: sort,
scope: scope,
route_id: route_id,
project_id: project_id,
value: value,
reason: reason
)
@http_client.get(path: '/suppressions', params: params)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Resources::Suppressions adds list/create/delete behavior with several query/body options, but there are no specs validating parameter mapping and request bodies. Add a spec/lettermint/resources/suppressions_spec.rb similar to domains_spec.rb to ensure filters/pagination and creation payloads are encoded correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +16
# Stats resource for retrieving email statistics.
class Stats < Base
# Get statistics for a date range.
# @param from [String] Start date (Y-m-d format, required)
# @param to [String] End date (Y-m-d format, required, max 90 days from start)
# @param project_id [String, nil] Filter by project ID
# @return [Hash] Stats data with totals and daily breakdown
def get(from:, to:, project_id: nil)
params = { 'from' => from, 'to' => to }
params['project_id'] = project_id if project_id
@http_client.get(path: '/stats', params: params)
end
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Resources::Stats#get is a new API surface area but there are no specs covering the query param construction (from, to, optional project_id) or error propagation. Add a spec/lettermint/resources/stats_spec.rb to lock in the endpoint contract.

Copilot uses AI. Check for mistakes.
- Add Accept headers for non-JSON endpoints (messages source/html/text)
- Add route_id/route_ids params to Stats#get per Issue #9
- Refactor build_params with compact, use consistently in domains
- Add .to_sym to auth_headers for robustness
- Fix YARD doc return type for TeamAPI#ping
- Fix hash indentation in team_spec.rb
- Add clarifying comment about Projects#find return type
- Add comprehensive specs for webhooks, messages, projects, routes,
  suppressions, and stats resources (193 new examples)

446 examples, 0 failures
@delano delano merged commit 73696b9 into main Apr 2, 2026
6 checks passed
@delano delano deleted the feature/9-complete-team-api-resources branch April 2, 2026 04:53
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.

Complete TeamAPI resource classes and require chain

2 participants