Documentation

API Reference

Base URL: https://api.mailq.cloud/api/v1

Authentication

The MailQ API supports two authentication mechanisms. All endpoints require one of the two methods. Base URL: `https://api.mailq.cloud/api/v1`

GET
X-API-Key header

API Key Authentication

Authenticate using an API key by passing it in the `X-API-Key` header. Keys are prefixed with `mq_` for identification. Manage your keys at `/api/v1/apikeys`.

Header

X-API-Key: mq_XXXXXXXX_rest_of_key

Response 200

Authenticated

// Request proceeds normally

Error Responses

401Missing or invalid API key
GET
Authorization: Bearer header

M2M OIDC Bearer Token

Authenticate using an OAuth 2.1 client credentials token. The JWT must carry an `org` claim (or `organization`) holding your tenant's organization ID. Token endpoint: `https://id.mailq.cloud/connect/token` — fetch the exact value for your tenant from `/api/v1/m2m-applications/config`.

Header

Authorization: Bearer <jwt-token>

Response 200

Authenticated

// Request proceeds normally

Error Responses

401Missing or invalid token

Emails

Create, queue, and retrieve email messages.

POST
/api/v1/emails

Create & queue an email

Create and immediately queue a new outbound email message. Supports scheduling, attachments, CC/BCC, Reply-To, tags, metadata, custom headers, per-message tracking control, and inline images. Use the Idempotency-Key header for safe retries, and sandbox for dry-run validation.

ⓘ

A scheduled email comes back with `status: "Scheduled"` and the `scheduledFor` timestamp. A sandbox dry-run comes back with `status: "sandbox"`, `isSandbox: true` and an all-zero `id` — nothing is stored or delivered.

Request Headers

Idempotency-KeystringUnique key for this request (e.g. a UUID). If a message with the same key was already created for this tenant, the original result is returned without sending a duplicate. Use this for safe retries on network failures.

Email message parameters

{
  "from": "[email protected]",
  "to": "[email protected]",
  "cc": "[email protected]",
  "bcc": "[email protected]",
  "replyTo": "[email protected]",
  "subject": "Email Subject",
  "htmlBody": "<h1>HTML Content</h1>",
  "textBody": "Plain text fallback",
  "disableAutoPlainText": false,
  "scheduledFor": "2025-02-15T10:00:00Z",
  "scheduleForBusinessHours": false,
  "timezone": "America/New_York",
  "disableTracking": false,
  "tags": ["receipt", "order-123"],
  "metadata": { "orderId": "order-123", "customerId": "cust-456" },
  "customHeaders": { "X-Entity-Ref-ID": "order-123" },
  "sandbox": false,
  "attachments": [
    {
      "filename": "document.pdf",
      "contentType": "application/pdf",
      "content": "<base64>"
    },
    {
      "filename": "logo.png",
      "contentType": "image/png",
      "content": "<base64>",
      "contentId": "[email protected]",
      "disposition": "inline"
    }
  ]
}

Response 200

Email accepted and queued. The create response echoes only the message's identity and status — fetch the email by id to see bodies, tags, metadata, tracking and delivery detail.

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "messageId": "unique-message-id",
  "from": "[email protected]",
  "to": "[email protected]",
  "subject": "Email Subject",
  "status": "Pending",
  "statusEnum": "Pending",
  "scheduledFor": null,
  "createdAt": "2025-02-14T10:00:00Z"
}

Error Responses

400Domain not verifiedThe from address is not on a verified sending domain
400Invalid to addressExactly one recipient is required, and it must contain an @
400Reply-To not allowedThe reply-to address is not on the tenant's Reply-To allowlist
400invalid-tagsTags validation failed — max 10 tags, each max 50 chars
400invalid-metadataMetadata validation failed — max 20 pairs, key max 40 chars, value max 500 chars
400invalid-headerCustom header rejected — reserved name or value too long
400Recipient blacklistedThe recipient is on the bounce blacklist; a failed message record is created
401Unauthorized
413Message too largeMessage exceeds the size limit
429Rate limit exceededToo many requests
429Email quota exceededThe tenant's email quota is exhausted
429Storage quota exceededThe tenant's attachment storage quota is exhausted
GET
/api/v1/emails

