From afcdd4663d8e4fdd37d2d30d3d70dba9fe55226f Mon Sep 17 00:00:00 2001 From: David Stockley Date: Tue, 10 Feb 2026 14:13:17 +0000 Subject: [PATCH] feat: add dashboard analytics to sdk and public api --- .../analytics/email-time-series.mdx | 3 + .../analytics/reputation-metrics.mdx | 3 + apps/docs/api-reference/openapi.json | 121 ++++++ apps/docs/docs.json | 7 + apps/web/src/server/api/routers/dashboard.ts | 117 +---- .../api/analytics/email-time-series.ts | 68 +++ .../api/analytics/reputation-metrics-data.ts | 48 +++ apps/web/src/server/public-api/index.ts | 6 + .../src/server/service/dashboard-service.ts | 133 ++++++ packages/sdk/src/analytics.ts | 56 +++ packages/sdk/src/usesend.ts | 2 + packages/sdk/types/schema.d.ts | 405 +++++++++++++++++- 12 files changed, 855 insertions(+), 114 deletions(-) create mode 100644 apps/docs/api-reference/analytics/email-time-series.mdx create mode 100644 apps/docs/api-reference/analytics/reputation-metrics.mdx create mode 100644 apps/web/src/server/public-api/api/analytics/email-time-series.ts create mode 100644 apps/web/src/server/public-api/api/analytics/reputation-metrics-data.ts create mode 100644 apps/web/src/server/service/dashboard-service.ts create mode 100644 packages/sdk/src/analytics.ts diff --git a/apps/docs/api-reference/analytics/email-time-series.mdx b/apps/docs/api-reference/analytics/email-time-series.mdx new file mode 100644 index 00000000..12512df0 --- /dev/null +++ b/apps/docs/api-reference/analytics/email-time-series.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/analytics/email-time-series +--- diff --git a/apps/docs/api-reference/analytics/reputation-metrics.mdx b/apps/docs/api-reference/analytics/reputation-metrics.mdx new file mode 100644 index 00000000..68d46620 --- /dev/null +++ b/apps/docs/api-reference/analytics/reputation-metrics.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/analytics/reputation-metrics +--- diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 65f74a46..fbbc3daa 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -2043,6 +2043,127 @@ } } } + }, + "/v1/analytics/email-time-series": { + "get": { + "parameters": [ + { + "schema": { + "type": "string", + "enum": ["7", "30"], + "example": "30" + }, + "required": false, + "name": "days", + "in": "query", + "description": "Number of days to retrieve data for (default: 30)" + }, + { + "schema": { "type": "string" }, + "required": false, + "name": "domainId", + "in": "query", + "description": "Filter by domain ID" + } + ], + "responses": { + "200": { + "description": "Retrieve email time series data", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { "type": "string" }, + "sent": { "type": "integer" }, + "delivered": { "type": "integer" }, + "opened": { "type": "integer" }, + "clicked": { "type": "integer" }, + "bounced": { "type": "integer" }, + "complained": { "type": "integer" } + }, + "required": [ + "date", + "sent", + "delivered", + "opened", + "clicked", + "bounced", + "complained" + ] + } + }, + "totalCounts": { + "type": "object", + "properties": { + "sent": { "type": "integer" }, + "delivered": { "type": "integer" }, + "opened": { "type": "integer" }, + "clicked": { "type": "integer" }, + "bounced": { "type": "integer" }, + "complained": { "type": "integer" } + }, + "required": [ + "sent", + "delivered", + "opened", + "clicked", + "bounced", + "complained" + ] + } + }, + "required": ["result", "totalCounts"] + } + } + } + } + } + } + }, + "/v1/analytics/reputation-metrics": { + "get": { + "parameters": [ + { + "schema": { "type": "string" }, + "required": false, + "name": "domainId", + "in": "query", + "description": "Filter by domain ID" + } + ], + "responses": { + "200": { + "description": "Retrieve reputation metrics data", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivered": { "type": "integer" }, + "hardBounced": { "type": "integer" }, + "complained": { "type": "integer" }, + "bounceRate": { "type": "number" }, + "complaintRate": { "type": "number" } + }, + "required": [ + "delivered", + "hardBounced", + "complained", + "bounceRate", + "complaintRate" + ] + } + } + } + } + } + } } } } diff --git a/apps/docs/docs.json b/apps/docs/docs.json index bb54345d..f7ed3b45 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -105,6 +105,13 @@ "api-reference/campaigns/pause-campaign", "api-reference/campaigns/resume-campaign" ] + }, + { + "group": "Analytics", + "pages": [ + "api-reference/analytics/email-time-series", + "api-reference/analytics/reputation-metrics" + ] } ] }, diff --git a/apps/web/src/server/api/routers/dashboard.ts b/apps/web/src/server/api/routers/dashboard.ts index 9e1e7c49..140cca38 100644 --- a/apps/web/src/server/api/routers/dashboard.ts +++ b/apps/web/src/server/api/routers/dashboard.ts @@ -1,9 +1,6 @@ -import { Prisma } from "@prisma/client"; -import { format, subDays } from "date-fns"; import { z } from "zod"; - import { createTRPCRouter, teamProcedure } from "~/server/api/trpc"; -import { db } from "~/server/db"; +import { emailTimeSeries, reputationMetricsData } from "~/server/service/dashboard-service"; export const dashboardRouter = createTRPCRouter({ emailTimeSeries: teamProcedure @@ -15,88 +12,10 @@ export const dashboardRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const { team } = ctx; - const days = input.days !== 7 ? 30 : 7; - - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - const isoStartDate = startDate.toISOString().split("T")[0]; - - type DailyEmailUsage = { - date: string; - sent: number; - delivered: number; - opened: number; - clicked: number; - bounced: number; - complained: number; - }; - - const result = await db.$queryRaw>` - SELECT - date, - SUM(sent)::integer AS sent, - SUM(delivered)::integer AS delivered, - SUM(opened)::integer AS opened, - SUM(clicked)::integer AS clicked, - SUM(bounced)::integer AS bounced, - SUM(complained)::integer AS complained - FROM "DailyEmailUsage" - WHERE "teamId" = ${team.id} - AND "date" >= ${isoStartDate} - ${input.domain ? Prisma.sql`AND "domainId" = ${input.domain}` : Prisma.sql``} - GROUP BY "date" - ORDER BY "date" ASC - `; - - // Fill in any missing dates with 0 values - const filledResult: DailyEmailUsage[] = []; - const endDateObj = new Date(); - for (let i = days; i > -1; i--) { - const dateStr = subDays(endDateObj, i) - .toISOString() - .split("T")[0] as string; - const existingData = result.find((r) => r.date === dateStr); + const response = await emailTimeSeries({team, days: input.days, domain: input.domain}) - if (existingData) { - filledResult.push({ - ...existingData, - date: format(dateStr, "MMM dd"), - }); - } else { - filledResult.push({ - date: format(dateStr, "MMM dd"), - sent: 0, - delivered: 0, - opened: 0, - clicked: 0, - bounced: 0, - complained: 0, - }); - } - } - - const totalCounts = result.reduce( - (acc, curr) => { - acc.sent += curr.sent; - acc.delivered += curr.delivered; - acc.opened += curr.opened; - acc.clicked += curr.clicked; - acc.bounced += curr.bounced; - acc.complained += curr.complained; - return acc; - }, - { - sent: 0, - delivered: 0, - opened: 0, - clicked: 0, - bounced: 0, - complained: 0, - } - ); - - return { result: filledResult, totalCounts }; + return response }), reputationMetricsData: teamProcedure @@ -107,34 +26,8 @@ export const dashboardRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const { team } = ctx; + const response = await reputationMetricsData({team, domain: input.domain}) - const reputations = await db.cumulatedMetrics.findMany({ - where: { - teamId: team.id, - ...(input.domain ? { domainId: input.domain } : {}), - }, - }); - - const results = reputations.reduce( - (acc, curr) => { - acc.delivered += Number(curr.delivered); - acc.hardBounced += Number(curr.hardBounced); - acc.complained += Number(curr.complained); - return acc; - }, - { delivered: 0, hardBounced: 0, complained: 0 } - ); - - const resultWithRates = { - ...results, - bounceRate: results.delivered - ? (results.hardBounced / results.delivered) * 100 - : 0, - complaintRate: results.delivered - ? (results.complained / results.delivered) * 100 - : 0, - }; - - return resultWithRates; + return response; }), }); diff --git a/apps/web/src/server/public-api/api/analytics/email-time-series.ts b/apps/web/src/server/public-api/api/analytics/email-time-series.ts new file mode 100644 index 00000000..a95a1dc3 --- /dev/null +++ b/apps/web/src/server/public-api/api/analytics/email-time-series.ts @@ -0,0 +1,68 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { emailTimeSeries as emailTimeSeriesService } from "~/server/service/dashboard-service"; + +const route = createRoute({ + method: "get", + path: "/v1/analytics/email-time-series", + request: { + query: z.object({ + days: z.enum(["7", "30"]).optional().openapi({ + description: "Number of days to retrieve data for (default: 30)", + example: "30", + }), + domainId: z.string().optional().openapi({ + description: "Filter by domain ID", + }), + }), + }, + responses: { + 200: { + description: "Retrieve email time series data", + content: { + "application/json": { + schema: z.object({ + result: z.array( + z.object({ + date: z.string(), + sent: z.number().int(), + delivered: z.number().int(), + opened: z.number().int(), + clicked: z.number().int(), + bounced: z.number().int(), + complained: z.number().int(), + }) + ), + totalCounts: z.object({ + sent: z.number().int(), + delivered: z.number().int(), + opened: z.number().int(), + clicked: z.number().int(), + bounced: z.number().int(), + complained: z.number().int(), + }), + }), + }, + }, + }, + }, +}); + +function emailTimeSeries(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const daysParam = c.req.query("days"); + const domainIdParam = c.req.query("domainId"); + + const days = daysParam ? Number(daysParam) : undefined; + const domain = + team.apiKey.domainId ?? + (domainIdParam ? Number(domainIdParam) : undefined); + + const data = await emailTimeSeriesService({ days, domain, team }); + + return c.json(data); + }); +} + +export default emailTimeSeries; diff --git a/apps/web/src/server/public-api/api/analytics/reputation-metrics-data.ts b/apps/web/src/server/public-api/api/analytics/reputation-metrics-data.ts new file mode 100644 index 00000000..1fc7f3dc --- /dev/null +++ b/apps/web/src/server/public-api/api/analytics/reputation-metrics-data.ts @@ -0,0 +1,48 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { reputationMetricsData as reputationMetricsDataService } from "~/server/service/dashboard-service"; + +const route = createRoute({ + method: "get", + path: "/v1/analytics/reputation-metrics", + request: { + query: z.object({ + domainId: z.string().optional().openapi({ + description: "Filter by domain ID", + }), + }), + }, + responses: { + 200: { + description: "Retrieve reputation metrics data", + content: { + "application/json": { + schema: z.object({ + delivered: z.number().int(), + hardBounced: z.number().int(), + complained: z.number().int(), + bounceRate: z.number(), + complaintRate: z.number(), + }), + }, + }, + }, + }, +}); + +function reputationMetricsData(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const domainIdParam = c.req.query("domainId"); + + const domain = + team.apiKey.domainId ?? + (domainIdParam ? Number(domainIdParam) : undefined); + + const data = await reputationMetricsDataService({ domain, team }); + + return c.json(data); + }); +} + +export default reputationMetricsData; diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts index 0a57bbb9..9ced78a4 100644 --- a/apps/web/src/server/public-api/index.ts +++ b/apps/web/src/server/public-api/index.ts @@ -27,6 +27,8 @@ import createContactBook from "./api/contacts/create-contact-book"; import getContactBook from "./api/contacts/get-contact-book"; import updateContactBook from "./api/contacts/update-contact-book"; import deleteContactBook from "./api/contacts/delete-contact-book"; +import emailTimeSeries from "./api/analytics/email-time-series"; +import reputationMetricsData from "./api/analytics/reputation-metrics-data"; export const app = getApp(); @@ -68,4 +70,8 @@ scheduleCampaign(app); pauseCampaign(app); resumeCampaign(app); +/**Analytics related APIs */ +emailTimeSeries(app); +reputationMetricsData(app); + export default app; diff --git a/apps/web/src/server/service/dashboard-service.ts b/apps/web/src/server/service/dashboard-service.ts new file mode 100644 index 00000000..90228111 --- /dev/null +++ b/apps/web/src/server/service/dashboard-service.ts @@ -0,0 +1,133 @@ +import { db } from "~/server/db"; +import { format, subDays } from "date-fns"; +import { Prisma, Team } from "@prisma/client"; + +type EmailTimeSeries = { + days?: number; + domain?: number + team: Team +}; + +export async function emailTimeSeries(input: EmailTimeSeries) { + const days = input.days !== 7 ? 30 : 7; + const { domain, team } = input + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + const isoStartDate = startDate.toISOString().split("T")[0]; + + type DailyEmailUsage = { + date: string; + sent: number; + delivered: number; + opened: number; + clicked: number; + bounced: number; + complained: number; + }; + + const result = await db.$queryRaw>` + SELECT + date, + SUM(sent)::integer AS sent, + SUM(delivered)::integer AS delivered, + SUM(opened)::integer AS opened, + SUM(clicked)::integer AS clicked, + SUM(bounced)::integer AS bounced, + SUM(complained)::integer AS complained + FROM "DailyEmailUsage" + WHERE "teamId" = ${team.id} + AND "date" >= ${isoStartDate} + ${domain ? Prisma.sql`AND "domainId" = ${domain}` : Prisma.sql``} + GROUP BY "date" + ORDER BY "date" ASC + `; + + // Fill in any missing dates with 0 values + const filledResult: DailyEmailUsage[] = []; + const endDateObj = new Date(); + + for (let i = days; i > -1; i--) { + const dateStr = subDays(endDateObj, i) + .toISOString() + .split("T")[0] as string; + const existingData = result.find((r) => r.date === dateStr); + + if (existingData) { + filledResult.push({ + ...existingData, + date: format(dateStr, "MMM dd"), + }); + } else { + filledResult.push({ + date: format(dateStr, "MMM dd"), + sent: 0, + delivered: 0, + opened: 0, + clicked: 0, + bounced: 0, + complained: 0, + }); + } + } + + const totalCounts = result.reduce( + (acc, curr) => { + acc.sent += curr.sent; + acc.delivered += curr.delivered; + acc.opened += curr.opened; + acc.clicked += curr.clicked; + acc.bounced += curr.bounced; + acc.complained += curr.complained; + return acc; + }, + { + sent: 0, + delivered: 0, + opened: 0, + clicked: 0, + bounced: 0, + complained: 0, + } + ); + + return { result: filledResult, totalCounts }; +} + + +type ReputationMetricsData = { + domain?: number + team: Team +}; + +export async function reputationMetricsData(input: ReputationMetricsData) { + const { domain, team } = input + + const reputations = await db.cumulatedMetrics.findMany({ + where: { + teamId: team.id, + ...(domain ? { domainId: domain } : {}), + }, + }); + + const results = reputations.reduce( + (acc, curr) => { + acc.delivered += Number(curr.delivered); + acc.hardBounced += Number(curr.hardBounced); + acc.complained += Number(curr.complained); + return acc; + }, + { delivered: 0, hardBounced: 0, complained: 0 } + ); + + const resultWithRates = { + ...results, + bounceRate: results.delivered + ? (results.hardBounced / results.delivered) * 100 + : 0, + complaintRate: results.delivered + ? (results.complained / results.delivered) * 100 + : 0, + }; + + return resultWithRates; +} diff --git a/packages/sdk/src/analytics.ts b/packages/sdk/src/analytics.ts new file mode 100644 index 00000000..7e235c93 --- /dev/null +++ b/packages/sdk/src/analytics.ts @@ -0,0 +1,56 @@ +import { paths } from "../types/schema"; +import { ErrorResponse } from "../types"; +import { UseSend } from "./usesend"; + +type EmailTimeSeriesQuery = + paths["/v1/analytics/email-time-series"]["get"]["parameters"]["query"]; + +type EmailTimeSeriesResponseSuccess = + paths["/v1/analytics/email-time-series"]["get"]["responses"]["200"]["content"]["application/json"]; + +type EmailTimeSeriesResponse = { + data: EmailTimeSeriesResponseSuccess | null; + error: ErrorResponse | null; +}; + +type ReputationMetricsQuery = + paths["/v1/analytics/reputation-metrics"]["get"]["parameters"]["query"]; + +type ReputationMetricsResponseSuccess = + paths["/v1/analytics/reputation-metrics"]["get"]["responses"]["200"]["content"]["application/json"]; + +type ReputationMetricsResponse = { + data: ReputationMetricsResponseSuccess | null; + error: ErrorResponse | null; +}; + +export class Analytics { + constructor(private readonly usesend: UseSend) { + this.usesend = usesend; + } + + async emailTimeSeries( + query?: EmailTimeSeriesQuery, + ): Promise { + const params = new URLSearchParams(); + if (query?.days) params.set("days", query.days); + if (query?.domainId) params.set("domainId", query.domainId); + + const qs = params.toString(); + const path = `/analytics/email-time-series${qs ? `?${qs}` : ""}`; + + return this.usesend.get(path); + } + + async reputationMetrics( + query?: ReputationMetricsQuery, + ): Promise { + const params = new URLSearchParams(); + if (query?.domainId) params.set("domainId", query.domainId); + + const qs = params.toString(); + const path = `/analytics/reputation-metrics${qs ? `?${qs}` : ""}`; + + return this.usesend.get(path); + } +} diff --git a/packages/sdk/src/usesend.ts b/packages/sdk/src/usesend.ts index 682c5966..b9b80bf2 100644 --- a/packages/sdk/src/usesend.ts +++ b/packages/sdk/src/usesend.ts @@ -3,6 +3,7 @@ import { Contacts } from "./contact"; import { Emails } from "./email"; import { Domains } from "./domain"; import { Campaigns } from "./campaign"; +import { Analytics } from "./analytics"; import { Webhooks } from "./webhooks"; const defaultBaseUrl = "https://app.usesend.com"; @@ -24,6 +25,7 @@ export class UseSend { readonly domains = new Domains(this); readonly contacts = new Contacts(this); readonly campaigns = new Campaigns(this); + readonly analytics = new Analytics(this); url = baseUrl; constructor( diff --git a/packages/sdk/types/schema.d.ts b/packages/sdk/types/schema.d.ts index 6ef77a9c..42b1db69 100644 --- a/packages/sdk/types/schema.d.ts +++ b/packages/sdk/types/schema.d.ts @@ -561,7 +561,9 @@ export interface paths { post: { parameters: { query?: never; - header?: never; + header?: { + "Idempotency-Key"?: string; + }; path?: never; cookie?: never; }; @@ -628,7 +630,9 @@ export interface paths { post: { parameters: { query?: never; - header?: never; + header?: { + "Idempotency-Key"?: string; + }; path?: never; cookie?: never; }; @@ -724,6 +728,300 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/contactBooks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve contact books accessible by the API key */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** + * @description The ID of the contact book + * @example clx1234567890 + */ + id: string; + /** + * @description The name of the contact book + * @example Newsletter Subscribers + */ + name: string; + /** + * @description The ID of the team + * @example 1 + */ + teamId: number; + /** + * @description Custom properties for the contact book + * @example { + * "customField1": "value1" + * } + */ + properties: { + [key: string]: string; + }; + /** + * @description The emoji associated with the contact book + * @example 📙 + */ + emoji: string; + /** @description The creation timestamp */ + createdAt: string; + /** @description The last update timestamp */ + updatedAt: string; + _count?: { + /** @description The number of contacts in the contact book */ + contacts?: number; + }; + }[]; + }; + }; + }; + }; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + emoji?: string; + properties?: { + [key: string]: string; + }; + }; + }; + }; + responses: { + /** @description Create a new contact book */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + name: string; + teamId: number; + properties: { + [key: string]: string; + }; + emoji: string; + createdAt: string; + updatedAt: string; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/contactBooks/{contactBookId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve the contact book */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + name: string; + teamId: number; + properties: { + [key: string]: string; + }; + emoji: string; + createdAt: string; + updatedAt: string; + _count?: { + contacts?: number; + }; + }; + }; + }; + /** @description Forbidden - API key doesn't have access */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + /** @description Contact book not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Contact book deleted successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + success: boolean; + message: string; + }; + }; + }; + /** @description Forbidden - API key doesn't have access */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + /** @description Contact book not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; + options?: never; + head?: never; + patch: { + parameters: { + query?: never; + header?: never; + path: { + contactBookId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name?: string; + emoji?: string; + properties?: { + [key: string]: string; + }; + }; + }; + }; + responses: { + /** @description Update the contact book */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + name: string; + teamId: number; + properties: { + [key: string]: string; + }; + emoji: string; + createdAt: string; + updatedAt: string; + }; + }; + }; + /** @description Forbidden - API key doesn't have access */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + /** @description Contact book not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; + trace?: never; + }; "/v1/contactBooks/{contactBookId}/contacts": { parameters: { query?: never; @@ -863,6 +1161,7 @@ export interface paths { header?: never; path: { contactBookId: string; + contactId: string; }; cookie?: never; }; @@ -1235,6 +1534,108 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/analytics/email-time-series": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + /** @description Number of days to retrieve data for (default: 30) */ + days?: "7" | "30"; + /** @description Filter by domain ID */ + domainId?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve email time series data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + result: { + date: string; + sent: number; + delivered: number; + opened: number; + clicked: number; + bounced: number; + complained: number; + }[]; + totalCounts: { + sent: number; + delivered: number; + opened: number; + clicked: number; + bounced: number; + complained: number; + }; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/analytics/reputation-metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + /** @description Filter by domain ID */ + domainId?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Retrieve reputation metrics data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + delivered: number; + hardBounced: number; + complained: number; + bounceRate: number; + complaintRate: number; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components {