diff --git a/.envrc b/.envrc
new file mode 100644
index 00000000..8b808bbd
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,4 @@
+export MAILTRAP_ACCOUNT_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_id"
+export MAILTRAP_ORGANIZATION_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_id"
+export MAILTRAP_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_api_token"
+export MAILTRAP_ORGANIZATION_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_api_token"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36ee03d0..dcff975d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [Unreleased]
+
+### Features
+
+- **API Tokens API** — Added full CRUD support:
+ - `IAccountResource.ApiTokens().GetAll()` — `GET /api/accounts/{account_id}/api_tokens`
+ - `IAccountResource.ApiTokens().Create(request)` — `POST /api/accounts/{account_id}/api_tokens`
+ - `IAccountResource.ApiToken(id).GetDetails()` — `GET /api/accounts/{account_id}/api_tokens/{id}`
+ - `IAccountResource.ApiToken(id).Delete()` — `DELETE /api/accounts/{account_id}/api_tokens/{id}`
+ - `IAccountResource.ApiToken(id).Reset()` — `POST /api/accounts/{account_id}/api_tokens/{id}/reset`
+- **Organizations API** — Added new top-level `IMailtrapOrganizationClient` (spawned via `MailtrapClientFactory.CreateOrganizationClient()`) for organization-scoped operations:
+ - `IOrganizationResource.SubAccounts().GetAll()` — `GET /api/organizations/{organization_id}/sub_accounts`
+ - `IOrganizationResource.SubAccounts().Create(request)` — `POST /api/organizations/{organization_id}/sub_accounts`
+
## [3.1.1] - 2026-03-30
### Fixes & Maintenance
diff --git a/Mailtrap.sln b/Mailtrap.sln
index 79588b51..89218e89 100644
--- a/Mailtrap.sln
+++ b/Mailtrap.sln
@@ -110,6 +110,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.Stats", "e
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.EmailLogs", "examples\Mailtrap.Example.EmailLogs\Mailtrap.Example.EmailLogs.csproj", "{C4F8B2D3-5E6A-7F9B-0C1D-2E3F4A5B6C7D}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.ApiTokens", "examples\Mailtrap.Example.ApiTokens\Mailtrap.Example.ApiTokens.csproj", "{D5E6F7A8-1234-5678-9ABC-DEF012345601}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.Webhooks", "examples\Mailtrap.Example.Webhooks\Mailtrap.Example.Webhooks.csproj", "{D5E6F7A8-1234-5678-9ABC-DEF012345602}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mailtrap.Example.SubAccount", "examples\Mailtrap.Example.SubAccount\Mailtrap.Example.SubAccount.csproj", "{D5E6F7A8-1234-5678-9ABC-DEF012345603}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -228,6 +234,18 @@ Global
{C4F8B2D3-5E6A-7F9B-0C1D-2E3F4A5B6C7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4F8B2D3-5E6A-7F9B-0C1D-2E3F4A5B6C7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4F8B2D3-5E6A-7F9B-0C1D-2E3F4A5B6C7D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345601}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345601}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345602}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345602}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345602}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345603}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5E6F7A8-1234-5678-9ABC-DEF012345603}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -261,6 +279,9 @@ Global
{11894B20-F780-4FC9-8140-3601EBF54C10} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{B3E7A1C2-4D5F-6E8A-9B0C-1D2E3F4A5B6C} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
{C4F8B2D3-5E6A-7F9B-0C1D-2E3F4A5B6C7D} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
+ {D5E6F7A8-1234-5678-9ABC-DEF012345601} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
+ {D5E6F7A8-1234-5678-9ABC-DEF012345602} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
+ {D5E6F7A8-1234-5678-9ABC-DEF012345603} = {09E18837-1DDE-4EAF-80EC-DA55557C81EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0FF614CC-FEBC-4C66-B3FC-FCB73EE511D7}
diff --git a/README.md b/README.md
index e7c90b9c..710e8c8e 100644
--- a/README.md
+++ b/README.md
@@ -276,6 +276,7 @@ private static SendEmailRequest TemplateBasedRequest()
- Suppressions management – [`examples/Mailtrap.Example.Suppression`](examples/Mailtrap.Example.Suppression/)
- Email logs (list with filters and pagination; get message details with events) – [`examples/Mailtrap.Example.EmailLogs`](examples/Mailtrap.Example.EmailLogs/)
- Email sending statistics – [`examples/Mailtrap.Example.Stats`](examples/Mailtrap.Example.Stats/)
+- Webhooks management – [`examples/Mailtrap.Example.Webhooks`](examples/Mailtrap.Example.Webhooks/)
### Email Sandbox (Testing)
@@ -301,6 +302,8 @@ private static SendEmailRequest TemplateBasedRequest()
- Account access management – [`examples/Mailtrap.Example.AccountAccess`](examples/Mailtrap.Example.AccountAccess/)
- Permissions management – [`examples/Mailtrap.Example.Permissions`](examples/Mailtrap.Example.Permissions/)
- Accounts management – [`examples/Mailtrap.Example.Account`](examples/Mailtrap.Example.Account/)
+- Sub accounts management – [`examples/Mailtrap.Example.SubAccount`](examples/Mailtrap.Example.SubAccount/)
+- API tokens management – [`examples/Mailtrap.Example.ApiTokens`](examples/Mailtrap.Example.ApiTokens/)
- Billing information – [`examples/Mailtrap.Example.Billing`](examples/Mailtrap.Example.Billing/)
- Comprehensive API Usage – [`examples/Mailtrap.Example.ApiUsage`](examples/Mailtrap.Example.ApiUsage/)
diff --git a/examples/Mailtrap.Example.ApiTokens/Mailtrap.Example.ApiTokens.csproj b/examples/Mailtrap.Example.ApiTokens/Mailtrap.Example.ApiTokens.csproj
new file mode 100644
index 00000000..b4b1d3ea
--- /dev/null
+++ b/examples/Mailtrap.Example.ApiTokens/Mailtrap.Example.ApiTokens.csproj
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/Mailtrap.Example.ApiTokens/Program.cs b/examples/Mailtrap.Example.ApiTokens/Program.cs
new file mode 100644
index 00000000..5fee8ac6
--- /dev/null
+++ b/examples/Mailtrap.Example.ApiTokens/Program.cs
@@ -0,0 +1,83 @@
+using Mailtrap;
+using Mailtrap.Accounts;
+using Mailtrap.ApiTokens;
+using Mailtrap.ApiTokens.Models;
+using Mailtrap.ApiTokens.Requests;
+using Mailtrap.ApiTokens.Responses;
+using Mailtrap.Core.Models;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+
+HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(args);
+
+IConfigurationSection config = hostBuilder.Configuration.GetSection("Mailtrap");
+
+hostBuilder.Services.AddMailtrapClient(config);
+
+using IHost host = hostBuilder.Build();
+
+ILogger logger = host.Services.GetRequiredService>();
+IMailtrapClient mailtrapClient = host.Services.GetRequiredService();
+
+try
+{
+ var accountId = 12345;
+ var inboxId = 67890;
+ IAccountResource accountResource = mailtrapClient.Account(accountId);
+
+ // Get resource for API tokens collection
+ IApiTokenCollectionResource apiTokensResource = accountResource.ApiTokens();
+
+ // List all API tokens visible to the current API token
+ IList apiTokens = await apiTokensResource.GetAll();
+ logger.LogInformation("Found {Count} API token(s).", apiTokens.Count);
+
+ // Create a new API token scoped to a specific inbox with Viewer access
+ var createRequest = new CreateApiTokenRequest
+ {
+ Name = "Demo Viewer Token"
+ };
+ createRequest.Resources.Add(new ApiTokenAccessRequest(ResourceType.Inbox, inboxId, AccessLevel.Viewer));
+
+ CreateApiTokenResponse createdToken = await apiTokensResource.Create(createRequest);
+
+ // The full token value is only returned at creation time - store it securely
+ logger.LogInformation(
+ "Created API Token: Id={Id}, Name={Name}, Last4={Last4}",
+ createdToken.Id,
+ createdToken.Name,
+ createdToken.Last4Digits);
+ logger.LogInformation("Full token value (store securely, returned only once): {Token}", createdToken.Token);
+
+ // Get resource for the specific API token
+ IApiTokenResource apiTokenResource = accountResource.ApiToken(createdToken.Id);
+
+ // Get details of the API token
+ ApiToken tokenDetails = await apiTokenResource.GetDetails();
+ logger.LogInformation("Token details: Id={Id}, Name={Name}, CreatedBy={CreatedBy}",
+ tokenDetails.Id,
+ tokenDetails.Name,
+ tokenDetails.CreatedBy);
+
+ // Reset the API token - expires the current token and returns a new one with the same permissions
+ ApiTokenResetResponse resetResponse = await apiTokenResource.Reset();
+ logger.LogInformation(
+ "Reset API Token: Id={Id}, NewLast4={Last4}",
+ resetResponse.Id,
+ resetResponse.Last4Digits);
+ logger.LogInformation("New token value (store securely, returned only once): {Token}", resetResponse.Token);
+
+ // Delete the API token
+ // The API token resource becomes invalid after deletion and should not be used anymore
+ await apiTokenResource.Delete();
+ logger.LogInformation("API Token Deleted.");
+}
+catch (Exception ex)
+{
+ logger.LogError(ex, "An error occurred during API call.");
+ Environment.ExitCode = 1;
+ return;
+}
diff --git a/examples/Mailtrap.Example.ApiTokens/Properties/launchSettings.json b/examples/Mailtrap.Example.ApiTokens/Properties/launchSettings.json
new file mode 100644
index 00000000..b8daf491
--- /dev/null
+++ b/examples/Mailtrap.Example.ApiTokens/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Project": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/Mailtrap.Example.ApiTokens/appsettings.json b/examples/Mailtrap.Example.ApiTokens/appsettings.json
new file mode 100644
index 00000000..7cf4089d
--- /dev/null
+++ b/examples/Mailtrap.Example.ApiTokens/appsettings.json
@@ -0,0 +1,17 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "System": "Warning",
+ "Microsoft": "Warning"
+ },
+ "Debug": {
+ "LogLevel": {
+ "Default": "Debug"
+ }
+ }
+ },
+ "Mailtrap": {
+ "ApiToken": ""
+ }
+}
diff --git a/examples/Mailtrap.Example.SubAccount/Mailtrap.Example.SubAccount.csproj b/examples/Mailtrap.Example.SubAccount/Mailtrap.Example.SubAccount.csproj
new file mode 100644
index 00000000..6b512ec9
--- /dev/null
+++ b/examples/Mailtrap.Example.SubAccount/Mailtrap.Example.SubAccount.csproj
@@ -0,0 +1 @@
+
diff --git a/examples/Mailtrap.Example.SubAccount/Program.cs b/examples/Mailtrap.Example.SubAccount/Program.cs
new file mode 100644
index 00000000..1f3c533b
--- /dev/null
+++ b/examples/Mailtrap.Example.SubAccount/Program.cs
@@ -0,0 +1,61 @@
+using Mailtrap;
+using Mailtrap.Organizations;
+using Mailtrap.Organizations.Models;
+using Mailtrap.Organizations.Requests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+
+HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(args);
+
+IConfigurationSection config = hostBuilder.Configuration.GetSection("Mailtrap");
+
+hostBuilder.Services.AddMailtrapClient(config);
+
+using IHost host = hostBuilder.Build();
+
+ILogger logger = host.Services.GetRequiredService>();
+
+// Sub accounts live under /api/organizations/{organization_id}/... so they are
+// accessed via the organization-scoped client, not IMailtrapClient.
+IMailtrapOrganizationClient organizationClient = host.Services.GetRequiredService();
+
+try
+{
+ var organizationId = 12345;
+ IOrganizationResource organizationResource = organizationClient.Organization(organizationId);
+
+ // Get resource for sub accounts collection
+ IOrganizationSubAccountCollectionResource subAccountsResource = organizationResource.SubAccounts();
+
+ // List sub accounts of the organization
+ IList subAccounts = await subAccountsResource.GetAll();
+ logger.LogInformation("Found {Count} sub account(s).", subAccounts.Count);
+
+ foreach (SubAccount subAccount in subAccounts)
+ {
+ logger.LogInformation("Sub Account: Id={Id}, Name={Name}", subAccount.Id, subAccount.Name);
+ }
+
+ // Create a new sub account under the organization
+ var createRequest = new CreateSubAccountRequest
+ {
+ Account = new SubAccountAttributes
+ {
+ Name = "Demo Sub Account"
+ }
+ };
+ SubAccount createdSubAccount = await subAccountsResource.Create(createRequest);
+ logger.LogInformation(
+ "Created Sub Account: Id={Id}, Name={Name}",
+ createdSubAccount.Id,
+ createdSubAccount.Name);
+}
+catch (Exception ex)
+{
+ logger.LogError(ex, "An error occurred during API call.");
+ Environment.ExitCode = 1;
+ return;
+}
diff --git a/examples/Mailtrap.Example.SubAccount/Properties/launchSettings.json b/examples/Mailtrap.Example.SubAccount/Properties/launchSettings.json
new file mode 100644
index 00000000..b8daf491
--- /dev/null
+++ b/examples/Mailtrap.Example.SubAccount/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Project": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/Mailtrap.Example.SubAccount/appsettings.json b/examples/Mailtrap.Example.SubAccount/appsettings.json
new file mode 100644
index 00000000..7cf4089d
--- /dev/null
+++ b/examples/Mailtrap.Example.SubAccount/appsettings.json
@@ -0,0 +1,17 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "System": "Warning",
+ "Microsoft": "Warning"
+ },
+ "Debug": {
+ "LogLevel": {
+ "Default": "Debug"
+ }
+ }
+ },
+ "Mailtrap": {
+ "ApiToken": ""
+ }
+}
diff --git a/examples/Mailtrap.Example.Webhooks/Mailtrap.Example.Webhooks.csproj b/examples/Mailtrap.Example.Webhooks/Mailtrap.Example.Webhooks.csproj
new file mode 100644
index 00000000..6b512ec9
--- /dev/null
+++ b/examples/Mailtrap.Example.Webhooks/Mailtrap.Example.Webhooks.csproj
@@ -0,0 +1 @@
+
diff --git a/examples/Mailtrap.Example.Webhooks/Program.cs b/examples/Mailtrap.Example.Webhooks/Program.cs
new file mode 100644
index 00000000..ebc98eed
--- /dev/null
+++ b/examples/Mailtrap.Example.Webhooks/Program.cs
@@ -0,0 +1,92 @@
+using Mailtrap;
+using Mailtrap.Accounts;
+using Mailtrap.Webhooks;
+using Mailtrap.Core.Models;
+using Mailtrap.Webhooks.Models;
+using Mailtrap.Webhooks.Requests;
+using Mailtrap.Webhooks.Responses;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+
+HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(args);
+
+IConfigurationSection config = hostBuilder.Configuration.GetSection("Mailtrap");
+
+hostBuilder.Services.AddMailtrapClient(config);
+
+using IHost host = hostBuilder.Build();
+
+ILogger logger = host.Services.GetRequiredService>();
+IMailtrapClient mailtrapClient = host.Services.GetRequiredService();
+
+try
+{
+ var accountId = 12345;
+ IAccountResource accountResource = mailtrapClient.Account(accountId);
+
+ // Get resource for webhooks collection
+ IWebhookCollectionResource webhooksResource = accountResource.Webhooks();
+
+ // List all webhooks for the account
+ IList webhooks = await webhooksResource.GetAll();
+ logger.LogInformation("Found {Count} webhook(s).", webhooks.Count);
+
+ // Create a new "email_sending" webhook subscribed to delivery + bounce events on the transactional stream
+ var createRequest = new CreateWebhookRequest
+ {
+ Url = new Uri("https://example.com/webhooks/mailtrap"),
+ WebhookType = WebhookType.EmailSending,
+ Active = true,
+ PayloadFormat = WebhookPayloadFormat.Json,
+ SendingStream = SendingStream.Transactional
+ };
+ createRequest.EventTypes.Add(WebhookEventType.Delivery);
+ createRequest.EventTypes.Add(WebhookEventType.Bounce);
+
+ CreateWebhookResponse createdWebhook = await webhooksResource.Create(createRequest);
+
+ // The signing secret is only returned at creation time - store it securely
+ logger.LogInformation(
+ "Created Webhook: Id={Id}, Url={Url}, Type={Type}",
+ createdWebhook.Id,
+ createdWebhook.Url,
+ createdWebhook.WebhookType);
+ logger.LogInformation("Signing secret (store securely, returned only once): {Secret}", createdWebhook.SigningSecret);
+
+ // Get resource for the specific webhook
+ IWebhookResource webhookResource = accountResource.Webhook(createdWebhook.Id);
+
+ // Get details of the webhook
+ Webhook webhookDetails = await webhookResource.GetDetails();
+ logger.LogInformation("Webhook details: Id={Id}, Url={Url}, Active={Active}",
+ webhookDetails.Id,
+ webhookDetails.Url,
+ webhookDetails.Active);
+
+ // Update the webhook - swap the event types and disable it
+ var updateRequest = new UpdateWebhookRequest
+ {
+ Active = false,
+ EventTypes = [WebhookEventType.Delivery, WebhookEventType.Open, WebhookEventType.Click]
+ };
+ Webhook updatedWebhook = await webhookResource.Update(updateRequest);
+ logger.LogInformation(
+ "Updated Webhook: Id={Id}, Active={Active}, EventTypes={EventTypes}",
+ updatedWebhook.Id,
+ updatedWebhook.Active,
+ string.Join(",", updatedWebhook.EventTypes));
+
+ // Delete the webhook
+ // The webhook resource becomes invalid after deletion and should not be used anymore
+ Webhook deletedWebhook = await webhookResource.Delete();
+ logger.LogInformation("Webhook Deleted: Id={Id}", deletedWebhook.Id);
+}
+catch (Exception ex)
+{
+ logger.LogError(ex, "An error occurred during API call.");
+ Environment.ExitCode = 1;
+ return;
+}
diff --git a/examples/Mailtrap.Example.Webhooks/Properties/launchSettings.json b/examples/Mailtrap.Example.Webhooks/Properties/launchSettings.json
new file mode 100644
index 00000000..b8daf491
--- /dev/null
+++ b/examples/Mailtrap.Example.Webhooks/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Project": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/Mailtrap.Example.Webhooks/appsettings.json b/examples/Mailtrap.Example.Webhooks/appsettings.json
new file mode 100644
index 00000000..7cf4089d
--- /dev/null
+++ b/examples/Mailtrap.Example.Webhooks/appsettings.json
@@ -0,0 +1,17 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "System": "Warning",
+ "Microsoft": "Warning"
+ },
+ "Debug": {
+ "LogLevel": {
+ "Default": "Debug"
+ }
+ }
+ },
+ "Mailtrap": {
+ "ApiToken": ""
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs b/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs
index ade436a3..6ddfad51 100644
--- a/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs
+++ b/src/Mailtrap.Abstractions/Accounts/IAccountResource.cs
@@ -246,4 +246,57 @@ public interface IAccountResource : IRestResource
/// When is null or empty.
///
public IEmailLogResource EmailLog(string sendingMessageId);
+
+ ///
+ /// Gets API token collection resource for the account, represented by this resource instance.
+ ///
+ ///
+ ///
+ /// API token collection resource for the account, represented by this resource instance.
+ ///
+ public IApiTokenCollectionResource ApiTokens();
+
+ ///
+ /// Gets resource for specific API token, identified by .
+ ///
+ ///
+ ///
+ /// ID of API token to get resource for.
+ ///
+ ///
+ ///
+ /// Resource for the API token with specified ID.
+ ///
+ ///
+ ///
+ /// When is less than or equal to zero.
+ ///
+ public IApiTokenResource ApiToken(long apiTokenId);
+
+
+ ///
+ /// Gets webhook collection resource for the account, represented by this resource instance.
+ ///
+ ///
+ ///
+ /// Webhook collection resource for the account, represented by this resource instance.
+ ///
+ public IWebhookCollectionResource Webhooks();
+
+ ///
+ /// Gets resource for specific webhook, identified by .
+ ///
+ ///
+ ///
+ /// ID of webhook to get resource for.
+ ///
+ ///
+ ///
+ /// Resource for the webhook with specified ID.
+ ///
+ ///
+ ///
+ /// When is less than or equal to zero.
+ ///
+ public IWebhookResource Webhook(long webhookId);
}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/IApiTokenCollectionResource.cs b/src/Mailtrap.Abstractions/ApiTokens/IApiTokenCollectionResource.cs
new file mode 100644
index 00000000..19e759c7
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/IApiTokenCollectionResource.cs
@@ -0,0 +1,42 @@
+namespace Mailtrap.ApiTokens;
+
+
+///
+/// Represents API token collection resource.
+///
+public interface IApiTokenCollectionResource : IRestResource
+{
+ ///
+ /// List all API tokens visible to the current API token.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Collection of API token details.
+ ///
+ public Task> GetAll(CancellationToken cancellationToken = default);
+
+ ///
+ /// Create a new API token.
+ ///
+ ///
+ ///
+ /// API token creation request.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Created API token details, including the full token value.
+ ///
+ ///
+ ///
+ /// The full token value is only returned once at creation — store it securely.
+ ///
+ public Task Create(CreateApiTokenRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/IApiTokenResource.cs b/src/Mailtrap.Abstractions/ApiTokens/IApiTokenResource.cs
new file mode 100644
index 00000000..328e0b7e
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/IApiTokenResource.cs
@@ -0,0 +1,49 @@
+namespace Mailtrap.ApiTokens;
+
+
+///
+/// Represents a single API token resource.
+///
+public interface IApiTokenResource : IRestResource
+{
+ ///
+ /// Get details of the API token represented by this resource.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// API token details.
+ ///
+ public Task GetDetails(CancellationToken cancellationToken = default);
+
+ ///
+ /// Permanently delete the API token represented by this resource.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ public Task Delete(CancellationToken cancellationToken = default);
+
+ ///
+ /// Reset the API token represented by this resource.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// New API token details, including the full token value.
+ ///
+ ///
+ ///
+ /// Expires the requested token and creates a new token with the same permissions.
+ /// The old token stops working after a short grace period. The response includes
+ /// the new token value — store it securely; it is only returned once.
+ ///
+ public Task Reset(CancellationToken cancellationToken = default);
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Models/ApiToken.cs b/src/Mailtrap.Abstractions/ApiTokens/Models/ApiToken.cs
new file mode 100644
index 00000000..fff7c952
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Models/ApiToken.cs
@@ -0,0 +1,76 @@
+namespace Mailtrap.ApiTokens.Models;
+
+
+///
+/// Represents API token details.
+///
+public sealed record ApiToken
+{
+ ///
+ /// Gets the API token identifier.
+ ///
+ ///
+ ///
+ /// API token identifier.
+ ///
+ [JsonPropertyName("id")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public long Id { get; set; }
+
+ ///
+ /// Gets the API token display name.
+ ///
+ ///
+ ///
+ /// API token display name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(2)]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets the last 4 characters of the token.
+ ///
+ ///
+ ///
+ /// Last 4 characters of the token. The full token value is only returned on create or reset.
+ ///
+ [JsonPropertyName("last_4_digits")]
+ [JsonPropertyOrder(3)]
+ public string Last4Digits { get; set; } = string.Empty;
+
+ ///
+ /// Gets the name of the user or token that created this API token.
+ ///
+ ///
+ ///
+ /// Creator name or if not available.
+ ///
+ [JsonPropertyName("created_by")]
+ [JsonPropertyOrder(4)]
+ public string? CreatedBy { get; set; }
+
+ ///
+ /// Gets the date and time when the API token expires.
+ ///
+ ///
+ ///
+ /// Expiration date and time, or if the token does not expire.
+ ///
+ [JsonPropertyName("expires_at")]
+ [JsonPropertyOrder(5)]
+ public DateTimeOffset? ExpiresAt { get; set; }
+
+ ///
+ /// Gets the resource accesses granted to this API token.
+ ///
+ ///
+ ///
+ /// Collection of resource accesses.
+ ///
+ [JsonPropertyName("resources")]
+ [JsonPropertyOrder(6)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Resources { get; } = [];
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Models/ApiTokenAccess.cs b/src/Mailtrap.Abstractions/ApiTokens/Models/ApiTokenAccess.cs
new file mode 100644
index 00000000..8c014885
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Models/ApiTokenAccess.cs
@@ -0,0 +1,42 @@
+namespace Mailtrap.ApiTokens.Models;
+
+
+///
+/// Represents an access entry granted to an API token for a specific resource.
+///
+public sealed record ApiTokenAccess
+{
+ ///
+ /// Gets the resource type.
+ ///
+ ///
+ ///
+ /// Resource type.
+ ///
+ [JsonPropertyName("resource_type")]
+ [JsonPropertyOrder(1)]
+ public ResourceType Type { get; set; } = ResourceType.Unknown;
+
+ ///
+ /// Gets the resource identifier.
+ ///
+ ///
+ ///
+ /// Resource identifier.
+ ///
+ [JsonPropertyName("resource_id")]
+ [JsonPropertyOrder(2)]
+ [JsonRequired]
+ public long Id { get; set; }
+
+ ///
+ /// Gets the resource access level.
+ ///
+ ///
+ ///
+ /// Access level for resource.
+ ///
+ [JsonPropertyName("access_level")]
+ [JsonPropertyOrder(3)]
+ public AccessLevel AccessLevel { get; set; } = AccessLevel.Indeterminate;
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Requests/ApiTokenAccessRequest.cs b/src/Mailtrap.Abstractions/ApiTokens/Requests/ApiTokenAccessRequest.cs
new file mode 100644
index 00000000..db9025de
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Requests/ApiTokenAccessRequest.cs
@@ -0,0 +1,96 @@
+namespace Mailtrap.ApiTokens.Requests;
+
+
+///
+/// Represents an access entry to grant to an API token for a specific resource.
+///
+public sealed record ApiTokenAccessRequest : IValidatable
+{
+ ///
+ /// Gets the resource type.
+ ///
+ ///
+ ///
+ /// Resource type.
+ ///
+ [JsonPropertyName("resource_type")]
+ [JsonPropertyOrder(1)]
+ public ResourceType ResourceType { get; }
+
+ ///
+ /// Gets the resource identifier.
+ ///
+ ///
+ ///
+ /// Resource identifier.
+ ///
+ [JsonPropertyName("resource_id")]
+ [JsonPropertyOrder(2)]
+ public long ResourceId { get; }
+
+ ///
+ /// Gets the resource access level.
+ ///
+ ///
+ ///
+ /// Access level for resource. Allowed values: or .
+ ///
+ [JsonPropertyName("access_level")]
+ [JsonPropertyOrder(3)]
+ public AccessLevel AccessLevel { get; }
+
+
+ ///
+ /// Primary instance constructor.
+ ///
+ ///
+ ///
+ /// Type of the resource to grant access to.
+ ///
+ ///
+ ///
+ /// ID of the resource to grant access to.
+ ///
+ ///
+ ///
+ /// Access level for the resource. Allowed values: or .
+ ///
+ ///
+ ///
+ /// When is .
+ ///
+ ///
+ ///
+ /// When is less than or equal to zero,
+ /// or is not or .
+ ///
+ public ApiTokenAccessRequest(
+ ResourceType resourceType,
+ long resourceId,
+ AccessLevel accessLevel)
+ {
+ Ensure.NotNull(resourceType, nameof(resourceType));
+ Ensure.GreaterThanZero(resourceId, nameof(resourceId));
+
+ if (accessLevel is not AccessLevel.Viewer and not AccessLevel.Admin)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(accessLevel),
+ accessLevel,
+ "Allowed values are Viewer or Admin");
+ }
+
+ ResourceType = resourceType;
+ ResourceId = resourceId;
+ AccessLevel = accessLevel;
+ }
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return ApiTokenAccessRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Requests/CreateApiTokenRequest.cs b/src/Mailtrap.Abstractions/ApiTokens/Requests/CreateApiTokenRequest.cs
new file mode 100644
index 00000000..2f332166
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Requests/CreateApiTokenRequest.cs
@@ -0,0 +1,41 @@
+namespace Mailtrap.ApiTokens.Requests;
+
+
+///
+/// Request to create a new API token.
+///
+public sealed record CreateApiTokenRequest : IValidatable
+{
+ ///
+ /// Gets or sets the API token display name.
+ ///
+ ///
+ ///
+ /// API token display name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets the resource accesses to grant to the API token.
+ ///
+ ///
+ ///
+ /// Collection of resource accesses.
+ ///
+ [JsonPropertyName("resources")]
+ [JsonPropertyOrder(2)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Resources { get; } = [];
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return CreateApiTokenRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Responses/ApiTokenResetResponse.cs b/src/Mailtrap.Abstractions/ApiTokens/Responses/ApiTokenResetResponse.cs
new file mode 100644
index 00000000..c36c7df1
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Responses/ApiTokenResetResponse.cs
@@ -0,0 +1,60 @@
+namespace Mailtrap.ApiTokens.Responses;
+
+
+///
+/// Response returned when an API token is reset. Includes the new full token value,
+/// which is only returned once at reset time.
+///
+public sealed record ApiTokenResetResponse
+{
+ ///
+ /// Gets the API token identifier.
+ ///
+ [JsonPropertyName("id")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public long Id { get; set; }
+
+ ///
+ /// Gets the API token display name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(2)]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets the last 4 characters of the new token.
+ ///
+ [JsonPropertyName("last_4_digits")]
+ [JsonPropertyOrder(3)]
+ public string Last4Digits { get; set; } = string.Empty;
+
+ ///
+ /// Gets the name of the user or token that created this API token.
+ ///
+ [JsonPropertyName("created_by")]
+ [JsonPropertyOrder(4)]
+ public string? CreatedBy { get; set; }
+
+ ///
+ /// Gets the date and time when the API token expires, or if it does not expire.
+ ///
+ [JsonPropertyName("expires_at")]
+ [JsonPropertyOrder(5)]
+ public DateTimeOffset? ExpiresAt { get; set; }
+
+ ///
+ /// Gets the resource accesses granted to this API token.
+ ///
+ [JsonPropertyName("resources")]
+ [JsonPropertyOrder(6)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Resources { get; } = [];
+
+ ///
+ /// Gets the new full token value. Only returned at reset time — store it securely.
+ ///
+ [JsonPropertyName("token")]
+ [JsonPropertyOrder(7)]
+ public string Token { get; set; } = string.Empty;
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Responses/CreateApiTokenResponse.cs b/src/Mailtrap.Abstractions/ApiTokens/Responses/CreateApiTokenResponse.cs
new file mode 100644
index 00000000..9a01e250
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Responses/CreateApiTokenResponse.cs
@@ -0,0 +1,60 @@
+namespace Mailtrap.ApiTokens.Responses;
+
+
+///
+/// Response returned when an API token is created. Includes the full token value,
+/// which is only returned once at creation time.
+///
+public sealed record CreateApiTokenResponse
+{
+ ///
+ /// Gets the API token identifier.
+ ///
+ [JsonPropertyName("id")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public long Id { get; set; }
+
+ ///
+ /// Gets the API token display name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(2)]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets the last 4 characters of the token.
+ ///
+ [JsonPropertyName("last_4_digits")]
+ [JsonPropertyOrder(3)]
+ public string Last4Digits { get; set; } = string.Empty;
+
+ ///
+ /// Gets the name of the user or token that created this API token.
+ ///
+ [JsonPropertyName("created_by")]
+ [JsonPropertyOrder(4)]
+ public string? CreatedBy { get; set; }
+
+ ///
+ /// Gets the date and time when the API token expires, or if it does not expire.
+ ///
+ [JsonPropertyName("expires_at")]
+ [JsonPropertyOrder(5)]
+ public DateTimeOffset? ExpiresAt { get; set; }
+
+ ///
+ /// Gets the resource accesses granted to this API token.
+ ///
+ [JsonPropertyName("resources")]
+ [JsonPropertyOrder(6)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Resources { get; } = [];
+
+ ///
+ /// Gets the full token value. Only returned at creation time — store it securely.
+ ///
+ [JsonPropertyName("token")]
+ [JsonPropertyOrder(7)]
+ public string Token { get; set; } = string.Empty;
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Validators/ApiTokenAccessRequestValidator.cs b/src/Mailtrap.Abstractions/ApiTokens/Validators/ApiTokenAccessRequestValidator.cs
new file mode 100644
index 00000000..b3e4ce7a
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Validators/ApiTokenAccessRequestValidator.cs
@@ -0,0 +1,22 @@
+namespace Mailtrap.ApiTokens.Validators;
+
+
+internal sealed class ApiTokenAccessRequestValidator : AbstractValidator
+{
+ public static ApiTokenAccessRequestValidator Instance { get; } = new();
+
+ public ApiTokenAccessRequestValidator()
+ {
+ RuleFor(r => r.ResourceType)
+ .NotEmpty()
+ .NotEqual(ResourceType.None);
+
+ RuleFor(r => r.ResourceId)
+ .GreaterThan(0);
+
+ RuleFor(r => r.AccessLevel)
+ .NotEmpty()
+ .IsInEnum()
+ .Must(l => l is AccessLevel.Admin or AccessLevel.Viewer);
+ }
+}
diff --git a/src/Mailtrap.Abstractions/ApiTokens/Validators/CreateApiTokenRequestValidator.cs b/src/Mailtrap.Abstractions/ApiTokens/Validators/CreateApiTokenRequestValidator.cs
new file mode 100644
index 00000000..477fb80c
--- /dev/null
+++ b/src/Mailtrap.Abstractions/ApiTokens/Validators/CreateApiTokenRequestValidator.cs
@@ -0,0 +1,25 @@
+namespace Mailtrap.ApiTokens.Validators;
+
+
+///
+/// Validator for requests.
+/// Ensures name is not empty and that every nested resource access entry is valid.
+///
+public sealed class CreateApiTokenRequestValidator : AbstractValidator
+{
+ ///
+ /// Static validator instance for reuse.
+ ///
+ public static CreateApiTokenRequestValidator Instance { get; } = new();
+
+ ///
+ /// Primary constructor.
+ ///
+ public CreateApiTokenRequestValidator()
+ {
+ RuleFor(r => r.Name).NotEmpty().MaximumLength(255);
+
+ RuleForEach(r => r.Resources)
+ .SetValidator(ApiTokenAccessRequestValidator.Instance);
+ }
+}
diff --git a/src/Mailtrap.Abstractions/EmailLogs/Models/SendingStream.cs b/src/Mailtrap.Abstractions/Core/Models/SendingStream.cs
similarity index 79%
rename from src/Mailtrap.Abstractions/EmailLogs/Models/SendingStream.cs
rename to src/Mailtrap.Abstractions/Core/Models/SendingStream.cs
index abd787c0..006ab65f 100644
--- a/src/Mailtrap.Abstractions/EmailLogs/Models/SendingStream.cs
+++ b/src/Mailtrap.Abstractions/Core/Models/SendingStream.cs
@@ -1,8 +1,8 @@
-namespace Mailtrap.EmailLogs.Models;
+namespace Mailtrap.Core.Models;
///
-/// Represents the sending stream for an email log message.
+/// Represents an email sending stream.
///
public sealed record SendingStream : StringEnum
{
diff --git a/src/Mailtrap.Abstractions/GlobalSuppressions.cs b/src/Mailtrap.Abstractions/GlobalSuppressions.cs
index 1f842691..3677d735 100644
--- a/src/Mailtrap.Abstractions/GlobalSuppressions.cs
+++ b/src/Mailtrap.Abstractions/GlobalSuppressions.cs
@@ -15,10 +15,12 @@
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO", Scope = "type", Target = "~T:Mailtrap.Emails.Requests.EmailRequest")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO", Scope = "type", Target = "~T:Mailtrap.Emails.Requests.BatchEmailRequest")]
[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Suppression Sending Stream type should be named according to its value", Scope = "type", Target = "~T:Mailtrap.Suppressions.Models.SuppressionSendingStream")]
-[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Email log sending stream type matches API value", Scope = "type", Target = "~T:Mailtrap.EmailLogs.Models.SendingStream")]
+[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Sending stream type matches API value", Scope = "type", Target = "~T:Mailtrap.Core.Models.SendingStream")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO; setter required for JSON deserialization on netstandard2.0/pre-.NET 8", Scope = "member", Target = "~P:Mailtrap.EmailLogs.Models.EmailLogsListResponse.Messages")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO; setter required for JSON deserialization when API returns null", Scope = "member", Target = "~P:Mailtrap.EmailLogs.Models.EmailLogMessage.CustomVariables")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO; setter required for JSON deserialization when API returns null", Scope = "member", Target = "~P:Mailtrap.EmailLogs.Models.EmailLogMessage.TemplateVariables")]
[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO; setter required for JSON deserialization when API returns null", Scope = "member", Target = "~P:Mailtrap.EmailLogs.Models.EmailLogMessage.Events")]
[assembly: SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = "API returns string; signed/temporary URL", Scope = "member", Target = "~P:Mailtrap.EmailLogs.Models.EmailLogMessage.RawMessageUrl")]
[assembly: SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = "API returns string; avoid Uri parsing for signed URLs", Scope = "member", Target = "~P:Mailtrap.EmailLogs.Models.EventDetailsClick.ClickUrl")]
+
+[assembly: SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "DTO; nullable setter required to distinguish 'unchanged' from 'set to empty' on PATCH", Scope = "member", Target = "~P:Mailtrap.Webhooks.Requests.UpdateWebhookRequest.EventTypes")]
diff --git a/src/Mailtrap.Abstractions/GlobalUsings.cs b/src/Mailtrap.Abstractions/GlobalUsings.cs
index 7899f17f..ed66356e 100644
--- a/src/Mailtrap.Abstractions/GlobalUsings.cs
+++ b/src/Mailtrap.Abstractions/GlobalUsings.cs
@@ -11,6 +11,11 @@
global using Mailtrap.AccountAccesses.Responses;
global using Mailtrap.Accounts;
global using Mailtrap.Accounts.Models;
+global using Mailtrap.ApiTokens;
+global using Mailtrap.ApiTokens.Models;
+global using Mailtrap.ApiTokens.Requests;
+global using Mailtrap.ApiTokens.Responses;
+global using Mailtrap.ApiTokens.Validators;
global using Mailtrap.Attachments;
global using Mailtrap.Attachments.Models;
global using Mailtrap.Billing;
@@ -61,6 +66,10 @@
global using Mailtrap.Inboxes;
global using Mailtrap.Inboxes.Models;
global using Mailtrap.Inboxes.Requests;
+global using Mailtrap.Organizations;
+global using Mailtrap.Organizations.Models;
+global using Mailtrap.Organizations.Requests;
+global using Mailtrap.Organizations.Validators;
global using Mailtrap.Permissions;
global using Mailtrap.Permissions.Models;
global using Mailtrap.Projects;
@@ -79,4 +88,9 @@
global using Mailtrap.TestingMessages.Models;
global using Mailtrap.TestingMessages.Requests;
global using Mailtrap.TestingMessages.Responses;
+global using Mailtrap.Webhooks;
+global using Mailtrap.Webhooks.Models;
+global using Mailtrap.Webhooks.Requests;
+global using Mailtrap.Webhooks.Responses;
+global using Mailtrap.Webhooks.Validators;
diff --git a/src/Mailtrap.Abstractions/IMailtrapClientFactory.cs b/src/Mailtrap.Abstractions/IMailtrapClientFactory.cs
index 33cffd90..946d3cc0 100644
--- a/src/Mailtrap.Abstractions/IMailtrapClientFactory.cs
+++ b/src/Mailtrap.Abstractions/IMailtrapClientFactory.cs
@@ -27,4 +27,17 @@ public interface IMailtrapClientFactory : IDisposable
/// Each call to this method is guaranteed to return a new instance of .
///
public IMailtrapClient CreateClient();
+
+ ///
+ /// Creates new instance of for organization-scoped operations.
+ ///
+ ///
+ ///
+ /// New instance.
+ ///
+ ///
+ ///
+ /// When factory was disposed.
+ ///
+ public IMailtrapOrganizationClient CreateOrganizationClient();
}
diff --git a/src/Mailtrap.Abstractions/IMailtrapOrganizationClient.cs b/src/Mailtrap.Abstractions/IMailtrapOrganizationClient.cs
new file mode 100644
index 00000000..4ecf9c3b
--- /dev/null
+++ b/src/Mailtrap.Abstractions/IMailtrapOrganizationClient.cs
@@ -0,0 +1,31 @@
+namespace Mailtrap;
+
+
+///
+/// Mailtrap organization-scoped API client.
+///
+///
+///
+/// Exposes organization-level operations such as managing sub accounts.
+/// Organization-scoped endpoints live under /api/organizations/{organization_id}/...
+/// and require token permissions for the targeted organization.
+///
+public interface IMailtrapOrganizationClient : IRestResource
+{
+ ///
+ /// Gets resource for specific organization, identified by .
+ ///
+ ///
+ ///
+ /// ID of organization to get resource for.
+ ///
+ ///
+ ///
+ /// Resource for the organization with specified ID.
+ ///
+ ///
+ ///
+ /// When is less than or equal to zero.
+ ///
+ public IOrganizationResource Organization(long organizationId);
+}
diff --git a/src/Mailtrap.Abstractions/Organizations/IOrganizationResource.cs b/src/Mailtrap.Abstractions/Organizations/IOrganizationResource.cs
new file mode 100644
index 00000000..c2d3451e
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Organizations/IOrganizationResource.cs
@@ -0,0 +1,17 @@
+namespace Mailtrap.Organizations;
+
+
+///
+/// Represents organization resource.
+///
+public interface IOrganizationResource : IRestResource
+{
+ ///
+ /// Gets sub account collection resource for the organization, represented by this resource instance.
+ ///
+ ///
+ ///
+ /// Sub account collection resource for the organization, represented by this resource instance.
+ ///
+ public IOrganizationSubAccountCollectionResource SubAccounts();
+}
diff --git a/src/Mailtrap.Abstractions/Organizations/IOrganizationSubAccountCollectionResource.cs b/src/Mailtrap.Abstractions/Organizations/IOrganizationSubAccountCollectionResource.cs
new file mode 100644
index 00000000..053d4454
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Organizations/IOrganizationSubAccountCollectionResource.cs
@@ -0,0 +1,38 @@
+namespace Mailtrap.Organizations;
+
+
+///
+/// Represents organization sub account collection resource.
+///
+public interface IOrganizationSubAccountCollectionResource : IRestResource
+{
+ ///
+ /// List sub accounts of the organization.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Collection of sub accounts.
+ ///
+ public Task> GetAll(CancellationToken cancellationToken = default);
+
+ ///
+ /// Create a new sub account under the organization.
+ ///
+ ///
+ ///
+ /// Sub account creation request.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Created sub account details.
+ ///
+ public Task Create(CreateSubAccountRequest request, CancellationToken cancellationToken = default);
+}
diff --git a/src/Mailtrap.Abstractions/Organizations/Models/SubAccount.cs b/src/Mailtrap.Abstractions/Organizations/Models/SubAccount.cs
new file mode 100644
index 00000000..c38b0668
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Organizations/Models/SubAccount.cs
@@ -0,0 +1,23 @@
+namespace Mailtrap.Organizations.Models;
+
+
+///
+/// Represents a sub account within an organization.
+///
+public sealed record SubAccount
+{
+ ///
+ /// Gets the sub account identifier.
+ ///
+ [JsonPropertyName("id")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public long Id { get; set; }
+
+ ///
+ /// Gets the sub account name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(2)]
+ public string Name { get; set; } = string.Empty;
+}
diff --git a/src/Mailtrap.Abstractions/Organizations/Models/SubAccountAttributes.cs b/src/Mailtrap.Abstractions/Organizations/Models/SubAccountAttributes.cs
new file mode 100644
index 00000000..cb7ddc63
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Organizations/Models/SubAccountAttributes.cs
@@ -0,0 +1,15 @@
+namespace Mailtrap.Organizations.Models;
+
+
+///
+/// Attributes used to create a sub account.
+///
+public sealed record SubAccountAttributes
+{
+ ///
+ /// Gets or sets the sub account name.
+ ///
+ [JsonPropertyName("name")]
+ [JsonPropertyOrder(1)]
+ public string Name { get; set; } = string.Empty;
+}
diff --git a/src/Mailtrap.Abstractions/Organizations/Requests/CreateSubAccountRequest.cs b/src/Mailtrap.Abstractions/Organizations/Requests/CreateSubAccountRequest.cs
new file mode 100644
index 00000000..4d67cd3c
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Organizations/Requests/CreateSubAccountRequest.cs
@@ -0,0 +1,26 @@
+namespace Mailtrap.Organizations.Requests;
+
+
+///
+/// Request to create a new sub account under an organization. Wraps the attributes
+/// in an account envelope, matching the API contract.
+///
+public sealed record CreateSubAccountRequest : IValidatable
+{
+ ///
+ /// Gets or sets the sub account attributes.
+ ///
+ [JsonPropertyName("account")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public SubAccountAttributes Account { get; set; } = new();
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return CreateSubAccountRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Organizations/Validators/CreateSubAccountRequestValidator.cs b/src/Mailtrap.Abstractions/Organizations/Validators/CreateSubAccountRequestValidator.cs
new file mode 100644
index 00000000..d37fa608
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Organizations/Validators/CreateSubAccountRequestValidator.cs
@@ -0,0 +1,22 @@
+namespace Mailtrap.Organizations.Validators;
+
+
+///
+/// Validator for .
+///
+public sealed class CreateSubAccountRequestValidator : AbstractValidator
+{
+ ///
+ /// Static validator instance for reuse.
+ ///
+ public static CreateSubAccountRequestValidator Instance { get; } = new();
+
+ ///
+ /// Primary constructor.
+ ///
+ public CreateSubAccountRequestValidator()
+ {
+ RuleFor(r => r.Account).NotNull();
+ RuleFor(r => r.Account.Name).NotEmpty().MaximumLength(255);
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/IWebhookCollectionResource.cs b/src/Mailtrap.Abstractions/Webhooks/IWebhookCollectionResource.cs
new file mode 100644
index 00000000..aa55c007
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/IWebhookCollectionResource.cs
@@ -0,0 +1,42 @@
+namespace Mailtrap.Webhooks;
+
+
+///
+/// Represents webhook collection resource.
+///
+public interface IWebhookCollectionResource : IRestResource
+{
+ ///
+ /// Returns all webhooks for the account.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Collection of webhook details.
+ ///
+ public Task> GetAll(CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a new webhook with details specified by .
+ ///
+ ///
+ ///
+ /// Request containing webhook details for creation.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Created webhook details, including the .
+ ///
+ ///
+ ///
+ /// The signing secret is only returned at creation time - store it securely.
+ ///
+ public Task Create(CreateWebhookRequest request, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Mailtrap.Abstractions/Webhooks/IWebhookResource.cs b/src/Mailtrap.Abstractions/Webhooks/IWebhookResource.cs
new file mode 100644
index 00000000..cec1bce6
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/IWebhookResource.cs
@@ -0,0 +1,51 @@
+namespace Mailtrap.Webhooks;
+
+
+///
+/// Represents webhook resource.
+///
+public interface IWebhookResource : IRestResource
+{
+ ///
+ /// Gets details of the webhook, represented by the current resource instance.
+ ///
+ ///
+ ///
+ /// Token to control operation cancellation.
+ ///
+ ///
+ ///
+ /// Requested webhook details.
+ ///
+ public Task GetDetails(CancellationToken cancellationToken = default);
+
+ ///
+ /// Updates the webhook, represented by the current resource instance, with details specified by .
+ ///
+ ///
+ ///
+ /// Webhook details for update. Only properties set on the request are sent.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Updated webhook details.
+ ///
+ public Task Update(UpdateWebhookRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Permanently deletes the webhook, represented by the current resource instance.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Deleted webhook details.
+ ///
+ public Task Delete(CancellationToken cancellationToken = default);
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Models/Webhook.cs b/src/Mailtrap.Abstractions/Webhooks/Models/Webhook.cs
new file mode 100644
index 00000000..fd66d066
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Models/Webhook.cs
@@ -0,0 +1,100 @@
+namespace Mailtrap.Webhooks.Models;
+
+
+///
+/// Represents webhook details.
+///
+public record Webhook
+{
+ ///
+ /// Gets or sets webhook identifier.
+ ///
+ ///
+ ///
+ /// Webhook identifier.
+ ///
+ [JsonPropertyName("id")]
+ [JsonPropertyOrder(1)]
+ public long Id { get; set; }
+
+ ///
+ /// Gets or sets the URL that will receive webhook payloads.
+ ///
+ ///
+ ///
+ /// The URL that will receive webhook payloads.
+ ///
+ [JsonPropertyName("url")]
+ [JsonPropertyOrder(2)]
+ public Uri? Url { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the webhook is active.
+ ///
+ ///
+ ///
+ /// when the webhook is active; otherwise.
+ ///
+ [JsonPropertyName("active")]
+ [JsonPropertyOrder(3)]
+ public bool Active { get; set; }
+
+ ///
+ /// Gets or sets webhook type.
+ ///
+ ///
+ ///
+ /// Webhook type. Allowed values: "email_sending", "audit_log".
+ ///
+ [JsonPropertyName("webhook_type")]
+ [JsonPropertyOrder(4)]
+ public WebhookType WebhookType { get; set; } = WebhookType.Unknown;
+
+ ///
+ /// Gets or sets the format of the webhook payload.
+ ///
+ ///
+ ///
+ /// Webhook payload format. Allowed values: "json", "jsonlines".
+ ///
+ [JsonPropertyName("payload_format")]
+ [JsonPropertyOrder(5)]
+ public WebhookPayloadFormat PayloadFormat { get; set; } = WebhookPayloadFormat.Unknown;
+
+ ///
+ /// Gets or sets sending stream the webhook is subscribed to.
+ /// Applicable only for email_sending webhooks.
+ ///
+ ///
+ ///
+ /// Sending stream or when not applicable.
+ ///
+ [JsonPropertyName("sending_stream")]
+ [JsonPropertyOrder(6)]
+ public SendingStream? SendingStream { get; set; }
+
+ ///
+ /// Gets or sets the domain ID the webhook is scoped to.
+ /// Applicable only for email_sending webhooks; means all domains.
+ ///
+ ///
+ ///
+ /// Domain identifier or when scoped to all domains.
+ ///
+ [JsonPropertyName("domain_id")]
+ [JsonPropertyOrder(7)]
+ public long? DomainId { get; set; }
+
+ ///
+ /// Gets or sets the list of event types the webhook is subscribed to.
+ /// Applicable only for email_sending webhooks.
+ ///
+ ///
+ ///
+ /// List of event types.
+ ///
+ [JsonPropertyName("event_types")]
+ [JsonPropertyOrder(8)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList EventTypes { get; } = [];
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Models/WebhookEventType.cs b/src/Mailtrap.Abstractions/Webhooks/Models/WebhookEventType.cs
new file mode 100644
index 00000000..f64e4592
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Models/WebhookEventType.cs
@@ -0,0 +1,53 @@
+namespace Mailtrap.Webhooks.Models;
+
+
+///
+/// Event type the email_sending webhook can subscribe to.
+///
+public sealed record WebhookEventType : StringEnum
+{
+ ///
+ /// Gets the value representing "delivery" event.
+ ///
+ public static readonly WebhookEventType Delivery = Define("delivery");
+
+ ///
+ /// Gets the value representing "soft_bounce" event.
+ ///
+ public static readonly WebhookEventType SoftBounce = Define("soft_bounce");
+
+ ///
+ /// Gets the value representing "bounce" event.
+ ///
+ public static readonly WebhookEventType Bounce = Define("bounce");
+
+ ///
+ /// Gets the value representing "suspension" event.
+ ///
+ public static readonly WebhookEventType Suspension = Define("suspension");
+
+ ///
+ /// Gets the value representing "unsubscribe" event.
+ ///
+ public static readonly WebhookEventType Unsubscribe = Define("unsubscribe");
+
+ ///
+ /// Gets the value representing "open" event.
+ ///
+ public static readonly WebhookEventType Open = Define("open");
+
+ ///
+ /// Gets the value representing "spam_complaint" event.
+ ///
+ public static readonly WebhookEventType SpamComplaint = Define("spam_complaint");
+
+ ///
+ /// Gets the value representing "click" event.
+ ///
+ public static readonly WebhookEventType Click = Define("click");
+
+ ///
+ /// Gets the value representing "reject" event.
+ ///
+ public static readonly WebhookEventType Reject = Define("reject");
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Models/WebhookPayloadFormat.cs b/src/Mailtrap.Abstractions/Webhooks/Models/WebhookPayloadFormat.cs
new file mode 100644
index 00000000..faaa3508
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Models/WebhookPayloadFormat.cs
@@ -0,0 +1,26 @@
+namespace Mailtrap.Webhooks.Models;
+
+
+///
+/// Format of the webhook payload.
+///
+public sealed record WebhookPayloadFormat : StringEnum
+{
+ ///
+ /// Gets the value representing "json" payload format.
+ ///
+ ///
+ ///
+ /// Represents "json" payload format.
+ ///
+ public static readonly WebhookPayloadFormat Json = Define("json");
+
+ ///
+ /// Gets the value representing "jsonlines" payload format.
+ ///
+ ///
+ ///
+ /// Represents "jsonlines" payload format.
+ ///
+ public static readonly WebhookPayloadFormat JsonLines = Define("jsonlines");
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Models/WebhookType.cs b/src/Mailtrap.Abstractions/Webhooks/Models/WebhookType.cs
new file mode 100644
index 00000000..08182caa
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Models/WebhookType.cs
@@ -0,0 +1,26 @@
+namespace Mailtrap.Webhooks.Models;
+
+
+///
+/// Webhook type. Determines which events the webhook can subscribe to.
+///
+public sealed record WebhookType : StringEnum
+{
+ ///
+ /// Gets the value representing "email_sending" webhook type.
+ ///
+ ///
+ ///
+ /// Represents "email_sending" webhook type.
+ ///
+ public static readonly WebhookType EmailSending = Define("email_sending");
+
+ ///
+ /// Gets the value representing "audit_log" webhook type.
+ ///
+ ///
+ ///
+ /// Represents "audit_log" webhook type.
+ ///
+ public static readonly WebhookType AuditLog = Define("audit_log");
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Requests/CreateWebhookRequest.cs b/src/Mailtrap.Abstractions/Webhooks/Requests/CreateWebhookRequest.cs
new file mode 100644
index 00000000..7bae6364
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Requests/CreateWebhookRequest.cs
@@ -0,0 +1,105 @@
+namespace Mailtrap.Webhooks.Requests;
+
+
+///
+/// Request object for creating a webhook.
+///
+public sealed record CreateWebhookRequest : IValidatable
+{
+ ///
+ /// Gets or sets the URL that will receive webhook payloads.
+ ///
+ ///
+ ///
+ /// Target URL.
+ ///
+ [JsonPropertyName("url")]
+ [JsonPropertyOrder(1)]
+ [JsonRequired]
+ public Uri? Url { get; set; }
+
+ ///
+ /// Gets or sets webhook type.
+ ///
+ ///
+ ///
+ /// Webhook type. Allowed values: "email_sending", "audit_log".
+ ///
+ [JsonPropertyName("webhook_type")]
+ [JsonPropertyOrder(2)]
+ [JsonRequired]
+ public WebhookType WebhookType { get; set; } = WebhookType.Unknown;
+
+ ///
+ /// Gets or sets a value indicating whether the webhook is active. Defaults to .
+ ///
+ ///
+ ///
+ /// Active flag or to use the API default.
+ ///
+ [JsonPropertyName("active")]
+ [JsonPropertyOrder(3)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Active { get; set; }
+
+ ///
+ /// Gets or sets the format of the webhook payload.
+ ///
+ ///
+ ///
+ /// Webhook payload format or to use the API default ("json").
+ ///
+ [JsonPropertyName("payload_format")]
+ [JsonPropertyOrder(4)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WebhookPayloadFormat? PayloadFormat { get; set; }
+
+ ///
+ /// Gets or sets sending stream the webhook subscribes to.
+ /// Required for email_sending webhook type.
+ ///
+ ///
+ ///
+ /// Sending stream or .
+ ///
+ [JsonPropertyName("sending_stream")]
+ [JsonPropertyOrder(5)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public SendingStream? SendingStream { get; set; }
+
+ ///
+ /// Gets the list of event types to subscribe to.
+ /// Required for email_sending webhook type.
+ ///
+ ///
+ ///
+ /// List of event types.
+ ///
+ [JsonPropertyName("event_types")]
+ [JsonPropertyOrder(6)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList EventTypes { get; } = [];
+
+ ///
+ /// Gets or sets the domain ID to scope the webhook to.
+ /// When omitted, the webhook applies to all domains.
+ /// Applicable only for email_sending webhooks.
+ ///
+ ///
+ ///
+ /// Domain identifier or .
+ ///
+ [JsonPropertyName("domain_id")]
+ [JsonPropertyOrder(7)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public long? DomainId { get; set; }
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return CreateWebhookRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Requests/UpdateWebhookRequest.cs b/src/Mailtrap.Abstractions/Webhooks/Requests/UpdateWebhookRequest.cs
new file mode 100644
index 00000000..6acf5dfb
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Requests/UpdateWebhookRequest.cs
@@ -0,0 +1,67 @@
+namespace Mailtrap.Webhooks.Requests;
+
+
+///
+/// Request object for updating a webhook. Only properties set on the request are sent.
+///
+public sealed record UpdateWebhookRequest : IValidatable
+{
+ ///
+ /// Gets or sets the URL that will receive webhook payloads.
+ ///
+ ///
+ ///
+ /// Target URL or to leave unchanged.
+ ///
+ [JsonPropertyName("url")]
+ [JsonPropertyOrder(1)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Uri? Url { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the webhook is active.
+ ///
+ ///
+ ///
+ /// Active flag or to leave unchanged.
+ ///
+ [JsonPropertyName("active")]
+ [JsonPropertyOrder(2)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Active { get; set; }
+
+ ///
+ /// Gets or sets the format of the webhook payload.
+ ///
+ ///
+ ///
+ /// Webhook payload format or to leave unchanged.
+ ///
+ [JsonPropertyName("payload_format")]
+ [JsonPropertyOrder(3)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WebhookPayloadFormat? PayloadFormat { get; set; }
+
+ ///
+ /// Gets or sets the list of event types the webhook subscribes to.
+ /// Applicable only for email_sending webhooks.
+ /// Leave to keep the existing event types unchanged.
+ ///
+ ///
+ ///
+ /// List of event types or to leave unchanged.
+ ///
+ [JsonPropertyName("event_types")]
+ [JsonPropertyOrder(4)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IList? EventTypes { get; set; }
+
+
+ ///
+ public ValidationResult Validate()
+ {
+ return UpdateWebhookRequestValidator.Instance
+ .Validate(this)
+ .ToMailtrapValidationResult();
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Responses/CreateWebhookResponse.cs b/src/Mailtrap.Abstractions/Webhooks/Responses/CreateWebhookResponse.cs
new file mode 100644
index 00000000..75304839
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Responses/CreateWebhookResponse.cs
@@ -0,0 +1,27 @@
+namespace Mailtrap.Webhooks.Responses;
+
+
+///
+/// Response object for webhook creation.
+///
+///
+///
+/// Extends with , which is only returned upon webhook creation.
+///
+public sealed record CreateWebhookResponse : Webhook
+{
+ ///
+ /// Gets or sets the secret key for verifying webhook payload signatures using HMAC SHA-256.
+ ///
+ ///
+ ///
+ /// Only returned at creation time. Store it securely - it cannot be retrieved later.
+ ///
+ ///
+ ///
+ /// Signing secret.
+ ///
+ [JsonPropertyName("signing_secret")]
+ [JsonPropertyOrder(9)]
+ public string SigningSecret { get; set; } = string.Empty;
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Validators/CreateWebhookRequestValidator.cs b/src/Mailtrap.Abstractions/Webhooks/Validators/CreateWebhookRequestValidator.cs
new file mode 100644
index 00000000..eb7456f9
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Validators/CreateWebhookRequestValidator.cs
@@ -0,0 +1,42 @@
+namespace Mailtrap.Webhooks.Validators;
+
+
+///
+/// Validator for .
+/// Ensures Url is an absolute URI, WebhookType is set,
+/// and that email_sending webhooks specify SendingStream and EventTypes.
+///
+public sealed class CreateWebhookRequestValidator : AbstractValidator
+{
+ ///
+ /// Static validator instance for reuse.
+ ///
+ public static CreateWebhookRequestValidator Instance { get; } = new();
+
+ ///
+ /// Primary constructor.
+ ///
+ public CreateWebhookRequestValidator()
+ {
+ RuleFor(r => r.Url)
+ .NotNull()
+ .Must(u => u is { IsAbsoluteUri: true })
+ .WithMessage("'Url' must be an absolute URI.");
+
+ RuleFor(r => r.WebhookType)
+ .NotNull()
+ .Must(t => t != WebhookType.None && t != WebhookType.Unknown)
+ .WithMessage("'WebhookType' must be set to a valid value.");
+
+ When(r => r.WebhookType == WebhookType.EmailSending, () =>
+ {
+ RuleFor(r => r.SendingStream)
+ .NotNull()
+ .WithMessage("'SendingStream' is required for 'email_sending' webhook type.");
+
+ RuleFor(r => r.EventTypes)
+ .Must(events => events is { Count: > 0 })
+ .WithMessage("'EventTypes' must contain at least one event for 'email_sending' webhook type.");
+ });
+ }
+}
diff --git a/src/Mailtrap.Abstractions/Webhooks/Validators/UpdateWebhookRequestValidator.cs b/src/Mailtrap.Abstractions/Webhooks/Validators/UpdateWebhookRequestValidator.cs
new file mode 100644
index 00000000..85e6742f
--- /dev/null
+++ b/src/Mailtrap.Abstractions/Webhooks/Validators/UpdateWebhookRequestValidator.cs
@@ -0,0 +1,27 @@
+namespace Mailtrap.Webhooks.Validators;
+
+
+///
+/// Validator for .
+/// Ensures Url, when provided, is an absolute URI.
+///
+public sealed class UpdateWebhookRequestValidator : AbstractValidator
+{
+ ///
+ /// Static validator instance for reuse.
+ ///
+ public static UpdateWebhookRequestValidator Instance { get; } = new();
+
+ ///
+ /// Primary constructor.
+ ///
+ public UpdateWebhookRequestValidator()
+ {
+ When(r => r.Url is not null, () =>
+ {
+ RuleFor(r => r.Url)
+ .Must(u => u is { IsAbsoluteUri: true })
+ .WithMessage("'Url' must be an absolute URI.");
+ });
+ }
+}
diff --git a/src/Mailtrap/Accounts/AccountResource.cs b/src/Mailtrap/Accounts/AccountResource.cs
index 003d2a76..2b596b47 100644
--- a/src/Mailtrap/Accounts/AccountResource.cs
+++ b/src/Mailtrap/Accounts/AccountResource.cs
@@ -120,4 +120,32 @@ public IEmailLogResource EmailLog(string sendingMessageId)
}
#endregion
+
+ #region API Tokens
+
+ public IApiTokenCollectionResource ApiTokens()
+ => new ApiTokenCollectionResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.ApiTokensSegment));
+
+ public IApiTokenResource ApiToken(long apiTokenId)
+ {
+ Ensure.GreaterThanZero(apiTokenId, nameof(apiTokenId));
+
+ return new ApiTokenResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.ApiTokensSegment).Append(apiTokenId));
+ }
+
+ #endregion
+
+ #region Webhooks
+
+ public IWebhookCollectionResource Webhooks()
+ => new WebhookCollectionResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.WebhooksSegment));
+
+ public IWebhookResource Webhook(long webhookId)
+ {
+ Ensure.GreaterThanZero(webhookId, nameof(webhookId));
+
+ return new WebhookResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.WebhooksSegment).Append(webhookId));
+ }
+
+ #endregion
}
diff --git a/src/Mailtrap/ApiTokens/ApiTokenCollectionResource.cs b/src/Mailtrap/ApiTokens/ApiTokenCollectionResource.cs
new file mode 100644
index 00000000..bfae88e8
--- /dev/null
+++ b/src/Mailtrap/ApiTokens/ApiTokenCollectionResource.cs
@@ -0,0 +1,15 @@
+namespace Mailtrap.ApiTokens;
+
+
+internal sealed class ApiTokenCollectionResource : RestResource, IApiTokenCollectionResource
+{
+ public ApiTokenCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task> GetAll(CancellationToken cancellationToken = default)
+ => await GetList(cancellationToken).ConfigureAwait(false);
+
+ public async Task Create(CreateApiTokenRequest request, CancellationToken cancellationToken = default)
+ => await Create(request, cancellationToken).ConfigureAwait(false);
+}
diff --git a/src/Mailtrap/ApiTokens/ApiTokenResource.cs b/src/Mailtrap/ApiTokens/ApiTokenResource.cs
new file mode 100644
index 00000000..a7b1a511
--- /dev/null
+++ b/src/Mailtrap/ApiTokens/ApiTokenResource.cs
@@ -0,0 +1,30 @@
+namespace Mailtrap.ApiTokens;
+
+
+internal sealed class ApiTokenResource : RestResource, IApiTokenResource
+{
+ private const string ResetSegment = "reset";
+
+
+ public ApiTokenResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task GetDetails(CancellationToken cancellationToken = default)
+ => await Get(cancellationToken).ConfigureAwait(false);
+
+ public async Task Delete(CancellationToken cancellationToken = default)
+ => await DeleteWithStatusCodeResult(cancellationToken).ConfigureAwait(false);
+
+ public async Task Reset(CancellationToken cancellationToken = default)
+ {
+ var uri = ResourceUri.Append(ResetSegment);
+
+ var result = await RestResourceCommandFactory
+ .CreatePost(uri)
+ .Execute(cancellationToken)
+ .ConfigureAwait(false);
+
+ return result;
+ }
+}
diff --git a/src/Mailtrap/Core/Constants/UrlSegments.cs b/src/Mailtrap/Core/Constants/UrlSegments.cs
index 73fc0707..079c08fd 100644
--- a/src/Mailtrap/Core/Constants/UrlSegments.cs
+++ b/src/Mailtrap/Core/Constants/UrlSegments.cs
@@ -16,4 +16,8 @@ internal static class UrlSegments
internal static string SuppressionsSegment { get; } = "suppressions";
internal static string StatsSegment { get; } = "stats";
internal static string EmailLogsSegment { get; } = "email_logs";
+ internal static string ApiTokensSegment { get; } = "api_tokens";
+ internal static string OrganizationsSegment { get; } = "organizations";
+ internal static string SubAccountsSegment { get; } = "sub_accounts";
+ internal static string WebhooksSegment { get; } = "webhooks";
}
diff --git a/src/Mailtrap/Core/Rest/Commands/PostWithoutBodyRestResourceCommand.cs b/src/Mailtrap/Core/Rest/Commands/PostWithoutBodyRestResourceCommand.cs
new file mode 100644
index 00000000..2c7b5da2
--- /dev/null
+++ b/src/Mailtrap/Core/Rest/Commands/PostWithoutBodyRestResourceCommand.cs
@@ -0,0 +1,18 @@
+namespace Mailtrap.Core.Rest.Commands;
+
+
+internal sealed class PostWithoutBodyRestResourceCommand : RestResourceCommand
+{
+ public PostWithoutBodyRestResourceCommand(
+ IHttpClientProvider httpClientProvider,
+ IHttpRequestMessageFactory httpRequestMessageFactory,
+ IHttpResponseHandlerFactory httpResponseHandlerFactory,
+ Uri resourceUri)
+ : base(
+ httpClientProvider,
+ httpRequestMessageFactory,
+ httpResponseHandlerFactory,
+ resourceUri,
+ HttpMethod.Post)
+ { }
+}
diff --git a/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs b/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs
index ec544c94..6975271a 100644
--- a/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs
+++ b/src/Mailtrap/Core/Rest/IRestResourceCommandFactory.cs
@@ -10,6 +10,7 @@ internal interface IRestResourceCommandFactory
public IRestResourceCommand CreateDeleteWithStatusCodeResult(Uri resourceUri);
public IRestResourceCommand CreatePostWithStatusCodeResult(Uri resourceUri, TRequest request) where TRequest : class;
public IRestResourceCommand CreatePost(Uri resourceUri, TRequest request) where TRequest : class;
+ public IRestResourceCommand CreatePost(Uri resourceUri);
public IRestResourceCommand CreatePut(Uri resourceUri, TRequest request) where TRequest : class;
public IRestResourceCommand CreatePatchWithContent(Uri resourceUri, TRequest request) where TRequest : class;
}
diff --git a/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs b/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs
index 373c2a2b..dccd1dbb 100644
--- a/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs
+++ b/src/Mailtrap/Core/Rest/RestResourceCommandFactory.cs
@@ -78,6 +78,13 @@ public IRestResourceCommand CreatePost(Uri resou
resourceUri,
request);
+ public IRestResourceCommand CreatePost(Uri resourceUri)
+ => new PostWithoutBodyRestResourceCommand(
+ _httpClientProvider,
+ _httpRequestMessageFactory,
+ _httpResponseHandlerFactory,
+ resourceUri);
+
public IRestResourceCommand CreatePut(Uri resourceUri, TRequest request)
where TRequest : class
=> new PutRestResourceCommand(
diff --git a/src/Mailtrap/GlobalUsings.cs b/src/Mailtrap/GlobalUsings.cs
index aa3c688d..42e992a6 100644
--- a/src/Mailtrap/GlobalUsings.cs
+++ b/src/Mailtrap/GlobalUsings.cs
@@ -13,6 +13,10 @@
global using Mailtrap.AccountAccesses.Responses;
global using Mailtrap.Accounts;
global using Mailtrap.Accounts.Models;
+global using Mailtrap.ApiTokens;
+global using Mailtrap.ApiTokens.Models;
+global using Mailtrap.ApiTokens.Requests;
+global using Mailtrap.ApiTokens.Responses;
global using Mailtrap.Attachments;
global using Mailtrap.Attachments.Models;
global using Mailtrap.Billing;
@@ -61,6 +65,9 @@
global using Mailtrap.Inboxes;
global using Mailtrap.Inboxes.Models;
global using Mailtrap.Inboxes.Requests;
+global using Mailtrap.Organizations;
+global using Mailtrap.Organizations.Models;
+global using Mailtrap.Organizations.Requests;
global using Mailtrap.Permissions;
global using Mailtrap.Permissions.Models;
global using Mailtrap.Projects;
@@ -78,6 +85,10 @@
global using Mailtrap.TestingMessages.Models;
global using Mailtrap.TestingMessages.Requests;
global using Mailtrap.TestingMessages.Responses;
+global using Mailtrap.Webhooks;
+global using Mailtrap.Webhooks.Models;
+global using Mailtrap.Webhooks.Requests;
+global using Mailtrap.Webhooks.Responses;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
diff --git a/src/Mailtrap/MailtrapClientFactory.cs b/src/Mailtrap/MailtrapClientFactory.cs
index c42349f6..abf04805 100644
--- a/src/Mailtrap/MailtrapClientFactory.cs
+++ b/src/Mailtrap/MailtrapClientFactory.cs
@@ -156,6 +156,9 @@ public MailtrapClientFactory(
///
public IMailtrapClient CreateClient() => _serviceProvider.GetRequiredService();
+ ///
+ public IMailtrapOrganizationClient CreateOrganizationClient() => _serviceProvider.GetRequiredService();
+
#endregion
diff --git a/src/Mailtrap/MailtrapClientServiceCollectionExtensions.cs b/src/Mailtrap/MailtrapClientServiceCollectionExtensions.cs
index 2eaafaeb..ce5d9033 100644
--- a/src/Mailtrap/MailtrapClientServiceCollectionExtensions.cs
+++ b/src/Mailtrap/MailtrapClientServiceCollectionExtensions.cs
@@ -144,6 +144,7 @@ internal static IServiceCollection AddMailtrapServices(this IServiceCollectio
services.TryAddTransient(services => services.GetRequiredService().CreateBatchDefault());
services.TryAddTransient();
+ services.TryAddTransient();
return services;
}
diff --git a/src/Mailtrap/MailtrapOrganizationClient.cs b/src/Mailtrap/MailtrapOrganizationClient.cs
new file mode 100644
index 00000000..1011866e
--- /dev/null
+++ b/src/Mailtrap/MailtrapOrganizationClient.cs
@@ -0,0 +1,20 @@
+namespace Mailtrap;
+
+
+///
+/// implementation.
+///
+internal sealed class MailtrapOrganizationClient : RestResource, IMailtrapOrganizationClient
+{
+ public MailtrapOrganizationClient(IRestResourceCommandFactory restResourceCommandFactory)
+ : base(restResourceCommandFactory, Endpoints.ApiDefaultUrl.Append(UrlSegments.ApiRootSegment))
+ { }
+
+
+ public IOrganizationResource Organization(long organizationId)
+ {
+ Ensure.GreaterThanZero(organizationId, nameof(organizationId));
+
+ return new OrganizationResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.OrganizationsSegment).Append(organizationId));
+ }
+}
diff --git a/src/Mailtrap/Organizations/OrganizationResource.cs b/src/Mailtrap/Organizations/OrganizationResource.cs
new file mode 100644
index 00000000..6c5c92f5
--- /dev/null
+++ b/src/Mailtrap/Organizations/OrganizationResource.cs
@@ -0,0 +1,12 @@
+namespace Mailtrap.Organizations;
+
+
+internal sealed class OrganizationResource : RestResource, IOrganizationResource
+{
+ public OrganizationResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public IOrganizationSubAccountCollectionResource SubAccounts()
+ => new OrganizationSubAccountCollectionResource(RestResourceCommandFactory, ResourceUri.Append(UrlSegments.SubAccountsSegment));
+}
diff --git a/src/Mailtrap/Organizations/OrganizationSubAccountCollectionResource.cs b/src/Mailtrap/Organizations/OrganizationSubAccountCollectionResource.cs
new file mode 100644
index 00000000..3dee8913
--- /dev/null
+++ b/src/Mailtrap/Organizations/OrganizationSubAccountCollectionResource.cs
@@ -0,0 +1,15 @@
+namespace Mailtrap.Organizations;
+
+
+internal sealed class OrganizationSubAccountCollectionResource : RestResource, IOrganizationSubAccountCollectionResource
+{
+ public OrganizationSubAccountCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task> GetAll(CancellationToken cancellationToken = default)
+ => await GetList(cancellationToken).ConfigureAwait(false);
+
+ public async Task Create(CreateSubAccountRequest request, CancellationToken cancellationToken = default)
+ => await Create(request, cancellationToken).ConfigureAwait(false);
+}
diff --git a/src/Mailtrap/Webhooks/Requests/CreateWebhookRequestDto.cs b/src/Mailtrap/Webhooks/Requests/CreateWebhookRequestDto.cs
new file mode 100644
index 00000000..9f2337e3
--- /dev/null
+++ b/src/Mailtrap/Webhooks/Requests/CreateWebhookRequestDto.cs
@@ -0,0 +1,23 @@
+namespace Mailtrap.Webhooks.Requests;
+
+
+///
+/// Request DTO that wraps in the webhook envelope expected by the API.
+///
+internal sealed record CreateWebhookRequestDto : IValidatable
+{
+ [JsonPropertyName("webhook")]
+ [JsonPropertyOrder(1)]
+ public CreateWebhookRequest Webhook { get; }
+
+
+ public CreateWebhookRequestDto(CreateWebhookRequest webhook)
+ {
+ Ensure.NotNull(webhook, nameof(webhook));
+
+ Webhook = webhook;
+ }
+
+
+ public ValidationResult Validate() => Webhook.Validate();
+}
diff --git a/src/Mailtrap/Webhooks/Requests/UpdateWebhookRequestDto.cs b/src/Mailtrap/Webhooks/Requests/UpdateWebhookRequestDto.cs
new file mode 100644
index 00000000..6a1a7f0c
--- /dev/null
+++ b/src/Mailtrap/Webhooks/Requests/UpdateWebhookRequestDto.cs
@@ -0,0 +1,23 @@
+namespace Mailtrap.Webhooks.Requests;
+
+
+///
+/// Request DTO that wraps in the webhook envelope expected by the API.
+///
+internal sealed record UpdateWebhookRequestDto : IValidatable
+{
+ [JsonPropertyName("webhook")]
+ [JsonPropertyOrder(1)]
+ public UpdateWebhookRequest Webhook { get; }
+
+
+ public UpdateWebhookRequestDto(UpdateWebhookRequest webhook)
+ {
+ Ensure.NotNull(webhook, nameof(webhook));
+
+ Webhook = webhook;
+ }
+
+
+ public ValidationResult Validate() => Webhook.Validate();
+}
diff --git a/src/Mailtrap/Webhooks/Requests/WebhookRequestExtensions.cs b/src/Mailtrap/Webhooks/Requests/WebhookRequestExtensions.cs
new file mode 100644
index 00000000..d2e5da0b
--- /dev/null
+++ b/src/Mailtrap/Webhooks/Requests/WebhookRequestExtensions.cs
@@ -0,0 +1,9 @@
+namespace Mailtrap.Webhooks.Requests;
+
+
+internal static class WebhookRequestExtensions
+{
+ public static CreateWebhookRequestDto ToDto(this CreateWebhookRequest request) => new(request);
+
+ public static UpdateWebhookRequestDto ToDto(this UpdateWebhookRequest request) => new(request);
+}
diff --git a/src/Mailtrap/Webhooks/Responses/CreateWebhookResponseDto.cs b/src/Mailtrap/Webhooks/Responses/CreateWebhookResponseDto.cs
new file mode 100644
index 00000000..0ec7c290
--- /dev/null
+++ b/src/Mailtrap/Webhooks/Responses/CreateWebhookResponseDto.cs
@@ -0,0 +1,13 @@
+namespace Mailtrap.Webhooks.Responses;
+
+
+///
+/// Response DTO that unwraps the created webhook (including signing_secret) from the data envelope.
+///
+internal sealed record CreateWebhookResponseDto
+{
+ [JsonPropertyName("data")]
+ [JsonPropertyOrder(1)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public CreateWebhookResponse Webhook { get; } = new();
+}
diff --git a/src/Mailtrap/Webhooks/Responses/WebhookListResponseDto.cs b/src/Mailtrap/Webhooks/Responses/WebhookListResponseDto.cs
new file mode 100644
index 00000000..a30523eb
--- /dev/null
+++ b/src/Mailtrap/Webhooks/Responses/WebhookListResponseDto.cs
@@ -0,0 +1,13 @@
+namespace Mailtrap.Webhooks.Responses;
+
+
+///
+/// Response DTO that unwraps the list of webhooks from the data envelope.
+///
+internal sealed record WebhookListResponseDto
+{
+ [JsonPropertyName("data")]
+ [JsonPropertyOrder(1)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public IList Webhooks { get; } = [];
+}
diff --git a/src/Mailtrap/Webhooks/Responses/WebhookResponseDto.cs b/src/Mailtrap/Webhooks/Responses/WebhookResponseDto.cs
new file mode 100644
index 00000000..98a00455
--- /dev/null
+++ b/src/Mailtrap/Webhooks/Responses/WebhookResponseDto.cs
@@ -0,0 +1,13 @@
+namespace Mailtrap.Webhooks.Responses;
+
+
+///
+/// Response DTO that unwraps a single webhook from the data envelope.
+///
+internal sealed record WebhookResponseDto
+{
+ [JsonPropertyName("data")]
+ [JsonPropertyOrder(1)]
+ [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
+ public Webhook Webhook { get; } = new();
+}
diff --git a/src/Mailtrap/Webhooks/WebhookCollectionResource.cs b/src/Mailtrap/Webhooks/WebhookCollectionResource.cs
new file mode 100644
index 00000000..a6490203
--- /dev/null
+++ b/src/Mailtrap/Webhooks/WebhookCollectionResource.cs
@@ -0,0 +1,25 @@
+namespace Mailtrap.Webhooks;
+
+
+internal sealed class WebhookCollectionResource : RestResource, IWebhookCollectionResource
+{
+ public WebhookCollectionResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task> GetAll(CancellationToken cancellationToken = default)
+ {
+ var response = await Get(cancellationToken).ConfigureAwait(false);
+
+ return response.Webhooks;
+ }
+
+ public async Task Create(CreateWebhookRequest request, CancellationToken cancellationToken = default)
+ {
+ Ensure.NotNull(request, nameof(request));
+
+ var response = await Create(request.ToDto(), cancellationToken).ConfigureAwait(false);
+
+ return response.Webhook;
+ }
+}
diff --git a/src/Mailtrap/Webhooks/WebhookResource.cs b/src/Mailtrap/Webhooks/WebhookResource.cs
new file mode 100644
index 00000000..da6f6550
--- /dev/null
+++ b/src/Mailtrap/Webhooks/WebhookResource.cs
@@ -0,0 +1,32 @@
+namespace Mailtrap.Webhooks;
+
+
+internal sealed class WebhookResource : RestResource, IWebhookResource
+{
+ public WebhookResource(IRestResourceCommandFactory restResourceCommandFactory, Uri resourceUri)
+ : base(restResourceCommandFactory, resourceUri) { }
+
+
+ public async Task GetDetails(CancellationToken cancellationToken = default)
+ {
+ var response = await Get(cancellationToken).ConfigureAwait(false);
+
+ return response.Webhook;
+ }
+
+ public async Task Update(UpdateWebhookRequest request, CancellationToken cancellationToken = default)
+ {
+ Ensure.NotNull(request, nameof(request));
+
+ var response = await Update(request.ToDto(), cancellationToken).ConfigureAwait(false);
+
+ return response.Webhook;
+ }
+
+ public async Task Delete(CancellationToken cancellationToken = default)
+ {
+ var response = await Delete(cancellationToken).ConfigureAwait(false);
+
+ return response.Webhook;
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/ApiTokens/ApiTokensIntegrationTests.cs b/tests/Mailtrap.IntegrationTests/ApiTokens/ApiTokensIntegrationTests.cs
new file mode 100644
index 00000000..edd340e3
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/ApiTokens/ApiTokensIntegrationTests.cs
@@ -0,0 +1,185 @@
+namespace Mailtrap.IntegrationTests.ApiTokens;
+
+
+[TestFixture]
+internal sealed class ApiTokensIntegrationTests
+{
+ private const string Feature = "ApiTokens";
+ private const string ResetSegment = "reset";
+
+ private readonly long _accountId;
+ private readonly Uri _resourceUri;
+ private readonly MailtrapClientOptions _clientConfig;
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+
+ public ApiTokensIntegrationTests()
+ {
+ var random = TestContext.CurrentContext.Random;
+
+ _accountId = random.NextLong();
+ _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.AccountsSegment)
+ .Append(_accountId)
+ .Append(UrlSegmentsTestConstants.ApiTokensSegment);
+
+ var token = random.GetString();
+ _clientConfig = new MailtrapClientOptions(token);
+ _jsonSerializerOptions = _clientConfig.ToJsonSerializerOptions();
+ }
+
+
+ [Test]
+ public async Task GetAll_Success()
+ {
+ // Arrange
+ using var responseContent = await Feature.LoadFileToStringContent();
+ var expectedResponse = await responseContent.DeserializeStringContentAsync>(_jsonSerializerOptions);
+ expectedResponse.Should().NotBeNull();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Get,
+ _resourceUri.AbsoluteUri,
+ responseContent,
+ HttpStatusCode.OK,
+ _clientConfig);
+
+ // Act
+ var result = await clientScope.Client
+ .Account(_accountId)
+ .ApiTokens()
+ .GetAll()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Should().BeEquivalentTo(expectedResponse);
+ }
+
+ [Test]
+ public async Task Create_Success()
+ {
+ // Arrange
+ var request = new CreateApiTokenRequest { Name = "My API Token" };
+ request.Resources.Add(new ApiTokenAccessRequest(ResourceType.Account, 3229, AccessLevel.Admin));
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+ var expectedResponse = await responseContent.DeserializeStringContentAsync(_jsonSerializerOptions);
+ expectedResponse.Should().NotBeNull();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Post,
+ _resourceUri.AbsoluteUri,
+ responseContent,
+ HttpStatusCode.OK,
+ _clientConfig);
+
+ // Act
+ var result = await clientScope.Client
+ .Account(_accountId)
+ .ApiTokens()
+ .Create(request)
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Should().BeEquivalentTo(expectedResponse);
+ }
+
+ [Test]
+ public async Task GetDetails_Success()
+ {
+ // Arrange
+ var apiTokenId = TestContext.CurrentContext.Random.NextLong();
+ var requestUri = _resourceUri.Append(apiTokenId).AbsoluteUri;
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+ var expectedResponse = await responseContent.DeserializeStringContentAsync(_jsonSerializerOptions);
+ expectedResponse.Should().NotBeNull();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Get,
+ requestUri,
+ responseContent,
+ HttpStatusCode.OK,
+ _clientConfig);
+
+ // Act
+ var result = await clientScope.Client
+ .Account(_accountId)
+ .ApiToken(apiTokenId)
+ .GetDetails()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Should().BeEquivalentTo(expectedResponse);
+ }
+
+ [Test]
+ public async Task Delete_Success()
+ {
+ // Arrange
+ var apiTokenId = TestContext.CurrentContext.Random.NextLong();
+ var requestUri = _resourceUri.Append(apiTokenId).AbsoluteUri;
+
+ using var responseContent = new StringContent(string.Empty);
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Delete,
+ requestUri,
+ responseContent,
+ HttpStatusCode.NoContent,
+ _clientConfig);
+
+ // Act
+ await clientScope.Client
+ .Account(_accountId)
+ .ApiToken(apiTokenId)
+ .Delete()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+ }
+
+ [Test]
+ public async Task Reset_Success()
+ {
+ // Arrange
+ var apiTokenId = TestContext.CurrentContext.Random.NextLong();
+ var requestUri = _resourceUri.Append(apiTokenId).Append(ResetSegment).AbsoluteUri;
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+ var expectedResponse = await responseContent.DeserializeStringContentAsync(_jsonSerializerOptions);
+ expectedResponse.Should().NotBeNull();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Post,
+ requestUri,
+ responseContent,
+ HttpStatusCode.OK,
+ _clientConfig);
+
+ // Act
+ var result = await clientScope.Client
+ .Account(_accountId)
+ .ApiToken(apiTokenId)
+ .Reset()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Should().BeEquivalentTo(expectedResponse);
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/ApiTokens/Create_Success.json b/tests/Mailtrap.IntegrationTests/ApiTokens/Create_Success.json
new file mode 100644
index 00000000..14b63ae6
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/ApiTokens/Create_Success.json
@@ -0,0 +1,15 @@
+{
+ "id": 12345,
+ "name": "My API Token",
+ "last_4_digits": "x7k9",
+ "created_by": "user@example.com",
+ "expires_at": null,
+ "resources": [
+ {
+ "resource_type": "account",
+ "resource_id": 3229,
+ "access_level": 100
+ }
+ ],
+ "token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
+}
diff --git a/tests/Mailtrap.IntegrationTests/ApiTokens/GetAll_Success.json b/tests/Mailtrap.IntegrationTests/ApiTokens/GetAll_Success.json
new file mode 100644
index 00000000..d68c5dd9
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/ApiTokens/GetAll_Success.json
@@ -0,0 +1,35 @@
+[
+ {
+ "id": 12345,
+ "name": "Production token",
+ "last_4_digits": "x7k9",
+ "created_by": "user@example.com",
+ "expires_at": null,
+ "resources": [
+ {
+ "resource_type": "account",
+ "resource_id": 3229,
+ "access_level": 100
+ }
+ ]
+ },
+ {
+ "id": 12346,
+ "name": "Read-only token",
+ "last_4_digits": "ab12",
+ "created_by": "owner@example.com",
+ "expires_at": "2026-12-31T23:59:59Z",
+ "resources": [
+ {
+ "resource_type": "project",
+ "resource_id": 99,
+ "access_level": 10
+ },
+ {
+ "resource_type": "inbox",
+ "resource_id": 4567,
+ "access_level": 10
+ }
+ ]
+ }
+]
diff --git a/tests/Mailtrap.IntegrationTests/ApiTokens/GetDetails_Success.json b/tests/Mailtrap.IntegrationTests/ApiTokens/GetDetails_Success.json
new file mode 100644
index 00000000..5cbe901a
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/ApiTokens/GetDetails_Success.json
@@ -0,0 +1,14 @@
+{
+ "id": 12345,
+ "name": "Production token",
+ "last_4_digits": "x7k9",
+ "created_by": "user@example.com",
+ "expires_at": null,
+ "resources": [
+ {
+ "resource_type": "account",
+ "resource_id": 3229,
+ "access_level": 100
+ }
+ ]
+}
diff --git a/tests/Mailtrap.IntegrationTests/ApiTokens/Reset_Success.json b/tests/Mailtrap.IntegrationTests/ApiTokens/Reset_Success.json
new file mode 100644
index 00000000..2d0e46b7
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/ApiTokens/Reset_Success.json
@@ -0,0 +1,15 @@
+{
+ "id": 12345,
+ "name": "Production token",
+ "last_4_digits": "z9y8",
+ "created_by": "user@example.com",
+ "expires_at": null,
+ "resources": [
+ {
+ "resource_type": "account",
+ "resource_id": 3229,
+ "access_level": 100
+ }
+ ],
+ "token": "newtokenvaluereturnedonresetonceonly1234"
+}
diff --git a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs
index e0122a80..c9bbcf8a 100644
--- a/tests/Mailtrap.IntegrationTests/GlobalUsings.cs
+++ b/tests/Mailtrap.IntegrationTests/GlobalUsings.cs
@@ -7,6 +7,9 @@
global using Mailtrap.AccountAccesses.Requests;
global using Mailtrap.AccountAccesses.Responses;
global using Mailtrap.Accounts.Models;
+global using Mailtrap.ApiTokens.Models;
+global using Mailtrap.ApiTokens.Requests;
+global using Mailtrap.ApiTokens.Responses;
global using Mailtrap.Attachments.Models;
global using Mailtrap.Configuration;
global using Mailtrap.Core.Constants;
@@ -32,6 +35,8 @@
global using Mailtrap.EmailTemplates.Requests;
global using Mailtrap.Inboxes.Models;
global using Mailtrap.Inboxes.Requests;
+global using Mailtrap.Organizations.Models;
+global using Mailtrap.Organizations.Requests;
global using Mailtrap.IntegrationTests.TestConstants;
global using Mailtrap.IntegrationTests.TestExtensions;
global using Mailtrap.Projects.Requests;
diff --git a/tests/Mailtrap.IntegrationTests/Organizations/Create_Success.json b/tests/Mailtrap.IntegrationTests/Organizations/Create_Success.json
new file mode 100644
index 00000000..71a4586a
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Organizations/Create_Success.json
@@ -0,0 +1 @@
+{ "id": 12347, "name": "New Team Account" }
diff --git a/tests/Mailtrap.IntegrationTests/Organizations/GetAll_Success.json b/tests/Mailtrap.IntegrationTests/Organizations/GetAll_Success.json
new file mode 100644
index 00000000..9ee7eaaa
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Organizations/GetAll_Success.json
@@ -0,0 +1,4 @@
+[
+ { "id": 12345, "name": "Development Team Account" },
+ { "id": 12346, "name": "QA Team Account" }
+]
diff --git a/tests/Mailtrap.IntegrationTests/Organizations/OrganizationsIntegrationTests.cs b/tests/Mailtrap.IntegrationTests/Organizations/OrganizationsIntegrationTests.cs
new file mode 100644
index 00000000..96d73dad
--- /dev/null
+++ b/tests/Mailtrap.IntegrationTests/Organizations/OrganizationsIntegrationTests.cs
@@ -0,0 +1,91 @@
+namespace Mailtrap.IntegrationTests.Organizations;
+
+
+[TestFixture]
+internal sealed class OrganizationsIntegrationTests
+{
+ private const string Feature = "Organizations";
+
+ private readonly long _organizationId;
+ private readonly Uri _resourceUri;
+ private readonly MailtrapClientOptions _clientConfig;
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+
+ public OrganizationsIntegrationTests()
+ {
+ var random = TestContext.CurrentContext.Random;
+
+ _organizationId = random.NextLong();
+ _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.OrganizationsSegment)
+ .Append(_organizationId)
+ .Append(UrlSegmentsTestConstants.SubAccountsSegment);
+
+ var token = random.GetString();
+ _clientConfig = new MailtrapClientOptions(token);
+ _jsonSerializerOptions = _clientConfig.ToJsonSerializerOptions();
+ }
+
+
+ [Test]
+ public async Task GetAll_Success()
+ {
+ // Arrange
+ using var responseContent = await Feature.LoadFileToStringContent();
+ var expectedResponse = await responseContent.DeserializeStringContentAsync>(_jsonSerializerOptions);
+ expectedResponse.Should().NotBeNull();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Get,
+ _resourceUri.AbsoluteUri,
+ responseContent,
+ HttpStatusCode.OK,
+ _clientConfig);
+
+ // Act
+ var result = await clientScope.OrganizationClient
+ .Organization(_organizationId)
+ .SubAccounts()
+ .GetAll()
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Should().BeEquivalentTo(expectedResponse);
+ }
+
+ [Test]
+ public async Task Create_Success()
+ {
+ // Arrange
+ var request = new CreateSubAccountRequest { Account = new SubAccountAttributes { Name = "New Team Account" } };
+
+ using var responseContent = await Feature.LoadFileToStringContent();
+ var expectedResponse = await responseContent.DeserializeStringContentAsync(_jsonSerializerOptions);
+ expectedResponse.Should().NotBeNull();
+
+ using var mockHttp = new MockHttpMessageHandler();
+ using var clientScope = mockHttp.ConfigureAndCreateClient(
+ HttpMethod.Post,
+ _resourceUri.AbsoluteUri,
+ responseContent,
+ HttpStatusCode.OK,
+ _clientConfig);
+
+ // Act
+ var result = await clientScope.OrganizationClient
+ .Organization(_organizationId)
+ .SubAccounts()
+ .Create(request)
+ .ConfigureAwait(false);
+
+ // Assert
+ mockHttp.VerifyNoOutstandingExpectation();
+
+ result.Should().BeEquivalentTo(expectedResponse);
+ }
+}
diff --git a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs
index edf6f5ac..d4b91515 100644
--- a/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs
+++ b/tests/Mailtrap.IntegrationTests/TestConstants/UrlSegmentsTestConstants.cs
@@ -27,4 +27,7 @@ internal static class UrlSegmentsTestConstants
internal static string EventsSegment { get; } = "events";
internal static string BatchEmailSegment { get; } = "batch";
internal static string StatsSegment { get; } = "stats";
+ internal static string ApiTokensSegment { get; } = "api_tokens";
+ internal static string OrganizationsSegment { get; } = "organizations";
+ internal static string SubAccountsSegment { get; } = "sub_accounts";
}
diff --git a/tests/Mailtrap.IntegrationTests/TestExtensions/MockHttpHandlerHelpers.cs b/tests/Mailtrap.IntegrationTests/TestExtensions/MockHttpHandlerHelpers.cs
index 7f342bbd..bc453122 100644
--- a/tests/Mailtrap.IntegrationTests/TestExtensions/MockHttpHandlerHelpers.cs
+++ b/tests/Mailtrap.IntegrationTests/TestExtensions/MockHttpHandlerHelpers.cs
@@ -8,7 +8,7 @@ namespace Mailtrap.IntegrationTests.TestExtensions;
/// and its associated within tests.
/// When disposed, it releases all resources created by the underlying service provider.
///
-internal sealed class MailtrapClientScope(IMailtrapClient client, ServiceProvider provider) : IDisposable
+internal sealed class MailtrapClientScope(IMailtrapClient client, IMailtrapOrganizationClient organizationClient, ServiceProvider provider) : IDisposable
{
///
/// Gets the configured Mailtrap client instance.
@@ -18,6 +18,12 @@ internal sealed class MailtrapClientScope(IMailtrapClient client, ServiceProvide
/// The instance resolved from the test service provider.
///
public IMailtrapClient Client { get; } = client;
+
+ ///
+ /// Gets the configured Mailtrap organization client instance.
+ ///
+ public IMailtrapOrganizationClient OrganizationClient { get; } = organizationClient;
+
private readonly ServiceProvider _provider = provider;
///
@@ -247,8 +253,9 @@ private static MailtrapClientScope BuildClient(this MockHttpMessageHandler mockH
var provider = serviceCollection.BuildServiceProvider();
var client = provider.GetRequiredService();
+ var organizationClient = provider.GetRequiredService();
- return new MailtrapClientScope(client, provider);
+ return new MailtrapClientScope(client, organizationClient, provider);
}
///
diff --git a/tests/Mailtrap.UnitTests/ApiTokens/ApiTokenCollectionResourceTests.cs b/tests/Mailtrap.UnitTests/ApiTokens/ApiTokenCollectionResourceTests.cs
new file mode 100644
index 00000000..4430609e
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/ApiTokens/ApiTokenCollectionResourceTests.cs
@@ -0,0 +1,53 @@
+namespace Mailtrap.UnitTests.ApiTokens;
+
+
+[TestFixture]
+internal sealed class ApiTokenCollectionResourceTests
+{
+ private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of();
+ private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.AccountsSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong())
+ .Append(UrlSegmentsTestConstants.ApiTokensSegment);
+
+
+ #region Constructor
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull()
+ {
+ // Act
+ var act = () => new ApiTokenCollectionResource(null!, _resourceUri);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull()
+ {
+ // Act
+ var act = () => new ApiTokenCollectionResource(_commandFactoryMock, null!);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void ResourceUri_ShouldBeInitializedProperly()
+ {
+ // Arrange
+ var client = CreateResource();
+
+ // Assert
+ client.ResourceUri.Should().Be(_resourceUri);
+ }
+
+ #endregion
+
+
+
+ private ApiTokenCollectionResource CreateResource() => new(_commandFactoryMock, _resourceUri);
+}
diff --git a/tests/Mailtrap.UnitTests/ApiTokens/ApiTokenResourceTests.cs b/tests/Mailtrap.UnitTests/ApiTokens/ApiTokenResourceTests.cs
new file mode 100644
index 00000000..4fdff334
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/ApiTokens/ApiTokenResourceTests.cs
@@ -0,0 +1,54 @@
+namespace Mailtrap.UnitTests.ApiTokens;
+
+
+[TestFixture]
+internal sealed class ApiTokenResourceTests
+{
+ private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of();
+ private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.AccountsSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong())
+ .Append(UrlSegmentsTestConstants.ApiTokensSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong());
+
+
+ #region Constructor
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull()
+ {
+ // Act
+ var act = () => new ApiTokenResource(null!, _resourceUri);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull()
+ {
+ // Act
+ var act = () => new ApiTokenResource(_commandFactoryMock, null!);
+
+ // Assert
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void ResourceUri_ShouldBeInitializedProperly()
+ {
+ // Arrange
+ var client = CreateResource();
+
+ // Assert
+ client.ResourceUri.Should().Be(_resourceUri);
+ }
+
+ #endregion
+
+
+
+ private ApiTokenResource CreateResource() => new(_commandFactoryMock, _resourceUri);
+}
diff --git a/tests/Mailtrap.UnitTests/ApiTokens/CreateApiTokenRequestValidatorTests.cs b/tests/Mailtrap.UnitTests/ApiTokens/CreateApiTokenRequestValidatorTests.cs
new file mode 100644
index 00000000..4ed8909a
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/ApiTokens/CreateApiTokenRequestValidatorTests.cs
@@ -0,0 +1,50 @@
+namespace Mailtrap.UnitTests.ApiTokens;
+
+
+[TestFixture]
+internal sealed class CreateApiTokenRequestValidatorTests
+{
+ private static readonly CreateApiTokenRequestValidator s_validator = CreateApiTokenRequestValidator.Instance;
+
+
+ [Test]
+ public void Validate_WithValidName_ShouldPass()
+ {
+ var request = new CreateApiTokenRequest { Name = "My API Token" };
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Validate_WithEmptyName_ShouldFail()
+ {
+ var request = new CreateApiTokenRequest { Name = string.Empty };
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Name);
+ }
+
+ [Test]
+ public void Validate_WithNameLongerThan255_ShouldFail()
+ {
+ var request = new CreateApiTokenRequest { Name = new string('a', 256) };
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Name);
+ }
+
+ [Test]
+ public void Validate_WithValidResource_ShouldPass()
+ {
+ var request = new CreateApiTokenRequest { Name = "Token" };
+ request.Resources.Add(new ApiTokenAccessRequest(ResourceType.Account, 3229, AccessLevel.Admin));
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+}
diff --git a/tests/Mailtrap.UnitTests/GlobalUsings.cs b/tests/Mailtrap.UnitTests/GlobalUsings.cs
index 305b0223..10a9248e 100644
--- a/tests/Mailtrap.UnitTests/GlobalUsings.cs
+++ b/tests/Mailtrap.UnitTests/GlobalUsings.cs
@@ -11,6 +11,9 @@
global using Mailtrap.AccountAccesses.Models;
global using Mailtrap.AccountAccesses.Requests;
global using Mailtrap.Accounts;
+global using Mailtrap.ApiTokens;
+global using Mailtrap.ApiTokens.Requests;
+global using Mailtrap.ApiTokens.Validators;
global using Mailtrap.Attachments;
global using Mailtrap.Billing;
global using Mailtrap.Configuration;
@@ -51,6 +54,10 @@
global using Mailtrap.EmailTemplates.Requests;
global using Mailtrap.Inboxes;
global using Mailtrap.Inboxes.Requests;
+global using Mailtrap.Organizations;
+global using Mailtrap.Organizations.Models;
+global using Mailtrap.Organizations.Requests;
+global using Mailtrap.Organizations.Validators;
global using Mailtrap.Permissions;
global using Mailtrap.Projects;
global using Mailtrap.Projects.Requests;
diff --git a/tests/Mailtrap.UnitTests/Organizations/CreateSubAccountRequestValidatorTests.cs b/tests/Mailtrap.UnitTests/Organizations/CreateSubAccountRequestValidatorTests.cs
new file mode 100644
index 00000000..75041e49
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Organizations/CreateSubAccountRequestValidatorTests.cs
@@ -0,0 +1,39 @@
+namespace Mailtrap.UnitTests.Organizations;
+
+
+[TestFixture]
+internal sealed class CreateSubAccountRequestValidatorTests
+{
+ private static readonly CreateSubAccountRequestValidator s_validator = CreateSubAccountRequestValidator.Instance;
+
+
+ [Test]
+ public void Validate_WithValidName_ShouldPass()
+ {
+ var request = new CreateSubAccountRequest { Account = new SubAccountAttributes { Name = "Team A" } };
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Validate_WithEmptyName_ShouldFail()
+ {
+ var request = new CreateSubAccountRequest { Account = new SubAccountAttributes { Name = string.Empty } };
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Account.Name);
+ }
+
+ [Test]
+ public void Validate_WithNameLongerThan255_ShouldFail()
+ {
+ var request = new CreateSubAccountRequest { Account = new SubAccountAttributes { Name = new string('a', 256) } };
+
+ var result = s_validator.TestValidate(request);
+
+ result.ShouldHaveValidationErrorFor(r => r.Account.Name);
+ }
+}
diff --git a/tests/Mailtrap.UnitTests/Organizations/OrganizationSubAccountCollectionResourceTests.cs b/tests/Mailtrap.UnitTests/Organizations/OrganizationSubAccountCollectionResourceTests.cs
new file mode 100644
index 00000000..18f4bfb9
--- /dev/null
+++ b/tests/Mailtrap.UnitTests/Organizations/OrganizationSubAccountCollectionResourceTests.cs
@@ -0,0 +1,47 @@
+namespace Mailtrap.UnitTests.Organizations;
+
+
+[TestFixture]
+internal sealed class OrganizationSubAccountCollectionResourceTests
+{
+ private readonly IRestResourceCommandFactory _commandFactoryMock = Mock.Of();
+ private readonly Uri _resourceUri = EndpointsTestConstants.ApiDefaultUrl
+ .Append(
+ UrlSegmentsTestConstants.ApiRootSegment,
+ UrlSegmentsTestConstants.OrganizationsSegment)
+ .Append(TestContext.CurrentContext.Random.NextLong())
+ .Append(UrlSegmentsTestConstants.SubAccountsSegment);
+
+
+ #region Constructor
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenCommandFactoryIsNull()
+ {
+ var act = () => new OrganizationSubAccountCollectionResource(null!, _resourceUri);
+
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void Constructor_ShouldThrowArgumentNullException_WhenUriIsNull()
+ {
+ var act = () => new OrganizationSubAccountCollectionResource(_commandFactoryMock, null!);
+
+ act.Should().Throw();
+ }
+
+ [Test]
+ public void ResourceUri_ShouldBeInitializedProperly()
+ {
+ var client = CreateResource();
+
+ client.ResourceUri.Should().Be(_resourceUri);
+ }
+
+ #endregion
+
+
+
+ private OrganizationSubAccountCollectionResource CreateResource() => new(_commandFactoryMock, _resourceUri);
+}
diff --git a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs
index eaf9e57a..a234e60c 100644
--- a/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs
+++ b/tests/Mailtrap.UnitTests/TestConstants/UrlSegmentsTestConstants.cs
@@ -25,4 +25,7 @@ internal static class UrlSegmentsTestConstants
internal static string BatchEmailSegment { get; } = "batch";
internal static string StatsSegment { get; } = "stats";
internal static string EmailLogsSegment { get; } = "email_logs";
+ internal static string ApiTokensSegment { get; } = "api_tokens";
+ internal static string OrganizationsSegment { get; } = "organizations";
+ internal static string SubAccountsSegment { get; } = "sub_accounts";
}