List emails

Returns a paginated list of email messages for the current tenant, most recent first. Each item carries a summary of the message; fetch a single email to get its bodies, delivery logs, clicks and opens.

Query Parameters

skipintegerNumber of emails to skip (default: 0)
takeintegerNumber of emails to return (default: 50)

Response 200

Paginated list of emails

{
  "items": [ /* EmailMessageDto[] */ ],
  "total": 150,
  "skip": 0,
  "take": 50
}

Error Responses

401Unauthorized
GET
/api/v1/emails/{id}

Get email by ID

Retrieve a single email message by its UUID, including its SMTP delivery logs, tracked link clicks and individual opens.

ⓘ

Emails are scoped to your tenant. Requesting an email that belongs to another tenant returns 404, the same as an email that does not exist.

Path Parameters

iduuidEmail IDrequired

Response 200

Email message

{ /* EmailMessageDto */ }

Error Responses

401Unauthorized
404Email not found
GET
/api/v1/emails/{id}/clicks

Get link click statistics

Returns all tracked link clicks for a specific email.

ⓘ

The clicking party's IP address and user agent are recorded but deliberately not exposed through the public API.

Path Parameters

iduuidEmail IDrequired

Response 200

Array of link click records

[
  {
    "id": "uuid",
    "originalUrl": "https://example.com/page",
    "clickedAt": "2025-02-14T15:30:00Z",
    "createdAt": "2025-02-14T10:00:00Z"
  }
]

Error Responses

401Unauthorized
404Email not found
GET
/api/v1/emails/{emailId}/attachments/{attachmentId}

Download an attachment

Streams the raw bytes of an attachment belonging to one of your emails. The response is the file itself, with the attachment's original content type and filename — not JSON.

ⓘ

Attachment files are removed by the storage retention policy. Once removed, the attachment still appears on the email with `isAvailable: false`, and this endpoint returns 404.

Path Parameters

emailIduuidEmail IDrequired
attachmentIduuidAttachment ID, as returned in the email's `attachments` arrayrequired

Response 200

The attachment file bytes

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename=document.pdf

<binary>

Error Responses

401Unauthorized
404Email or attachment not found, or the file was removed by the retention policy

Domains

Verify and manage sending domains, including DKIM configuration and DNS validation.

GET
/api/v1/domains

List verified domains

Returns all verified sending domains for the current tenant.

Response 200

Array of domain objects

[
  {
    "id": "uuid",
    "domain": "mail.example.com",
    "isVerified": true,
    "verifiedAt": "2025-02-01T00:00:00Z",
    "dkimSelector": "mailq",
    "createdAt": "2025-01-15T00:00:00Z"
  }
]

Error Responses

401Unauthorized
GET
/api/v1/domains/{domainId}

Get domain details

Returns full details for a domain including DKIM and verification DNS records.

Path Parameters

domainIduuidDomain IDrequired

Response 200

Domain details with DNS records and statistics

{
  "id": "uuid",
  "domain": "mail.example.com",
  "isVerified": true,
  "verifiedAt": "2025-01-05T09:12:00Z",
  "dkimSelector": "mailq",
  "dkimDnsRecord": "v=DKIM1; k=rsa; p=MIGfMA0...",
  "verificationDnsRecord": "mailq-verification=token_value",
  "createdAt": "2025-01-01T00:00:00Z",
  "statistics": {
    "totalSent": 1250,
    "totalDelivered": 1200,
    "totalFailed": 50,
    "totalOpened": 800,
    "totalClicked": 250
  }
}

Error Responses

401Unauthorized
404Domain not found
GET
/api/v1/domains/{domainId}/dkim

Get DKIM DNS record

Returns the DKIM DNS record to add to your domain registrar.

Path Parameters

domainIduuidDomain IDrequired

Response 200

DKIM record details

{
  "selector": "mailq",
  "domain": "mail.example.com",
  "dnsRecord": "mailq._domainkey.mail.example.com",
  "dnsValue": "v=DKIM1; k=rsa; p=MIGfMA0..."
}

Error Responses

401Unauthorized
404Domain not found
POST
/api/v1/domains/{domainId}/check-dns

