@mxraven/admin - v2.1.0
    Preparing search index...

    @mxraven/admin - v2.1.0

    mxRaven Admin SDK (TypeScript)

    npm CI API reference license Node.js

    A TypeScript SDK for the mxRaven control-plane admin API. It manages a workspace end to end over the same /v2 REST surface the customer dashboard uses: domains, listeners, routing rules, API keys, suppressions, recipient sets, auto-reply templates, forwarding destinations, inbound routes, relays, storage integrations, webhook endpoints, identity providers, quotas, rate limits, governance, mail analytics, and the public auth login context.

    Full method and model documentation lives in the API reference.

    Status: early development. The public API is not yet stable.

    • Node.js 20.19 or later.
    • Zero runtime dependencies (uses the global fetch).
    pnpm add @mxraven/admin
    # or
    npm install @mxraven/admin
    # or
    yarn add @mxraven/admin

    The package ships dual ESM + CJS builds with type declarations for both, so either module system works.

    ESM

    import { AdminClient } from "@mxraven/admin";
    

    CommonJS

    const { AdminClient } = require("@mxraven/admin");
    

    TypeScript consumers get types automatically (dist/index.d.ts for ESM, dist/index.d.cts for CJS).

    import { AdminClient, domainStatus } from "@mxraven/admin";

    const token = process.env.MXRAVEN_TOKEN;
    if (token === undefined || token === "") {
    throw new Error("MXRAVEN_TOKEN is required");
    }

    const admin = new AdminClient({ token });
    const ws = admin.workspace("my-workspace");

    // Workspace identity and provisioning state.
    const tenant = await ws.tenant();
    console.log(tenant.slug, tenant.status, tenant.provisioningState);

    // List domains. Pagination is hidden behind an async iterator.
    for await (const domain of await ws.domains().list()) {
    console.log(domain.domainName, domain.status, domain.dkimVerified);
    }

    // Onboard a sending domain and operate on the hydrated entity.
    const created = await ws.domains().create("example.com");
    if (!created.dkimVerified) {
    await created.replace("dmarc@example.com");
    }

    The control plane authenticates every request with a bearer token (JWT or opaque access token) issued by ZITADEL, sent as Authorization: Bearer <token>. Public operations such as admin.auth().loginContext(...) work with a blank token.

    import { AdminClient, defaultRateLimitConfig } from "@mxraven/admin";

    const admin = new AdminClient({
    baseUrl: "https://api.mxraven.com", // default
    token, // omit for public operations only
    apiVersion: "v2", // default
    timeoutMs: 60_000, // default
    rateLimit: { ...defaultRateLimitConfig, maxRetries: 5, maxBackoffMs: 120_000 },
    fetch: customFetch, // optional transport override
    });
    Option Type Default Notes
    baseUrl string https://api.mxraven.com Trailing slashes are stripped.
    token string "" Bearer token; blank for public calls.
    apiVersion string v2 Prefixed to every client-relative path.
    rateLimit RateLimitConfig defaultRateLimitConfig Retry behaviour for 429.
    fetch FetchLike globalThis.fetch Transport override.
    timeoutMs number 60000 Per-request timeout.

    Read-only accessors: admin.baseUrl, admin.apiVersion, admin.rateLimit, admin.token.

    Every client-relative path is prefixed with the configured API version in one place. Paths that already carry the prefix are left untouched.

    const admin = new AdminClient({ baseUrl, token, apiVersion: "v3" });
    // "/tenants/acme/domains" -> /v3/tenants/acme/domains

    When the control plane returns 429, the client waits for Retry-After (delta-seconds or an HTTP date) and retries GET and DELETE up to a bounded number of attempts. POST and PUT are never retried automatically.

    import { AdminClient, disabledRateLimitConfig } from "@mxraven/admin";

    const admin = new AdminClient({ token, rateLimit: disabledRateLimitConfig });

    If retries are exhausted, a RateLimitException is thrown with retryAfterMs set.

    Every network operation accepts an AbortSignal as options.signal, the TypeScript equivalent of a context.

    const controller = new AbortController();
    const paged = await ws.domains().list({ signal: controller.signal });

    AdminClient is the connection plus the public auth() family. admin.workspace(slug) binds a tenant and exposes every tenant-scoped collection.

    Owner Accessor Manages
    Workspace tenant() Workspace identity and provisioning state
    Workspace domains() Sending-domain onboarding and DNS verification
    Workspace listeners() Submission and MTA listeners
    Workspace suppressions() Tenant recipient suppressions
    Workspace recipientSets() Reusable recipient collections and members
    Workspace autoReplyTemplates() Auto-reply templates
    Workspace smtpForwardDestinations() Verified SMTP forwarding destinations
    Workspace inboundRoutes() Recipient-domain routing to MTA listeners
    Workspace smtpRelays() Tenant SMTP relay integrations
    Workspace storageIntegrations() Tenant object-storage integrations
    Workspace webhookEndpoints() Webhook endpoints, secrets, and deliveries
    Workspace identityProviders() Tenant external identity providers
    Workspace quotas() Effective tenant quotas and usage
    Workspace mtaRateLimits() Tenant MTA rate-limit override
    Workspace governance() Audit log, dedicated IP pools, resource search
    Workspace mailAnalytics() Mail activity and delivery aggregates
    AdminClient auth() Public tenant login context

    A workspace is a lightweight binding; admin.workspace("a") and admin.workspace("b") let you work across tenants from one client.

    const ws = admin.workspace("my-workspace");
    ws.slug; // "my-workspace"

    Reads and creations return an entity, not a bare record. The entity wraps the wire record with the client and path needed to act on the resource, so you can read it and operate on it directly.

    const domain = await ws.domains().create("example.com");

    domain.domainName; // delegated data accessor
    domain.data; // raw wire record (DomainData)
    domain.isDeleted; // local bookkeeping
    await domain.reload(); // refresh the snapshot
    await domain.replace("dmarc@example.com");
    for await (const grant of await domain.listenerGrants()) {
    console.log(grant.listenerDisplayName);
    }

    Every entity exposes three things:

    • data - every field of the underlying record, as a readonly getter, plus data for the raw record.
    • operations - reload(), mutations (replace, update, setActive, rotateSecret, sendVerification, …), and delete().
    • children - strictly-owned collections and singletons: listener.routingRules(), listener.apiKeys(), listener.sendingDomainPolicy(), listener.mtaRateLimit(), set.members(), endpoint.deliveries(), domain.listenerGrants().

    delete() is idempotent per instance; isDeleted is local bookkeeping for deletes performed through that instance.

    List operations return a Paged<T>, an async iterable that fetches pages lazily. It works with for await ... of and collection helpers, so you can map, filter, or stop early without managing cursors.

    const paged = await ws.domains().list();

    for await (const domain of paged) {
    console.log(domain.domainName);
    }

    const firstTen = await paged.take(10);
    const names = await paged.map((domain) => domain.domainName).toArray();
    const verified = await paged.filter((domain) => domain.dkimVerified).toArray();

    When you need to drive pagination yourself, pages can fetch their neighbours.

    const paged = await ws.domains().list();

    let page = paged.firstPage;
    while (page.nextPageToken !== undefined) {
    page = await page.nextPage(); // or page.previousPage()
    console.log(`page of ${page.items.length}`);
    }

    for await (const each of paged.pages()) {
    // one iteration per page
    }

    paged.page(cursor) fetches an arbitrary cursor. Cursors are opaque; never parse, construct, or persist them.

    Filterable list endpoints accept a single options object with enums for every closed value set. Unset filters are omitted.

    import { domainStatus } from "@mxraven/admin";

    const verified = await ws.domains().list({
    search: "example.com",
    status: domainStatus.verified,
    dkimVerified: true,
    pageSize: 50,
    });

    search requires at least 3 characters; shorter values are rejected locally with a clear message. pageSize must be between 1 and 500.

    Non-success responses are decoded from the RFC 9457 application/problem+json body and thrown as an ApiException or one of its status-specific subclasses. Branch on code, which is stable.

    import { ConflictException, NotFoundException, ValidationException } from "@mxraven/admin";

    try {
    await ws.domains().create("example.com");
    } catch (error) {
    if (error instanceof NotFoundException) {
    // 404
    } else if (error instanceof ConflictException) {
    // 409
    } else if (error instanceof ValidationException) {
    for (const issue of error.errors) {
    console.error(issue.pointer, issue.code);
    }
    }
    }
    Exception Status Notes
    BadRequestException 400 Malformed request
    AuthenticationException 401 Missing, invalid, or expired token
    PermissionDeniedException 403 Missing scope or tenant access
    NotFoundException 404 Not found or not visible
    ConflictException 409 Conflicts with current state
    ValidationException 422 Semantic rejection; see errors
    RateLimitException 429 Retried per config, then thrown
    ServerException 5xx Control-plane failure
    ApiException any Base class for all API errors

    Every exception carries status, code, title, detail, traceId, errors, problem, and retryable.

    • Wire fields are snake_case; models are camelCase and map automatically.
    • Timestamps are ISO-8601 strings.
    • Unset optional request fields are omitted; null is preserved so it can clear a nullable field.
    • Create-only secrets are returned once and never on reads: IssuedAPIKey.secret and IssuedWebhookEndpoint.signingSecret. Store them immediately.
    • Contract enums are as const objects with a derived union (domainStatus.verified, listenerType.mta, …).

    defaultTerminalActionPayload and its update equivalents are built with typed factories on terminalActionPayload, or passed raw via of.

    Action Factory
    DELIVER terminalActionPayload.deliver() / .empty()
    DELIVER_DEDICATED terminalActionPayload.deliverDedicated(poolId)
    SMARTHOST_RELAY terminalActionPayload.smartHostRelay(relayRef)
    RELAY terminalActionPayload.relay(relayRef)
    AUTO_REPLY terminalActionPayload.autoReply(templateRef)
    DROP terminalActionPayload.drop() / .drop(auditReason)
    REJECT terminalActionPayload.reject(smtp, enhanced, message)

    CreateRoutingRuleRequest / ReplaceRoutingRuleRequest take a RoutingRuleAction built with routingRuleAction.*.

    Action Factory Listener
    DELIVER_DEDICATED routingRuleAction.deliverDedicated(poolId) submission
    DELIVER routingRuleAction.deliver() submission
    SMARTHOST_RELAY routingRuleAction.smartHostRelay(relayRef) submission
    DROP routingRuleAction.drop() / .drop(auditReason) both
    REJECT routingRuleAction.reject(smtp, enhanced, message) both
    MODIFY_HEADER routingRuleAction.modifyHeader(operations) submission
    ADD_RECIPIENT routingRuleAction.addRecipient(recipients) submission
    NOTIFY_WEBHOOK routingRuleAction.notifyWebhook(webhookRef) both
    RELAY routingRuleAction.relay(relayRef) mta
    AUTO_REPLY routingRuleAction.autoReply(templateRef) mta
    DELIVER_WEBHOOK routingRuleAction.deliverWebhook(webhookRef) mta
    SMTP_FORWARD routingRuleAction.smtpForward(destinationRef) mta
    S3_STORE routingRuleAction.s3Store(storageRef, prefix, template) mta

    modifyHeaderOperation.append(header, value), .set(header, value), and .remove(header) build header operations.


    Common usage for each resource family. This is intentionally not the full API surface - every method, option, and model is documented in the API reference (or run pnpm run docs to build it locally). Every method accepts options.signal for cancellation.

    const tenant = await ws.tenant();
    console.log(tenant.slug, tenant.status, tenant.provisioningState);
    const domains = ws.domains();

    for await (const domain of await domains.list({ status: domainStatus.verified })) {
    console.log(domain.domainName, domain.status);
    }

    const domain = await domains.create("example.com", "dmarc@example.com");
    await domain.replace("dmarc@example.com");

    for await (const grant of await domain.listenerGrants()) {
    console.log(grant.listenerDisplayName);
    }

    await domain.delete();
    const listeners = ws.listeners();

    const listener = await listeners.create({
    displayName: "Outbound",
    listenerType: "submission",
    streamType: "transactional",
    defaultTerminalActionType: "DELIVER",
    defaultTerminalActionPayload: terminalActionPayload.deliver(),
    });

    await listener.rename("Outbound primary");
    await listener.updateRspamdScanning(true);
    await listener.updateNotifications(true);

    for await (const mta of await listeners.listMtaListeners()) {
    console.log(mta.displayName);
    }
    const rules = listener.routingRules();

    const rule = await rules.create({
    priority: 1,
    expressionText: "from == 'billing@example.com'",
    action: routingRuleAction.smtpForward("billing-dest"),
    });

    await rule.setActive(false);
    await rule.reorder(10);
    const issued = await listener.apiKeys().create();
    console.log(issued.username, issued.secret); // the secret is returned once

    for await (const key of await listener.apiKeys().list()) {
    await key.delete();
    }

    A submission listener's key authenticates SMTP submission; an MTA listener's key authenticates the client notification channel.

    await listener
    .sendingDomainPolicy()
    .update([{ domainId: domain.id, subdomainScope: "include_subdomains" }]);

    const policy = await listener.sendingDomainPolicy().get();
    console.log(policy.grants);

    MTA listeners only; throws locally on a submission listener.

    const rateLimit = listener.mtaRateLimit();
    const current = await rateLimit.get();

    await rateLimit.put({
    messageRatePerMinute: Math.floor(current.inherited.messageRatePerMinute / 2),
    recipientRatePerMinute: Math.floor(current.inherited.recipientRatePerMinute / 2),
    taskRatePerMinute: Math.floor(current.inherited.taskRatePerMinute / 2),
    burst: Math.max(1, Math.floor(current.inherited.burst / 2)),
    maxConcurrency: Math.max(1, Math.floor(current.inherited.maxConcurrency / 2)),
    });

    await rateLimit.delete();
    const suppressions = ws.suppressions();

    await suppressions.create({
    emailAddress: "bounce@example.com",
    reason: suppressionReason.bounce,
    });

    for await (const suppression of await suppressions.list({ search: "example.com" })) {
    console.log(suppression.emailAddress, suppression.reason);
    }
    const set = await ws.recipientSets().create({ setRef: "vip", displayName: "VIP" });

    await set.members().add("vip@example.com");
    await set.members().batchAdd(["a@example.com", "b@example.com"]);

    for await (const member of await set.members().list()) {
    console.log(member.emailAddress, member.addedAt);
    }
    const template = await ws.autoReplyTemplates().create({
    templateRef: "out-of-office",
    displayName: "Out of office",
    fromAddress: "noreply@example.com",
    subject: "We received your message",
    textBody: "We will get back to you shortly.",
    });

    await template.setActive(true);
    const destination = await ws.smtpForwardDestinations().create({
    destinationRef: "billing",
    displayName: "Billing",
    emailAddress: "billing@example.com",
    });

    await destination.sendVerification();
    const route = await ws.inboundRoutes().create({
    mtaListenerId: listener.id,
    domainName: "inbound.example.com",
    });

    await route.update({ mtaListenerId: listener.id, domainName: "inbound2.example.com" });
    await ws.smtpRelays().create({
    relayRef: "primary",
    displayName: "Primary relay",
    host: "smtp.example.com",
    port: 587,
    username: "user",
    password: "secret",
    });

    for await (const relay of await ws.smtpRelays().list()) {
    console.log(relay.relayRef, relay.isActive);
    }
    await ws.storageIntegrations().create({
    storageRef: "archive",
    displayName: "Archive",
    accessKey: "key",
    secretKey: "secret",
    bucketName: "bucket",
    region: "us-east-1",
    endpointUrl: "", // empty selects the AWS default
    });
    const endpoint = await ws.webhookEndpoints().create({
    webhookRef: "billing-events",
    displayName: "Billing events",
    targetUrl: "https://example.com/hooks",
    });
    console.log(endpoint.signingSecret); // returned once

    for await (const delivery of await endpoint.deliveries().list()) {
    console.log(delivery.outcome, delivery.statusCode);
    }

    const rotated = await endpoint.rotateSecret();
    console.log(rotated.signingSecret);

    createOAuth, createJwt, createSaml, createLdap, createGoogle, createAzureAd, createGitHub, createGitHubEnterpriseServer, createGitLab, createGitLabSelfHosted, and createApple are available alongside the OIDC create.

    const provider = await ws.identityProviders().createOAuth({
    idpRef: "corporate-sso",
    name: "Corporate SSO",
    clientId: "client-id",
    clientSecret: "client-secret",
    authorizationEndpoint: "https://idp.example.com/authorize",
    tokenEndpoint: "https://idp.example.com/token",
    userEndpoint: "https://idp.example.com/userinfo",
    });

    await provider.setAutoGrant(true);

    const provisioning = await ws.identityProviders().getProvisioning();
    await ws.identityProviders().updateProvisioning({ roles: provisioning.roles });
    const quota = await ws.quotas().get();
    console.log(quota.effective.maxDomains, quota.usage.domains, quota.overLimit);
    const before = await ws.mtaRateLimits().get();

    await ws.mtaRateLimits().put({
    messageRatePerMinute: Math.floor(before.inherited.messageRatePerMinute / 2),
    recipientRatePerMinute: Math.floor(before.inherited.recipientRatePerMinute / 2),
    taskRatePerMinute: Math.floor(before.inherited.taskRatePerMinute / 2),
    burst: Math.max(1, Math.floor(before.inherited.burst / 2)),
    maxConcurrency: Math.max(1, Math.floor(before.inherited.maxConcurrency / 2)),
    });

    await ws.mtaRateLimits().delete();
    for await (const entry of await ws.governance().listAuditLog({ pageSize: 100 })) {
    console.log(entry.action, entry.actorKind, entry.status);
    }

    const hits = await (
    await ws.governance().search({ query: "example.com", types: ["domain", "listener"] })
    ).toArray();

    Intervals are validated locally: UTC-only, startAt < endAt, at most 31 days, and aligned to hour or day boundaries.

    import { mailAnalyticsDimension, mailAnalyticsMetric } from "@mxraven/admin";

    const series = await ws.mailAnalytics().series({
    startAt: "2026-09-01T00:00:00Z",
    endAt: "2026-09-02T00:00:00Z",
    metric: mailAnalyticsMetric.dataBytes,
    dimension: mailAnalyticsDimension.stream,
    limit: 10,
    });

    const listenerOverview = await ws.mailAnalytics().listenerOverview({
    startAt: "2026-09-01T00:00:00Z",
    endAt: "2026-09-02T00:00:00Z",
    listenerId: listener.id,
    });
    const context = await admin.auth().loginContext("my-workspace");
    console.log(context.workspaceRef, context.displayName);

    For endpoints not yet wrapped by a typed client, AdminClient exposes raw request methods. Paths are client-relative and get the API version prefix automatically.

    Method Description
    get(path, query?, options?) GET
    post(path, body, options?) POST
    put(path, body, options?) PUT
    delete(path, options?) DELETE
    request(method, path, query, body, options?) Full control
    paged(path, query?, options?) Lazily paginated GET

    Response exposes status, headers, body, rawBody, header(name), as<T>(), asRaw<T>(), listOf<T>(), and pageOf<T>().

    const response = await admin.get("/tenants/my-workspace/domains");
    const body = response.body;

    Typed request and response models are exported from the package root. Notable groups:

    • Tenancy: Tenant, TenantStatus, TenantProvisioningState, TenantLoginContext.
    • Domains: Domain, DomainData, DomainStatus, DnsInstructionRecord, DnsRecordType, DomainListenerGrant, CreateDomainRequest, ReplaceDomainRequest.
    • Listeners: Listener, ListenerData, ListenerType, StreamType, TerminalActionType, TerminalActionPayload, CreateListenerRequest, UpdateListenerRequest, UpdateListenerDefaultTerminalActionRequest, UpdateListenerRspamdScanningRequest.
    • Routing: RoutingRule, RoutingRuleAction, RoutingRuleActionKind, RoutingRulePayload, ModifyHeaderOperation, ModifyHeaderOp, CreateRoutingRuleRequest, ReplaceRoutingRuleRequest, SetRoutingRuleActiveRequest, ReorderRoutingRuleRequest.
    • API keys and policy: APIKey, APIKeyData, IssuedAPIKey, SendingDomainPolicy, SendingDomainPolicyGrant, SendingDomainPolicyGrantRequest, SendingDomainSubdomainScope, ReplaceSendingDomainPolicyRequest.
    • Suppressions and recipient sets: TenantSuppression, SuppressionReason, RecipientSet, RecipientSetMember, RecipientSetMemberRequest, RecipientSetMembersBatchRequest, RecipientSetBatchResult, CreateRecipientSetRequest, UpdateRecipientSetRequest.
    • Auto-reply: AutoReplyTemplate, AutoReplyTemplateHeader, AutoReplySenderReadiness, AutoReplySenderReadinessStatus, CreateAutoReplyTemplateRequest, UpdateAutoReplyTemplateRequest, SetAutoReplyTemplateActiveRequest, MaskedTemplateField.
    • Forwarding, routes, relays, storage: SmtpForwardDestination, SmtpForwardDestinationConfirmation, SmtpForwardDestinationVerificationMethod, SmtpForwardDestinationVerificationStatus, InboundRoute, InboundRouteVerificationStatus, SmtpRelay, StorageIntegration, SetIntegrationActiveRequest.
    • Webhooks: WebhookEndpoint, WebhookDelivery, WebhookDeliveryKind, IssuedWebhookEndpoint, CreateWebhookEndpointRequest, UpdateWebhookEndpointRequest.
    • Identity: TenantIdentityProvider, IdentityProviderType, IdentityProviderLifecycleStatus, IdentityProvisioning, IdentityAccessClaim, ProviderOptions, LdapAttributes, and the CreateTenant*IdentityProviderRequest family.
    • Governance: AuditLog, AuditActorKind, AuditStatus, TenantResourceSearchResult, TenantSearchResourceType, TenantDedicatedIPPool.
    • Mail analytics: the MailAnalytics* family (metrics, dimensions, buckets, breakdowns, series, lifecycle, listener overviews, latency, and task-size statistics).
    • Shared: Problem, ProblemError, Page, Paged, Response, ListOptions, MIN_SEARCH_LENGTH, and the error classes.

    Timestamps are ISO-8601 strings. Contract enums are as const objects with a derived union.


    Requires Node.js 20.19 or later and pnpm.

    pnpm install     # install dependencies
    pnpm run build # dual ESM + CJS build with declarations (tsdown)
    pnpm run test # vitest
    pnpm run lint # oxlint (includes TSDoc validation)
    pnpm run format # oxfmt
    pnpm run docs # typedoc (Markdown) into docs/

    pnpm run check runs formatting, lint, typecheck, test, and build in sequence.

    Releases are managed with Changesets and published from GitHub Actions with npm provenance.

    Apache-2.0. See LICENSE and NOTICE.