Verify DNS records

Triggers a DNS check for the domain and returns validation results for each expected record.

Path Parameters

domainIduuidDomain IDrequired

Response 200

DNS validation results

{
  "isValid": true,
  "records": [
    {
      "type": "TXT",
      "name": "mailq._domainkey.mail.example.com",
      "expectedValue": "v=DKIM1; k=rsa; p=...",
      "actualValue": "v=DKIM1; k=rsa; p=...",
      "isValid": true,
      "error": null
    }
  ]
}

Error Responses

401Unauthorized
404Domain not found
GET
/api/v1/domains/{domainId}/statistics

Get domain statistics

Returns email delivery statistics for a domain over a date range.

Path Parameters

domainIduuidDomain IDrequired

Query Parameters

fromdatetimeStart date (default: 30 days ago)
todatetimeEnd date (default: today)

Response 200

Domain statistics

{
  "totalSent": 1250,
  "totalDelivered": 1200,
  "totalFailed": 50,
  "totalOpened": 800,
  "totalClicked": 250
}

Error Responses

401Unauthorized
404Domain not found
DELETE
/api/v1/domains/{domainId}

Delete domain

Removes a domain from the tenant. This does not affect previously sent emails.

Path Parameters

domainIduuidDomain IDrequired

Response 204

Domain deleted

// No content

Error Responses

401Unauthorized
404Domain not found

Templates

Send emails using pre-built multilingual Liquid templates. Templates support the full Liquid syntax: output values with `{{ name }}`, loop with `{% for item in items %}`, and branch with `{% if condition %}`. Reusable HTML snippets (headers, footers, signatures) can be extracted into partials and included in any template with `{% include 'slug' %}` — the partial matching the send language is resolved automatically at render time. Asset filters (`asset_url`, `asset_inline`) let you reference uploaded images by filename. See the Liquid Reference in the sidebar for the full syntax guide.

POST
/api/v1/templates/{templateId}/send

Send from template

Renders and queues a template email. Reference the template by its UUID or slug. Pass a `language` and any Liquid `parameters` required by the template. If the template body contains `{% include 'slug' %}` tags, the matching partial for the given language is resolved and rendered in place. Asset filters (`asset_url`, `asset_inline`) are resolved against the tenant's uploaded asset library at render time. Apart from the subject and HTML body, which come from the template, this endpoint accepts the same options as POST /api/v1/emails — attachments, tags, metadata, custom headers, tracking control, scheduling, sandbox mode and the Idempotency-Key header.

ⓘ

Partial templates (created in the portal with the Partial flag) cannot be sent directly — they are only resolved when included from another template. A template has no plain text variant, so `textBody` is not accepted; set `disableAutoPlainText` to send HTML-only.

Path Parameters

templateIduuid | slugTemplate ID or slugrequired

Request Headers

Idempotency-KeystringUnique key for this request (e.g. a UUID). If a message with the same key was already created for this tenant, the original result is returned without sending a duplicate. Use this for safe retries on network failures.

Template send parameters

{
  "language": "en",
  "from": "[email protected]",
  "to": "[email protected]",
  "cc": "[email protected]",
  "bcc": "[email protected]",
  "replyTo": "[email protected]",
  "parameters": {
    "user": {
      "name": "John Doe",
      "email": "[email protected]"
    },
    "items": [
      { "name": "Item 1", "price": 100 }
    ]
  },
  "scheduledFor": "2025-02-15T10:00:00Z",
  "scheduleForBusinessHours": false,
  "timezone": "America/New_York",
  "disableAutoPlainText": false,
  "disableTracking": false,
  "tags": ["welcome", "onboarding"],
  "metadata": { "orderId": "order-123" },
  "customHeaders": { "X-Entity-Ref-ID": "order-123" },
  "sandbox": false,
  "attachments": [
    {
      "filename": "document.pdf",
      "contentType": "application/pdf",
      "content": "<base64>"
    }
  ]
}

Response 200

Email queued (same shape as POST /emails response)

{ /* EmailMessageDto */ }

Error Responses

400Language required, language not found, or template render error
400Domain not verifiedThe from address is not on a verified sending domain
400invalid-tagsTags validation failed — max 10 tags, each max 50 chars
400invalid-metadataMetadata validation failed — max 20 pairs, key max 40 chars, value max 500 chars
400invalid-headerCustom header rejected — reserved name or value too long
401Unauthorized
404Template not found, inactive, or a partial
413Message too largeRendered message exceeds the size limit
429Rate limit exceededToo many requests
429Email quota exceededThe tenant's email quota is exhausted
429Storage quota exceededThe tenant's attachment storage quota is exhausted

Statistics

Aggregate email delivery statistics over a date range.

GET
/api/v1/statistics

Get email statistics

Returns daily delivery statistics (sent, delivered, failed, bounced, opened, clicked) for the specified date range.

Query Parameters

fromdatetimeStart date (default: 30 days ago)
todatetimeEnd date (default: today)

Response 200

Statistics with daily breakdown

{
  "period": {
    "from": "2025-01-15T00:00:00Z",
    "to": "2025-02-14T23:59:59Z"
  },
  "dailyStatistics": [
    {
      "date": "2025-01-15T00:00:00Z",
      "sent": 150,
      "delivered": 145,
      "failed": 5,
      "bounced": 0,
      "inbound": 2,
      "opened": 80,
      "clicked": 20
    }
  ]
}

Error Responses

401Unauthorized

Billing

Retrieve billing records and current quota usage.

GET
/api/v1/billing

Get billing records

Returns billing records for the specified date range.

Query Parameters

startDatedatetimeStart date (default: 1 month ago)
endDatedatetimeEnd date (default: today)

Response 200

Array of billing records

[
  {
    "id": "uuid",
    "billingDate": "2025-02-01T00:00:00Z",
    "emailsSent": 5000,
    "emailsDelivered": 4950
  }
]

Error Responses

401Unauthorized
GET
/api/v1/billing/quota

Get current quota usage

Returns remaining email sends and storage for the current billing period.

Response 200

Remaining quota

{
  "remainingEmails": 95000,
  "remainingStorageBytes": 9500000000
}

Error Responses

401Unauthorized

Tenants

Manage tenant information, quota, and business hours scheduling settings.

GET
/api/v1/tenants/quota

Get tenant quota

Returns the full quota configuration and current usage for the tenant.

Response 200

Quota details

{
  "emailQuota": 100000,
  "currentMonthEmailsSent": 5000,
  "remainingEmails": 95000,
  "storageQuotaBytes": 10000000000,
  "currentStorageBytes": 500000000,
  "remainingStorageBytes": 9500000000
}
GET
/api/v1/tenants/current

Get current tenant status

Returns the currently active tenant for the authenticated session.

Response 200

Current tenant

{
  "hasTenant": true,
  "tenantId": "uuid",
  "tenantName": "Acme Corporation",
  "message": null
}
GET
/api/v1/tenants/available

Get available tenants

Returns all tenants available to the authenticated user via their organization claims.

Response 200

Available tenants

{
  "tenants": [
    {
      "id": "uuid",
      "name": "Acme Corp",
      "organizationId": "org_id",
      "role": "admin"
    }
  ],
  "selectedTenantId": "uuid"
}
GET
/api/v1/tenants/business-hours

Get business hours settings

Returns the business hours configuration used for scheduled sends.

Response 200

Business hours settings

{
  "timezone": "America/New_York",
  "businessHoursEnabled": true,
  "businessHoursStart": 9,
  "businessHoursEnd": 17
}
PUT
/api/v1/tenants/business-hours

Update business hours settings

Updates the business hours window used when `scheduleForBusinessHours: true` is set on an email.

Business hours configuration

{
  "timezone": "America/New_York",
  "businessHoursEnabled": true,
  "businessHoursStart": 9,
  "businessHoursEnd": 17
}

Response 200

Updated business hours settings

{ /* BusinessHoursSettingsDto */ }

API Keys

List and manage API keys for the tenant.

GET
/api/v1/apikeys

List API keys

Returns all API keys for the current tenant. Secret values are masked — only the prefix is shown.

Response 200

Array of API key records

[
  {
    "id": "uuid",
    "name": "Production Key",
    "key": "mq_abc123_***",
    "expiresAt": "2026-02-14T00:00:00Z",
    "isActive": true,
    "createdAt": "2024-02-14T00:00:00Z",
    "lastUsedAt": "2025-02-14T10:00:00Z"
  }
]

Webhooks

Configure HTTP webhooks to receive real-time email lifecycle events. Webhook event payloads include tags and metadata from the original message. Optionally enable payload signing per webhook for request verification.

GET
/api/v1/webhooks

List webhooks

Returns all configured webhook endpoints for the tenant, including call statistics. The signing secret is never returned here — it is only returned on creation and rotation.

Response 200

Array of webhook configurations

[
  {
    "id": "uuid",
    "name": "Delivery Notification",
    "url": "https://your-app.com/webhooks/delivered",
    "method": "POST",
    "subscribeToDelivered": true,
    "subscribeToOpened": true,
    "subscribeToClicked": true,
    "subscribeToBounced": false,
    "subscribeToFailed": true,
    "isActive": true,
    "totalCalls": 15000,
    "successfulCalls": 14950,
    "failedCalls": 50,
    "lastCalledAt": "2025-02-14T10:00:00Z"
  }
]
GET
/api/v1/webhooks/{id}

Get webhook by ID

Returns a single webhook configuration by its UUID.

Path Parameters

iduuidWebhook IDrequired

Response 200

Webhook configuration

{ /* WebhookDto */ }

Error Responses

404Webhook not found
POST
/api/portal/webhooks/{id}/rotate-secret

Rotate signing secret

Generates a new signing secret for the webhook and returns it once. The previous secret is immediately invalidated. Store the returned secret securely — it cannot be retrieved again.

Path Parameters

iduuidWebhook IDrequired

Response 200

New signing secret (shown once — store it immediately)

{ "secret": "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" }

Error Responses

401Unauthorized — this is a Portal API endpoint and requires a portal session, not an API key
403Forbidden — requires the Admin role
404Webhook not found
POST
→ your endpoint (incoming request)

Payload signing & verification

When signing is enabled on a webhook or on a webhook routing rule, every request MailQ sends to your endpoint includes two additional HTTP request headers. You use these headers to verify that the request genuinely came from MailQ and has not been tampered with. Event webhooks and routing rule webhooks use exactly the same scheme; each has its own secret.

ⓘ

Signing is optional. Enable it per webhook or per webhook routing rule in the portal. The secret is generated once on creation and shown only then — or regenerated via rotate-secret.

Request Headers

X-MailQ-TimestampstringUnix timestamp (seconds) of when the request was sent, e.g. 1717498800. Validate this is within ±300 seconds of your server time to reject replayed requests.
X-MailQ-SignaturestringHMAC-SHA256 signature of the signed payload, prefixed with "sha256=". Example: sha256=b94d27b9934d3e08a52e52d7da7dabfac484efe04294e576b4b83d40c5e2e2a6

Signed payload (what is hashed)

// The string that is signed is:
//   {X-MailQ-Timestamp}.{raw JSON body}
//
// Example — timestamp 1717498800, body as received:
"1717498800.{\"event\":\"email.delivered\",\"timestamp\":\"2025-02-14T10:00:00Z\", ...}"

// HMAC-SHA256 of the above string using your secret key.
// The result is hex-encoded and prefixed with "sha256=".

Response 200

Example signed webhook POST body (email.delivered)

{
  "event": "email.delivered",
  "timestamp": "2025-02-14T10:00:00Z",
  "tags": ["receipt", "order-123"],
  "metadata": { "orderId": "order-123" },
  "email": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "messageId": "<guid@mailq>",
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Your order confirmation",
    "deliveredAt": "2025-02-14T10:00:05Z"
  }
}

Verification Examples

import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(
  secret: string,
  timestampHeader: string,
  signatureHeader: string,
  rawBody: string,          // the raw request body string — do NOT parse first
  toleranceSecs = 300
): boolean {
  // 1. Reject stale requests
  const ts = parseInt(timestampHeader, 10);
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSecs) return false;

  // 2. Recompute the signature
  const signed = `${timestampHeader}.${rawBody}`;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(signed)
    .digest("hex");

  // 3. Constant-time comparison (prevents timing attacks)
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const valid = verifyWebhook(
    process.env.MAILQ_WEBHOOK_SECRET!,
    req.headers["x-mailq-timestamp"] as string,
    req.headers["x-mailq-signature"] as string,
    req.body.toString()   // raw Buffer → string
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // handle event.event === "email.delivered" etc.
  res.sendStatus(200);
});

Routing

Configure inbound and outbound email routing rules. Supports forwarding to SMTP, email addresses, or webhooks.

GET
/api/v1/routing

List routing rules

Returns all routing rules for the tenant. Rules are evaluated in priority order.

Response 200

Array of routing rules

[
  {
    "id": "uuid",
    "name": "Route to Gmail",
    "type": "Smtp",
    "smtpHost": "smtp.gmail.com",
    "smtpPort": 587,
    "smtpUseSsl": false,
    "fromPattern": "*",
    "toPattern": "*",
    "subjectPattern": "*",
    "hasWebhookSecret": false,
    "priority": 10,
    "isActive": true
  }
]
GET
/api/v1/routing/{id}

Get routing rule by ID

Returns a single routing rule. Routing types: `Smtp`, `Email`, `Webhook`. `hasWebhookSecret` is true when the rule's webhook requests are signed (X-MailQ-Timestamp / X-MailQ-Signature, see Payload signing & verification); the secret itself is never returned.

Path Parameters

iduuidRouting rule IDrequired

Response 200

Routing rule

{ /* RoutingRuleDto */ }

Error Responses

404Rule not found
POST
/api/portal/routing/{id}/rotate-secret

Rotate routing webhook signing secret

Generates a new signing secret for a webhook routing rule and returns it once; also enables signing on a rule that had none. The previous secret is immediately invalidated. Store the returned secret securely — it cannot be retrieved again. Signing can also be enabled when creating the rule in the portal.

Path Parameters

iduuidRouting rule IDrequired

Response 200

New signing secret (shown once — store it immediately)

{ "secret": "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" }

Error Responses

400routing_rule_not_webhook — only webhook routing rules can be signed
403Forbidden — requires the Admin role
404Routing rule not found

Audit Logs

Query the immutable audit trail for all API activity in the tenant.

GET
/api/v1/audit-logs

Get audit logs

Returns paginated audit log entries. Each entry records the action, entity, authenticated user, authentication method, IP address, and HTTP details.

Query Parameters

fromdatetimeStart date
todatetimeEnd date
keywordstringFilter by keyword
skipintegerOffset (default: 0)
takeintegerLimit (default: 50)

Response 200

Paginated audit log entries

{
  "items": [
    {
      "id": "uuid",
      "action": "Email:Create",
      "entityType": "Email",
      "entityId": "uuid",
      "message": "Email created and queued",
      "userId": "user_id",
      "userEmail": "[email protected]",
      "authenticationMethod": "ApiKey",
      "ipAddress": "192.168.1.1",
      "httpMethod": "POST",
      "requestPath": "/api/v1/emails",
      "responseStatusCode": 200,
      "createdAt": "2025-02-14T10:00:00Z"
    }
  ],
  "total": 1500,
  "skip": 0,
  "take": 50
}

M2M Applications

Manage OAuth 2.1 Machine-to-Machine application credentials via Logto.

GET
/api/v1/m2m-applications/config

Get M2M configuration

Returns the M2M configuration including organization ID and token endpoint needed to obtain bearer tokens.

Response 200

M2M configuration

{
  "organizationId": "org_123abc",
  "tokenEndpoint": "https://id.mailq.cloud/connect/token",
  "endpoint": "https://id.mailq.cloud",
  "isConfigured": true,
  "hasClient": true,
  "hasOrganizationId": true
}

Error Responses

401Unauthorized
GET
/api/v1/m2m-applications

List M2M applications

Returns all M2M application registrations for the tenant.

Response 200

Array of M2M applications

[
  {
    "id": "uuid",
    "name": "Backend Service",
    "clientId": "client_id_from_logto",
    "hasSecret": true,
    "createdAt": "2025-01-01T00:00:00Z",
    "lastUsedAt": "2025-02-14T10:00:00Z",
    "isActive": true
  }
]