From c87b587a05f1f2350bab7bc39f5b0821a6241234 Mon Sep 17 00:00:00 2001 From: David Stockley Date: Sun, 4 Jan 2026 17:48:55 +0000 Subject: [PATCH 1/7] feat: contact books public api --- .../contact-books/create-contact-book.mdx | 3 + .../contact-books/delete-contact-book.mdx | 3 + .../contact-books/get-contact-book.mdx | 3 + .../contact-books/list-contact-books.mdx | 3 + .../contact-books/update-contact-book.mdx | 3 + apps/docs/api-reference/openapi.json | 339 ++++++++++++++++++ apps/docs/docs.json | 330 ++++++++--------- apps/web/src/lib/zod/contact-book-schema.ts | 23 ++ .../api/contact-books/create-contact-book.ts | 67 ++++ .../api/contact-books/delete-contact-book.ts | 86 +++++ .../api/contact-books/get-contact-book.ts | 85 +++++ .../api/contact-books/get-contact-books.ts | 37 ++ .../api/contact-books/update-contact-book.ts | 96 +++++ apps/web/src/server/public-api/index.ts | 12 + 14 files changed, 930 insertions(+), 160 deletions(-) create mode 100644 apps/docs/api-reference/contact-books/create-contact-book.mdx create mode 100644 apps/docs/api-reference/contact-books/delete-contact-book.mdx create mode 100644 apps/docs/api-reference/contact-books/get-contact-book.mdx create mode 100644 apps/docs/api-reference/contact-books/list-contact-books.mdx create mode 100644 apps/docs/api-reference/contact-books/update-contact-book.mdx create mode 100644 apps/web/src/lib/zod/contact-book-schema.ts create mode 100644 apps/web/src/server/public-api/api/contact-books/create-contact-book.ts create mode 100644 apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts create mode 100644 apps/web/src/server/public-api/api/contact-books/get-contact-book.ts create mode 100644 apps/web/src/server/public-api/api/contact-books/get-contact-books.ts create mode 100644 apps/web/src/server/public-api/api/contact-books/update-contact-book.ts diff --git a/apps/docs/api-reference/contact-books/create-contact-book.mdx b/apps/docs/api-reference/contact-books/create-contact-book.mdx new file mode 100644 index 00000000..c3be3e95 --- /dev/null +++ b/apps/docs/api-reference/contact-books/create-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/contact-books +--- diff --git a/apps/docs/api-reference/contact-books/delete-contact-book.mdx b/apps/docs/api-reference/contact-books/delete-contact-book.mdx new file mode 100644 index 00000000..5685304e --- /dev/null +++ b/apps/docs/api-reference/contact-books/delete-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /v1/contact-books/{id} +--- diff --git a/apps/docs/api-reference/contact-books/get-contact-book.mdx b/apps/docs/api-reference/contact-books/get-contact-book.mdx new file mode 100644 index 00000000..c82cd986 --- /dev/null +++ b/apps/docs/api-reference/contact-books/get-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/contact-books/{id} +--- diff --git a/apps/docs/api-reference/contact-books/list-contact-books.mdx b/apps/docs/api-reference/contact-books/list-contact-books.mdx new file mode 100644 index 00000000..300578fa --- /dev/null +++ b/apps/docs/api-reference/contact-books/list-contact-books.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/contact-books +--- diff --git a/apps/docs/api-reference/contact-books/update-contact-book.mdx b/apps/docs/api-reference/contact-books/update-contact-book.mdx new file mode 100644 index 00000000..e65ced09 --- /dev/null +++ b/apps/docs/api-reference/contact-books/update-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: patch /v1/contact-books/{id} +--- diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 3e6610d4..56d85372 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -1694,6 +1694,345 @@ } } } + }, + "/v1/contact-books": { + "get": { + "responses": { + "200": { + "description": "Retrieve contact books accessible by the API key", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the contact book", + "example": "clx1234567890" + }, + "name": { + "type": "string", + "description": "The name of the contact book", + "example": "Newsletter Subscribers" + }, + "teamId": { + "type": "number", + "description": "The ID of the team", + "example": 1 + }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" }, + "description": "Custom properties for the contact book", + "example": { "customField1": "value1" } + }, + "emoji": { + "type": "string", + "description": "The emoji associated with the contact book", + "example": "📙" + }, + "createdAt": { + "type": "string", + "description": "The creation timestamp" + }, + "updatedAt": { + "type": "string", + "description": "The last update timestamp" + }, + "_count": { + "type": "object", + "properties": { + "contacts": { + "type": "number", + "description": "The number of contacts in the contact book" + } + } + } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "emoji": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "Create a new contact book", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "teamId": { "type": "number" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "emoji": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/contact-books/{id}": { + "get": { + "parameters": [ + { + "schema": { "type": "string", "example": "clx1234567890" }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Retrieve the contact book", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "teamId": { "type": "number" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "emoji": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "_count": { + "type": "object", + "properties": { + "contacts": { "type": "number" } + } + } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Contact book not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "parameters": [ + { + "schema": { "type": "string", "example": "clx1234567890" }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "emoji": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Update the contact book", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "teamId": { "type": "number" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "emoji": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Contact book not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "parameters": [ + { + "schema": { "type": "string", "example": "clx1234567890" }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Contact book deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "success": { "type": "boolean" }, + "message": { "type": "string" } + }, + "required": ["id", "success", "message"] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Contact book not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } } } } diff --git a/apps/docs/docs.json b/apps/docs/docs.json index eac7cf29..4df07063 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -1,162 +1,172 @@ { - "$schema": "https://mintlify.com/docs.json", - "theme": "maple", - "name": "useSend", - "colors": { - "primary": "#21453D", - "light": "#E6FAF5", - "dark": "#21453D" - }, - "background": { - "color": { - "light": "#F5F5F5", - "dark": "#181825" - } - }, - "fonts": { - "family": "IBM Plex Mono" - }, - "favicon": "/favicon.svg", - "navigation": { - "tabs": [ - { - "tab": "Documentation", - "groups": [ - { - "group": "Getting Started", - "pages": [ - "introduction", - "get-started/nodejs", - "get-started/python", - "get-started/local", - "get-started/smtp" - ] - }, - { - "group": "Self Hosting", - "pages": ["self-hosting/overview", "self-hosting/railway"] - }, - { - "group": "Guides", - "pages": ["guides/use-with-react-email"] - }, - { - "group": "Community SDKs", - "pages": ["community-sdk/python", "community-sdk/go"] - } - ] - }, - { - "tab": "API Reference", - "groups": [ - { - "group": "API Reference", - "pages": ["api-reference/introduction"] - }, - { - "group": "Emails", - "pages": [ - "api-reference/emails/get-email", - "api-reference/emails/list-emails", - "api-reference/emails/send-email", - "api-reference/emails/batch-email", - "api-reference/emails/update-schedule", - "api-reference/emails/cancel-schedule" - ] - }, - { - "group": "Contacts", - "pages": [ - "api-reference/contacts/get-contact", - "api-reference/contacts/get-contacts", - "api-reference/contacts/create-contact", - "api-reference/contacts/update-contact", - "api-reference/contacts/upsert-contact", - "api-reference/contacts/delete-contact" - ] - }, - { - "group": "Domains", - "pages": [ - "api-reference/domains/get-domain", - "api-reference/domains/list-domains", - "api-reference/domains/create-domain", - "api-reference/domains/verify-domain", - "api-reference/domains/delete-domain" - ] - }, - { - "group": "Campaigns", - "pages": [ - "api-reference/campaigns/create-campaign", - "api-reference/campaigns/get-campaign", - "api-reference/campaigns/schedule-campaign", - "api-reference/campaigns/pause-campaign", - "api-reference/campaigns/resume-campaign" - ] - } - ] - }, - { - "tab": "Changelog", - "groups": [ - { - "group": "Updates", - "pages": ["changelog"] - } - ] - } - ], - "global": { - "anchors": [ - { - "anchor": "GitHub", - "href": "https://github.com/usesend/usesend", - "icon": "github" - }, - { - "anchor": "Community", - "href": "https://discord.gg/BU8n8pJv8S", - "icon": "discord" - } - ] - } - }, - "logo": { - "light": "/logo/logo-wordmark.svg", - "dark": "/logo/logo-wordmark-dark.svg" - }, - "api": { - "playground": { - "display": "interactive" - }, - "mdx": { - "server": "https://mintlify.com/api", - "auth": { - "method": "bearer" - } - } - }, - "navbar": { - "links": [ - { - "label": "Support", - "href": "mailto:hey@usesend.com" - } - ], - "primary": { - "type": "button", - "label": "Dashboard", - "href": "https://app.usesend.com" - } - }, - "footer": { - "socials": { - "x": "https://x.com/useSend_com", - "github": "https://github.com/usesend" - } - }, - "contextual": { - "options": ["copy", "view", "chatgpt", "claude", "perplexity"] - } + "$schema": "https://mintlify.com/docs.json", + "theme": "maple", + "name": "useSend", + "colors": { + "primary": "#21453D", + "light": "#E6FAF5", + "dark": "#21453D" + }, + "background": { + "color": { + "light": "#F5F5F5", + "dark": "#181825" + } + }, + "fonts": { + "family": "IBM Plex Mono" + }, + "favicon": "/favicon.svg", + "navigation": { + "tabs": [ + { + "tab": "Documentation", + "groups": [ + { + "group": "Getting Started", + "pages": [ + "introduction", + "get-started/nodejs", + "get-started/python", + "get-started/local", + "get-started/smtp" + ] + }, + { + "group": "Self Hosting", + "pages": ["self-hosting/overview", "self-hosting/railway"] + }, + { + "group": "Guides", + "pages": ["guides/use-with-react-email"] + }, + { + "group": "Community SDKs", + "pages": ["community-sdk/python", "community-sdk/go"] + } + ] + }, + { + "tab": "API Reference", + "groups": [ + { + "group": "API Reference", + "pages": ["api-reference/introduction"] + }, + { + "group": "Emails", + "pages": [ + "api-reference/emails/get-email", + "api-reference/emails/list-emails", + "api-reference/emails/send-email", + "api-reference/emails/batch-email", + "api-reference/emails/update-schedule", + "api-reference/emails/cancel-schedule" + ] + }, + { + "group": "Contact Books", + "pages": [ + "api-reference/contact-books/list-contact-books", + "api-reference/contact-books/get-contact-book", + "api-reference/contact-books/create-contact-book", + "api-reference/contact-books/update-contact-book", + "api-reference/contact-books/delete-contact-book" + ] + }, + { + "group": "Contacts", + "pages": [ + "api-reference/contacts/get-contact", + "api-reference/contacts/get-contacts", + "api-reference/contacts/create-contact", + "api-reference/contacts/update-contact", + "api-reference/contacts/upsert-contact", + "api-reference/contacts/delete-contact" + ] + }, + { + "group": "Domains", + "pages": [ + "api-reference/domains/get-domain", + "api-reference/domains/list-domains", + "api-reference/domains/create-domain", + "api-reference/domains/verify-domain", + "api-reference/domains/delete-domain" + ] + }, + { + "group": "Campaigns", + "pages": [ + "api-reference/campaigns/create-campaign", + "api-reference/campaigns/get-campaign", + "api-reference/campaigns/schedule-campaign", + "api-reference/campaigns/pause-campaign", + "api-reference/campaigns/resume-campaign" + ] + } + ] + }, + { + "tab": "Changelog", + "groups": [ + { + "group": "Updates", + "pages": ["changelog"] + } + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/usesend/usesend", + "icon": "github" + }, + { + "anchor": "Community", + "href": "https://discord.gg/BU8n8pJv8S", + "icon": "discord" + } + ] + } + }, + "logo": { + "light": "/logo/logo-wordmark.svg", + "dark": "/logo/logo-wordmark-dark.svg" + }, + "api": { + "playground": { + "display": "interactive" + }, + "mdx": { + "server": "https://mintlify.com/api", + "auth": { + "method": "bearer" + } + } + }, + "navbar": { + "links": [ + { + "label": "Support", + "href": "mailto:hey@usesend.com" + } + ], + "primary": { + "type": "button", + "label": "Dashboard", + "href": "https://app.usesend.com" + } + }, + "footer": { + "socials": { + "x": "https://x.com/useSend_com", + "github": "https://github.com/usesend" + } + }, + "contextual": { + "options": ["copy", "view", "chatgpt", "claude", "perplexity"] + } } diff --git a/apps/web/src/lib/zod/contact-book-schema.ts b/apps/web/src/lib/zod/contact-book-schema.ts new file mode 100644 index 00000000..d5e0c0c6 --- /dev/null +++ b/apps/web/src/lib/zod/contact-book-schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +export const ContactBookSchema = z.object({ + id: z + .string() + .openapi({ description: "The ID of the contact book", example: "clx1234567890" }), + name: z + .string() + .openapi({ description: "The name of the contact book", example: "Newsletter Subscribers" }), + teamId: z.number().openapi({ description: "The ID of the team", example: 1 }), + properties: z.record(z.string()).openapi({ + description: "Custom properties for the contact book", + example: { customField1: "value1" }, + }), + emoji: z + .string() + .openapi({ description: "The emoji associated with the contact book", example: "📙" }), + createdAt: z.string().openapi({ description: "The creation timestamp" }), + updatedAt: z.string().openapi({ description: "The last update timestamp" }), + _count: z.object({ + contacts: z.number().openapi({ description: "The number of contacts in the contact book" }), + }).optional(), +}); diff --git a/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts new file mode 100644 index 00000000..9e957094 --- /dev/null +++ b/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts @@ -0,0 +1,67 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { ContactBookSchema } from "~/lib/zod/contact-book-schema"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { createContactBook as createContactBookService } from "~/server/service/contact-book-service"; + +const route = createRoute({ + method: "post", + path: "/v1/contact-books", + request: { + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + name: z.string().min(1), + emoji: z.string().optional(), + properties: z.record(z.string()).optional(), + }), + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ContactBookSchema, + }, + }, + description: "Create a new contact book", + }, + }, +}); + +function createContactBook(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const body = c.req.valid("json"); + + console.log({ team }); + + const contactBook = await createContactBookService(team.id, body.name); + + // Update emoji and properties if provided + if (body.emoji || body.properties) { + const { updateContactBook } = await import( + "~/server/service/contact-book-service" + ); + const updated = await updateContactBook(contactBook.id, { + emoji: body.emoji, + properties: body.properties, + }); + + return c.json({ + ...updated, + properties: updated.properties as Record, + }); + } + + return c.json({ + ...contactBook, + properties: contactBook.properties as Record, + }); + }); +} + +export default createContactBook; diff --git a/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts new file mode 100644 index 00000000..fe4037b1 --- /dev/null +++ b/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts @@ -0,0 +1,86 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "../../hono"; +import { db } from "~/server/db"; +import { UnsendApiError } from "../../api-error"; +import { deleteContactBook as deleteContactBookService } from "~/server/service/contact-book-service"; + +const route = createRoute({ + method: "delete", + path: "/v1/contact-books/{id}", + request: { + params: z.object({ + id: z.string().openapi({ + param: { + name: "id", + in: "path", + }, + example: "clx1234567890", + }), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + id: z.string(), + success: z.boolean(), + message: z.string(), + }), + }, + }, + description: "Contact book deleted successfully", + }, + 403: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Forbidden - API key doesn't have access", + }, + 404: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Contact book not found", + }, + }, +}); + +function deleteContactBook(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const contactBookId = c.req.valid("param").id; + + const contactBook = await db.contactBook.findFirst({ + where: { + id: contactBookId, + teamId: team.id, + }, + }); + + if (!contactBook) { + throw new UnsendApiError({ + code: "NOT_FOUND", + message: "Contact book not found", + }); + } + + const deletedContactBook = await deleteContactBookService(contactBookId); + + return c.json({ + id: deletedContactBook.id, + success: true, + message: "Contact book deleted successfully", + }); + }); +} + +export default deleteContactBook; diff --git a/apps/web/src/server/public-api/api/contact-books/get-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/get-contact-book.ts new file mode 100644 index 00000000..09f54663 --- /dev/null +++ b/apps/web/src/server/public-api/api/contact-books/get-contact-book.ts @@ -0,0 +1,85 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { ContactBookSchema } from "~/lib/zod/contact-book-schema"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { db } from "~/server/db"; +import { UnsendApiError } from "../../api-error"; + +const route = createRoute({ + method: "get", + path: "/v1/contact-books/{id}", + request: { + params: z.object({ + id: z.string().openapi({ + param: { + name: "id", + in: "path", + }, + example: "clx1234567890", + }), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: ContactBookSchema, + }, + }, + description: "Retrieve the contact book", + }, + 403: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: + "Forbidden - API key doesn't have access to this contact book", + }, + 404: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Contact book not found", + }, + }, +}); + +function getContactBook(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const contactBookId = c.req.valid("param").id; + + const contactBook = await db.contactBook.findFirst({ + where: { + id: contactBookId, + teamId: team.id, + }, + include: { + _count: { + select: { contacts: true }, + }, + }, + }); + + if (!contactBook) { + throw new UnsendApiError({ + code: "NOT_FOUND", + message: "Contact book not found", + }); + } + + return c.json({ + ...contactBook, + properties: contactBook.properties as Record, + }); + }); +} + +export default getContactBook; diff --git a/apps/web/src/server/public-api/api/contact-books/get-contact-books.ts b/apps/web/src/server/public-api/api/contact-books/get-contact-books.ts new file mode 100644 index 00000000..fcda76d6 --- /dev/null +++ b/apps/web/src/server/public-api/api/contact-books/get-contact-books.ts @@ -0,0 +1,37 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { ContactBookSchema } from "~/lib/zod/contact-book-schema"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { getContactBooks as getContactBooksService } from "~/server/service/contact-book-service"; + +const route = createRoute({ + method: "get", + path: "/v1/contact-books", + responses: { + 200: { + content: { + "application/json": { + schema: z.array(ContactBookSchema), + }, + }, + description: "Retrieve contact books accessible by the API key", + }, + }, +}); + +function getContactBooks(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + + const contactBooks = await getContactBooksService(team.id); + + // Ensure properties is a Record + const sanitizedContactBooks = contactBooks.map((contactBook) => ({ + ...contactBook, + properties: contactBook.properties as Record, + })); + + return c.json(sanitizedContactBooks); + }); +} + +export default getContactBooks; diff --git a/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts new file mode 100644 index 00000000..9e695ae6 --- /dev/null +++ b/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts @@ -0,0 +1,96 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { ContactBookSchema } from "~/lib/zod/contact-book-schema"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { db } from "~/server/db"; +import { UnsendApiError } from "../../api-error"; +import { updateContactBook as updateContactBookService } from "~/server/service/contact-book-service"; + +const route = createRoute({ + method: "patch", + path: "/v1/contact-books/{id}", + request: { + params: z.object({ + id: z.string().openapi({ + param: { + name: "id", + in: "path", + }, + example: "clx1234567890", + }), + }), + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + name: z.string().min(1).optional(), + emoji: z.string().optional(), + properties: z.record(z.string()).optional(), + }), + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: ContactBookSchema, + }, + }, + description: "Update the contact book", + }, + 403: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: + "Forbidden - API key doesn't have access to this contact book", + }, + 404: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Contact book not found", + }, + }, +}); + +function updateContactBook(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = c.var.team; + const contactBookId = c.req.valid("param").id; + const body = c.req.valid("json"); + + const contactBook = await db.contactBook.findFirst({ + where: { + id: contactBookId, + teamId: team.id, + }, + }); + + if (!contactBook) { + throw new UnsendApiError({ + code: "NOT_FOUND", + message: "Contact book not found", + }); + } + + const updated = await updateContactBookService(contactBookId, body); + + return c.json({ + ...updated, + properties: updated.properties as Record, + }); + }); +} + +export default updateContactBook; diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts index 96eacf54..3823c1b3 100644 --- a/apps/web/src/server/public-api/index.ts +++ b/apps/web/src/server/public-api/index.ts @@ -21,6 +21,11 @@ import getCampaign from "./api/campaigns/get-campaign"; import scheduleCampaign from "./api/campaigns/schedule-campaign"; import pauseCampaign from "./api/campaigns/pause-campaign"; import resumeCampaign from "./api/campaigns/resume-campaign"; +import getContactBooks from "./api/contact-books/get-contact-books"; +import createContactBook from "./api/contact-books/create-contact-book"; +import getContactBook from "./api/contact-books/get-contact-book"; +import updateContactBook from "./api/contact-books/update-contact-book"; +import deleteContactBook from "./api/contact-books/delete-contact-book"; export const app = getApp(); @@ -47,6 +52,13 @@ getContacts(app); upsertContact(app); deleteContact(app); +/**Contact Book related APIs */ +getContactBooks(app); +createContactBook(app); +getContactBook(app); +updateContactBook(app); +deleteContactBook(app); + /**Campaign related APIs */ createCampaign(app); getCampaign(app); From 8a8be8004cdfe6349b67748c39251b32e96b267b Mon Sep 17 00:00:00 2001 From: David Stockley Date: Sun, 4 Jan 2026 18:04:32 +0000 Subject: [PATCH 2/7] refactor: dyrnamic imports, use getContactBook fn --- .../api/contact-books/create-contact-book.ts | 10 ++++------ .../api/contact-books/delete-contact-book.ts | 17 ++--------------- .../api/contact-books/update-contact-book.ts | 17 ++--------------- 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts index 9e957094..f32a5473 100644 --- a/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts +++ b/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts @@ -1,7 +1,10 @@ import { createRoute, z } from "@hono/zod-openapi"; import { ContactBookSchema } from "~/lib/zod/contact-book-schema"; import { PublicAPIApp } from "~/server/public-api/hono"; -import { createContactBook as createContactBookService } from "~/server/service/contact-book-service"; +import { + createContactBook as createContactBookService, + updateContactBook, +} from "~/server/service/contact-book-service"; const route = createRoute({ method: "post", @@ -37,15 +40,10 @@ function createContactBook(app: PublicAPIApp) { const team = c.var.team; const body = c.req.valid("json"); - console.log({ team }); - const contactBook = await createContactBookService(team.id, body.name); // Update emoji and properties if provided if (body.emoji || body.properties) { - const { updateContactBook } = await import( - "~/server/service/contact-book-service" - ); const updated = await updateContactBook(contactBook.id, { emoji: body.emoji, properties: body.properties, diff --git a/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts index fe4037b1..629ea7f5 100644 --- a/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts +++ b/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts @@ -1,8 +1,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { PublicAPIApp } from "../../hono"; -import { db } from "~/server/db"; -import { UnsendApiError } from "../../api-error"; import { deleteContactBook as deleteContactBookService } from "~/server/service/contact-book-service"; +import { getContactBook } from "../../api-utils"; const route = createRoute({ method: "delete", @@ -59,19 +58,7 @@ function deleteContactBook(app: PublicAPIApp) { const team = c.var.team; const contactBookId = c.req.valid("param").id; - const contactBook = await db.contactBook.findFirst({ - where: { - id: contactBookId, - teamId: team.id, - }, - }); - - if (!contactBook) { - throw new UnsendApiError({ - code: "NOT_FOUND", - message: "Contact book not found", - }); - } + await getContactBook(c, team.id); const deletedContactBook = await deleteContactBookService(contactBookId); diff --git a/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts b/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts index 9e695ae6..1d9f7041 100644 --- a/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts +++ b/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts @@ -1,9 +1,8 @@ import { createRoute, z } from "@hono/zod-openapi"; import { ContactBookSchema } from "~/lib/zod/contact-book-schema"; import { PublicAPIApp } from "~/server/public-api/hono"; -import { db } from "~/server/db"; -import { UnsendApiError } from "../../api-error"; import { updateContactBook as updateContactBookService } from "~/server/service/contact-book-service"; +import { getContactBook } from "../../api-utils"; const route = createRoute({ method: "patch", @@ -70,19 +69,7 @@ function updateContactBook(app: PublicAPIApp) { const contactBookId = c.req.valid("param").id; const body = c.req.valid("json"); - const contactBook = await db.contactBook.findFirst({ - where: { - id: contactBookId, - teamId: team.id, - }, - }); - - if (!contactBook) { - throw new UnsendApiError({ - code: "NOT_FOUND", - message: "Contact book not found", - }); - } + await getContactBook(c, team.id); const updated = await updateContactBookService(contactBookId, body); From 3a5a8df97add379084e74e07afa5e1d0fda8d621 Mon Sep 17 00:00:00 2001 From: David Stockley Date: Sun, 4 Jan 2026 18:36:04 +0000 Subject: [PATCH 3/7] refactor: move into contacts public api --- .../contact-books/create-contact-book.mdx | 3 - .../contact-books/delete-contact-book.mdx | 3 - .../contact-books/get-contact-book.mdx | 3 - .../contact-books/list-contact-books.mdx | 3 - .../contact-books/update-contact-book.mdx | 3 - .../contacts/create-contact-book.mdx | 3 + .../contacts/delete-contact-book.mdx | 3 + .../contacts/get-contact-book.mdx | 3 + .../contacts/list-contact-books.mdx | 3 + .../contacts/update-contact-book.mdx | 3 + apps/docs/api-reference/openapi.json | 4072 ++++++++--------- apps/docs/docs.json | 10 +- .../create-contact-book.ts | 2 +- .../delete-contact-book.ts | 8 +- .../get-contact-book.ts | 8 +- .../get-contact-books.ts | 2 +- .../update-contact-book.ts | 10 +- apps/web/src/server/public-api/index.ts | 10 +- 18 files changed, 2077 insertions(+), 2075 deletions(-) delete mode 100644 apps/docs/api-reference/contact-books/create-contact-book.mdx delete mode 100644 apps/docs/api-reference/contact-books/delete-contact-book.mdx delete mode 100644 apps/docs/api-reference/contact-books/get-contact-book.mdx delete mode 100644 apps/docs/api-reference/contact-books/list-contact-books.mdx delete mode 100644 apps/docs/api-reference/contact-books/update-contact-book.mdx create mode 100644 apps/docs/api-reference/contacts/create-contact-book.mdx create mode 100644 apps/docs/api-reference/contacts/delete-contact-book.mdx create mode 100644 apps/docs/api-reference/contacts/get-contact-book.mdx create mode 100644 apps/docs/api-reference/contacts/list-contact-books.mdx create mode 100644 apps/docs/api-reference/contacts/update-contact-book.mdx rename apps/web/src/server/public-api/api/{contact-books => contacts}/create-contact-book.ts (98%) rename apps/web/src/server/public-api/api/{contact-books => contacts}/delete-contact-book.ts (89%) rename apps/web/src/server/public-api/api/{contact-books => contacts}/get-contact-book.ts (90%) rename apps/web/src/server/public-api/api/{contact-books => contacts}/get-contact-books.ts (97%) rename apps/web/src/server/public-api/api/{contact-books => contacts}/update-contact-book.ts (89%) diff --git a/apps/docs/api-reference/contact-books/create-contact-book.mdx b/apps/docs/api-reference/contact-books/create-contact-book.mdx deleted file mode 100644 index c3be3e95..00000000 --- a/apps/docs/api-reference/contact-books/create-contact-book.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: post /v1/contact-books ---- diff --git a/apps/docs/api-reference/contact-books/delete-contact-book.mdx b/apps/docs/api-reference/contact-books/delete-contact-book.mdx deleted file mode 100644 index 5685304e..00000000 --- a/apps/docs/api-reference/contact-books/delete-contact-book.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: delete /v1/contact-books/{id} ---- diff --git a/apps/docs/api-reference/contact-books/get-contact-book.mdx b/apps/docs/api-reference/contact-books/get-contact-book.mdx deleted file mode 100644 index c82cd986..00000000 --- a/apps/docs/api-reference/contact-books/get-contact-book.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: get /v1/contact-books/{id} ---- diff --git a/apps/docs/api-reference/contact-books/list-contact-books.mdx b/apps/docs/api-reference/contact-books/list-contact-books.mdx deleted file mode 100644 index 300578fa..00000000 --- a/apps/docs/api-reference/contact-books/list-contact-books.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: get /v1/contact-books ---- diff --git a/apps/docs/api-reference/contact-books/update-contact-book.mdx b/apps/docs/api-reference/contact-books/update-contact-book.mdx deleted file mode 100644 index e65ced09..00000000 --- a/apps/docs/api-reference/contact-books/update-contact-book.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: patch /v1/contact-books/{id} ---- diff --git a/apps/docs/api-reference/contacts/create-contact-book.mdx b/apps/docs/api-reference/contacts/create-contact-book.mdx new file mode 100644 index 00000000..0895fa6e --- /dev/null +++ b/apps/docs/api-reference/contacts/create-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/contactBooks +--- diff --git a/apps/docs/api-reference/contacts/delete-contact-book.mdx b/apps/docs/api-reference/contacts/delete-contact-book.mdx new file mode 100644 index 00000000..3afb1c32 --- /dev/null +++ b/apps/docs/api-reference/contacts/delete-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /v1/contactBooks/{contactBookId} +--- diff --git a/apps/docs/api-reference/contacts/get-contact-book.mdx b/apps/docs/api-reference/contacts/get-contact-book.mdx new file mode 100644 index 00000000..3dab8e68 --- /dev/null +++ b/apps/docs/api-reference/contacts/get-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/contactBooks/{contactBookId} +--- diff --git a/apps/docs/api-reference/contacts/list-contact-books.mdx b/apps/docs/api-reference/contacts/list-contact-books.mdx new file mode 100644 index 00000000..27886117 --- /dev/null +++ b/apps/docs/api-reference/contacts/list-contact-books.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/contactBooks +--- diff --git a/apps/docs/api-reference/contacts/update-contact-book.mdx b/apps/docs/api-reference/contacts/update-contact-book.mdx new file mode 100644 index 00000000..7670494d --- /dev/null +++ b/apps/docs/api-reference/contacts/update-contact-book.mdx @@ -0,0 +1,3 @@ +--- +openapi: patch /v1/contactBooks/{contactBookId} +--- diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 56d85372..573b1edb 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -1,2038 +1,2038 @@ { - "openapi": "3.0.0", - "info": { "version": "1.0.0", "title": "useSend API" }, - "servers": [{ "url": "https://app.usesend.com/api" }], - "components": { - "securitySchemes": { "Bearer": { "type": "http", "scheme": "bearer" } }, - "schemas": {}, - "parameters": {} - }, - "paths": { - "/v1/domains": { - "get": { - "responses": { - "200": { - "description": "Retrieve domains accessible by the API key", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "number", - "description": "The ID of the domain", - "example": 1 - }, - "name": { - "type": "string", - "description": "The name of the domain", - "example": "example.com" - }, - "teamId": { - "type": "number", - "description": "The ID of the team", - "example": 1 - }, - "status": { - "type": "string", - "enum": [ - "NOT_STARTED", - "PENDING", - "SUCCESS", - "FAILED", - "TEMPORARY_FAILURE" - ] - }, - "region": { "type": "string", "default": "us-east-1" }, - "clickTracking": { "type": "boolean", "default": false }, - "openTracking": { "type": "boolean", "default": false }, - "publicKey": { "type": "string" }, - "dkimStatus": { "type": "string", "nullable": true }, - "spfDetails": { "type": "string", "nullable": true }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" }, - "dmarcAdded": { "type": "boolean", "default": false }, - "isVerifying": { "type": "boolean", "default": false }, - "errorMessage": { "type": "string", "nullable": true }, - "subdomain": { "type": "string", "nullable": true }, - "verificationError": { - "type": "string", - "nullable": true - }, - "lastCheckedTime": { "type": "string", "nullable": true }, - "dnsRecords": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["MX", "TXT"], - "description": "DNS record type", - "example": "TXT" - }, - "name": { - "type": "string", - "description": "DNS record name", - "example": "mail" - }, - "value": { - "type": "string", - "description": "DNS record value", - "example": "v=spf1 include:amazonses.com ~all" - }, - "ttl": { - "type": "string", - "description": "DNS record TTL", - "example": "Auto" - }, - "priority": { - "type": "string", - "nullable": true, - "description": "DNS record priority", - "example": "10" - }, - "status": { - "type": "string", - "enum": [ - "NOT_STARTED", - "PENDING", - "SUCCESS", - "FAILED", - "TEMPORARY_FAILURE" - ] - }, - "recommended": { - "type": "boolean", - "description": "Whether the record is recommended" - } - }, - "required": ["type", "name", "value", "ttl", "status"] - } - } - }, - "required": [ - "id", - "name", - "teamId", - "status", - "publicKey", - "createdAt", - "updatedAt", - "dnsRecords" - ] - } - } - } - } - } - } - }, - "post": { - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "region": { "type": "string" } - }, - "required": ["name", "region"] - } - } - } - }, - "responses": { - "200": { - "description": "Create a new domain", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "number", - "description": "The ID of the domain", - "example": 1 - }, - "name": { - "type": "string", - "description": "The name of the domain", - "example": "example.com" - }, - "teamId": { - "type": "number", - "description": "The ID of the team", - "example": 1 - }, - "status": { - "type": "string", - "enum": [ - "NOT_STARTED", - "PENDING", - "SUCCESS", - "FAILED", - "TEMPORARY_FAILURE" - ] - }, - "region": { "type": "string", "default": "us-east-1" }, - "clickTracking": { "type": "boolean", "default": false }, - "openTracking": { "type": "boolean", "default": false }, - "publicKey": { "type": "string" }, - "dkimStatus": { "type": "string", "nullable": true }, - "spfDetails": { "type": "string", "nullable": true }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" }, - "dmarcAdded": { "type": "boolean", "default": false }, - "isVerifying": { "type": "boolean", "default": false }, - "errorMessage": { "type": "string", "nullable": true }, - "subdomain": { "type": "string", "nullable": true }, - "verificationError": { "type": "string", "nullable": true }, - "lastCheckedTime": { "type": "string", "nullable": true }, - "dnsRecords": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["MX", "TXT"], - "description": "DNS record type", - "example": "TXT" - }, - "name": { - "type": "string", - "description": "DNS record name", - "example": "mail" - }, - "value": { - "type": "string", - "description": "DNS record value", - "example": "v=spf1 include:amazonses.com ~all" - }, - "ttl": { - "type": "string", - "description": "DNS record TTL", - "example": "Auto" - }, - "priority": { - "type": "string", - "nullable": true, - "description": "DNS record priority", - "example": "10" - }, - "status": { - "type": "string", - "enum": [ - "NOT_STARTED", - "PENDING", - "SUCCESS", - "FAILED", - "TEMPORARY_FAILURE" - ] - }, - "recommended": { - "type": "boolean", - "description": "Whether the record is recommended" - } - }, - "required": ["type", "name", "value", "ttl", "status"] - } - } - }, - "required": [ - "id", - "name", - "teamId", - "status", - "publicKey", - "createdAt", - "updatedAt", - "dnsRecords" - ] - } - } - } - } - } - } - }, - "/v1/domains/{id}/verify": { - "put": { - "parameters": [ - { - "schema": { "type": "number", "nullable": true, "example": 1 }, - "required": false, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Verify domain", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "message": { "type": "string" } }, - "required": ["message"] - } - } - } - }, - "403": { - "description": "Forbidden - API key doesn't have access to this domain", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - }, - "404": { - "description": "Domain not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - } - } - } - }, - "/v1/domains/{id}": { - "get": { - "parameters": [ - { - "schema": { "type": "number", "nullable": true, "example": 1 }, - "required": false, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Retrieve the domain", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "number", - "description": "The ID of the domain", - "example": 1 - }, - "name": { - "type": "string", - "description": "The name of the domain", - "example": "example.com" - }, - "teamId": { - "type": "number", - "description": "The ID of the team", - "example": 1 - }, - "status": { - "type": "string", - "enum": [ - "NOT_STARTED", - "PENDING", - "SUCCESS", - "FAILED", - "TEMPORARY_FAILURE" - ] - }, - "region": { "type": "string", "default": "us-east-1" }, - "clickTracking": { "type": "boolean", "default": false }, - "openTracking": { "type": "boolean", "default": false }, - "publicKey": { "type": "string" }, - "dkimStatus": { "type": "string", "nullable": true }, - "spfDetails": { "type": "string", "nullable": true }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" }, - "dmarcAdded": { "type": "boolean", "default": false }, - "isVerifying": { "type": "boolean", "default": false }, - "errorMessage": { "type": "string", "nullable": true }, - "subdomain": { "type": "string", "nullable": true }, - "verificationError": { "type": "string", "nullable": true }, - "lastCheckedTime": { "type": "string", "nullable": true }, - "dnsRecords": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["MX", "TXT"], - "description": "DNS record type", - "example": "TXT" - }, - "name": { - "type": "string", - "description": "DNS record name", - "example": "mail" - }, - "value": { - "type": "string", - "description": "DNS record value", - "example": "v=spf1 include:amazonses.com ~all" - }, - "ttl": { - "type": "string", - "description": "DNS record TTL", - "example": "Auto" - }, - "priority": { - "type": "string", - "nullable": true, - "description": "DNS record priority", - "example": "10" - }, - "status": { - "type": "string", - "enum": [ - "NOT_STARTED", - "PENDING", - "SUCCESS", - "FAILED", - "TEMPORARY_FAILURE" - ] - }, - "recommended": { - "type": "boolean", - "description": "Whether the record is recommended" - } - }, - "required": ["type", "name", "value", "ttl", "status"] - } - } - }, - "required": [ - "id", - "name", - "teamId", - "status", - "publicKey", - "createdAt", - "updatedAt", - "dnsRecords" - ] - } - } - } - } - } - }, - "delete": { - "parameters": [ - { - "schema": { "type": "number", "nullable": true, "example": 1 }, - "required": false, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Domain deleted successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "number" }, - "success": { "type": "boolean" }, - "message": { "type": "string" } - }, - "required": ["id", "success", "message"] - } - } - } - }, - "403": { - "description": "Forbidden - API key doesn't have access", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - }, - "404": { - "description": "Domain not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - } - } - } - }, - "/v1/emails/{emailId}": { - "get": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 3, - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "emailId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Retrieve the email", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "teamId": { "type": "number" }, - "to": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "replyTo": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "cc": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "bcc": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "from": { "type": "string" }, - "subject": { "type": "string" }, - "html": { "type": "string", "nullable": true }, - "text": { "type": "string", "nullable": true }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" }, - "emailEvents": { - "type": "array", - "items": { - "type": "object", - "properties": { - "emailId": { "type": "string" }, - "status": { - "type": "string", - "enum": [ - "SCHEDULED", - "QUEUED", - "SENT", - "DELIVERY_DELAYED", - "BOUNCED", - "REJECTED", - "RENDERING_FAILURE", - "DELIVERED", - "OPENED", - "CLICKED", - "COMPLAINED", - "FAILED", - "CANCELLED", - "SUPPRESSED" - ] - }, - "createdAt": { "type": "string" }, - "data": { "nullable": true } - }, - "required": ["emailId", "status", "createdAt"] - } - } - }, - "required": [ - "id", - "teamId", - "to", - "from", - "subject", - "html", - "text", - "createdAt", - "updatedAt", - "emailEvents" - ] - } - } - } - } - } - }, - "patch": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 3, - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "emailId", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "scheduledAt": { "type": "string", "format": "date-time" } - }, - "required": ["scheduledAt"] - } - } - } - }, - "responses": { - "200": { - "description": "Retrieve the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "emailId": { "type": "string" } } - } - } - } - } - } - } - }, - "/v1/emails": { - "get": { - "parameters": [ - { - "schema": { "type": "string", "default": "1", "example": "1" }, - "required": false, - "name": "page", - "in": "query" - }, - { - "schema": { "type": "string", "default": "50", "example": "50" }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "example": "2024-01-01T00:00:00Z" - }, - "required": false, - "name": "startDate", - "in": "query" - }, - { - "schema": { - "type": "string", - "format": "date-time", - "example": "2024-01-31T23:59:59Z" - }, - "required": false, - "name": "endDate", - "in": "query" - }, - { - "schema": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ], - "example": "123" - }, - "required": false, - "name": "domainId", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Retrieve a list of emails", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "to": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "replyTo": { - "anyOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "type": "string" } - }, - { "nullable": true } - ] - }, - "cc": { - "anyOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "type": "string" } - }, - { "nullable": true } - ] - }, - "bcc": { - "anyOf": [ - { "type": "string" }, - { - "type": "array", - "items": { "type": "string" } - }, - { "nullable": true } - ] - }, - "from": { "type": "string" }, - "subject": { "type": "string" }, - "html": { "type": "string", "nullable": true }, - "text": { "type": "string", "nullable": true }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" }, - "latestStatus": { - "type": "string", - "nullable": true, - "enum": [ - "SCHEDULED", - "QUEUED", - "SENT", - "DELIVERY_DELAYED", - "BOUNCED", - "REJECTED", - "RENDERING_FAILURE", - "DELIVERED", - "OPENED", - "CLICKED", - "COMPLAINED", - "FAILED", - "CANCELLED", - "SUPPRESSED" - ] - }, - "scheduledAt": { - "type": "string", - "nullable": true, - "format": "date-time" - }, - "domainId": { "type": "number", "nullable": true } - }, - "required": [ - "id", - "to", - "from", - "subject", - "html", - "text", - "createdAt", - "updatedAt", - "latestStatus", - "scheduledAt", - "domainId" - ] - } - }, - "count": { "type": "number" } - }, - "required": ["data", "count"] - } - } - } - } - } - }, - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Pass the optional Idempotency-Key header to make the request safe to retry. The key can be up to 256 characters. The server stores the canonical request body and behaves as follows:\n\n- Same key + same request body → returns the original emailId with 200 OK without re-sending.\n- Same key + different request body → returns 409 Conflict with code: NOT_UNIQUE so you can detect the mismatch.\n- Same key while another request is still being processed → returns 409 Conflict; retry after a short delay or once the first request completes.\n\nEntries expire after 24 hours. Use a unique key per logical send (for example, an order or signup ID)." - }, - "required": false, - "name": "Idempotency-Key", - "in": "header" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "to": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "from": { "type": "string" }, - "subject": { - "type": "string", - "minLength": 1, - "description": "Optional when templateId is provided" - }, - "templateId": { - "type": "string", - "description": "ID of a template from the dashboard" - }, - "variables": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "replyTo": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "cc": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "bcc": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "text": { - "type": "string", - "nullable": true, - "minLength": 1 - }, - "html": { - "type": "string", - "nullable": true, - "minLength": 1 - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string", - "minLength": 1 - }, - "description": "Custom headers to included with the emails" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "filename": { "type": "string", "minLength": 1 }, - "content": { "type": "string", "minLength": 1 } - }, - "required": ["filename", "content"] - }, - "maxItems": 10 - }, - "scheduledAt": { "type": "string", "format": "date-time" }, - "inReplyToId": { "type": "string", "nullable": true } - }, - "required": ["to", "from"] - } - } - } - }, - "responses": { - "200": { - "description": "Retrieve the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "emailId": { "type": "string" } } - } - } - } - } - } - } - }, - "/v1/emails/batch": { - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "description": "Pass the optional Idempotency-Key header to make the request safe to retry. The key can be up to 256 characters. The server stores the canonical request body and behaves as follows:\n\n- Same key + same request body → returns the original emailId with 200 OK without re-sending.\n- Same key + different request body → returns 409 Conflict with code: NOT_UNIQUE so you can detect the mismatch.\n- Same key while another request is still being processed → returns 409 Conflict; retry after a short delay or once the first request completes.\n\nEntries expire after 24 hours. Use a unique key per logical send (for example, an order or signup ID)." - }, - "required": false, - "name": "Idempotency-Key", - "in": "header" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "to": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "from": { "type": "string" }, - "subject": { - "type": "string", - "minLength": 1, - "description": "Optional when templateId is provided" - }, - "templateId": { - "type": "string", - "description": "ID of a template from the dashboard" - }, - "variables": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "replyTo": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "cc": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "bcc": { - "anyOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] - }, - "text": { - "type": "string", - "nullable": true, - "minLength": 1 - }, - "html": { - "type": "string", - "nullable": true, - "minLength": 1 - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string", - "minLength": 1 - }, - "description": "Custom headers to included with the emails" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "filename": { "type": "string", "minLength": 1 }, - "content": { "type": "string", "minLength": 1 } - }, - "required": ["filename", "content"] - }, - "maxItems": 10 - }, - "scheduledAt": { "type": "string", "format": "date-time" }, - "inReplyToId": { "type": "string", "nullable": true } - }, - "required": ["to", "from"] - }, - "maxItems": 100 - } - } - } - }, - "responses": { - "200": { - "description": "List of successfully created email IDs", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { "emailId": { "type": "string" } }, - "required": ["emailId"] - } - } - }, - "required": ["data"] - } - } - } - } - } - } - }, - "/v1/emails/{emailId}/cancel": { - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 3, - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "emailId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Retrieve the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "emailId": { "type": "string" } } - } - } - } - } - } - } - }, - "/v1/contactBooks/{contactBookId}/contacts": { - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 3, - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "contactBookId", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { "type": "string" }, - "firstName": { "type": "string" }, - "lastName": { "type": "string" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "subscribed": { "type": "boolean" } - }, - "required": ["email"] - } - } - } - }, - "responses": { - "200": { - "description": "Retrieve the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "contactId": { "type": "string" } } - } - } - } - } - } - }, - "get": { - "parameters": [ - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactBookId", - "in": "path" - }, - { - "schema": { "type": "string" }, - "required": false, - "name": "emails", - "in": "query" - }, - { - "schema": { "type": "number" }, - "required": false, - "name": "page", - "in": "query" - }, - { - "schema": { "type": "number" }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { "type": "string" }, - "required": false, - "name": "ids", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Retrieve multiple contacts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "firstName": { "type": "string", "nullable": true }, - "lastName": { "type": "string", "nullable": true }, - "email": { "type": "string" }, - "subscribed": { "type": "boolean", "default": true }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "contactBookId": { "type": "string" }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" } - }, - "required": [ - "id", - "email", - "properties", - "contactBookId", - "createdAt", - "updatedAt" - ] - } - } - } - } - } - } - } - }, - "/v1/contactBooks/{contactBookId}/contacts/{contactId}": { - "patch": { - "parameters": [ - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactBookId", - "in": "path" - }, - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactId", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "firstName": { "type": "string" }, - "lastName": { "type": "string" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "subscribed": { "type": "boolean" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Retrieve the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "contactId": { "type": "string" } } - } - } - } - } - } - }, - "get": { - "parameters": [ - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactBookId", - "in": "path" - }, - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Retrieve the contact", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "firstName": { "type": "string", "nullable": true }, - "lastName": { "type": "string", "nullable": true }, - "email": { "type": "string" }, - "subscribed": { "type": "boolean", "default": true }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "contactBookId": { "type": "string" }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" } - }, - "required": [ - "id", - "email", - "properties", - "contactBookId", - "createdAt", - "updatedAt" - ] - } - } - } - } - } - }, - "put": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 3, - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "contactBookId", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { "type": "string" }, - "firstName": { "type": "string" }, - "lastName": { "type": "string" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "subscribed": { "type": "boolean" } - }, - "required": ["email"] - } - } - } - }, - "responses": { - "200": { - "description": "Contact upserted successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "contactId": { "type": "string" } }, - "required": ["contactId"] - } - } - } - } - } - }, - "delete": { - "parameters": [ - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactBookId", - "in": "path" - }, - { - "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, - "required": true, - "name": "contactId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Contact deleted successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "success": { "type": "boolean" } }, - "required": ["success"] - } - } - } - } - } - } - }, - "/v1/campaigns": { - "post": { - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { "type": "string", "minLength": 1 }, - "from": { "type": "string", "minLength": 1 }, - "subject": { "type": "string", "minLength": 1 }, - "previewText": { "type": "string" }, - "contactBookId": { "type": "string", "minLength": 1 }, - "content": { "type": "string", "minLength": 1 }, - "html": { "type": "string", "minLength": 1 }, - "replyTo": { - "anyOf": [ - { "type": "string", "minLength": 1 }, - { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - ] - }, - "cc": { - "anyOf": [ - { "type": "string", "minLength": 1 }, - { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - ] - }, - "bcc": { - "anyOf": [ - { "type": "string", "minLength": 1 }, - { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - ] - }, - "sendNow": { "type": "boolean" }, - "scheduledAt": { - "type": "string", - "description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" - }, - "batchSize": { - "type": "integer", - "minimum": 1, - "maximum": 100000 - } - }, - "required": ["name", "from", "subject", "contactBookId"] - } - } - } - }, - "responses": { - "200": { - "description": "Create a campaign", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "from": { "type": "string" }, - "subject": { "type": "string" }, - "previewText": { "type": "string", "nullable": true }, - "contactBookId": { "type": "string", "nullable": true }, - "html": { "type": "string", "nullable": true }, - "content": { "type": "string", "nullable": true }, - "status": { "type": "string" }, - "scheduledAt": { - "type": "string", - "nullable": true, - "format": "date-time" - }, - "batchSize": { "type": "integer" }, - "batchWindowMinutes": { "type": "integer" }, - "total": { "type": "integer" }, - "sent": { "type": "integer" }, - "delivered": { "type": "integer" }, - "opened": { "type": "integer" }, - "clicked": { "type": "integer" }, - "unsubscribed": { "type": "integer" }, - "bounced": { "type": "integer" }, - "hardBounced": { "type": "integer" }, - "complained": { "type": "integer" }, - "replyTo": { - "type": "array", - "items": { "type": "string" } - }, - "cc": { "type": "array", "items": { "type": "string" } }, - "bcc": { "type": "array", "items": { "type": "string" } }, - "createdAt": { "type": "string", "format": "date-time" }, - "updatedAt": { "type": "string", "format": "date-time" } - }, - "required": [ - "id", - "name", - "from", - "subject", - "previewText", - "contactBookId", - "html", - "content", - "status", - "scheduledAt", - "batchSize", - "batchWindowMinutes", - "total", - "sent", - "delivered", - "opened", - "clicked", - "unsubscribed", - "bounced", - "hardBounced", - "complained", - "replyTo", - "cc", - "bcc", - "createdAt", - "updatedAt" - ] - } - } - } - } - } - } - }, - "/v1/campaigns/{campaignId}": { - "get": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "example": "cmp_123" - }, - "required": true, - "name": "campaignId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Get campaign details", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "from": { "type": "string" }, - "subject": { "type": "string" }, - "previewText": { "type": "string", "nullable": true }, - "contactBookId": { "type": "string", "nullable": true }, - "html": { "type": "string", "nullable": true }, - "content": { "type": "string", "nullable": true }, - "status": { "type": "string" }, - "scheduledAt": { - "type": "string", - "nullable": true, - "format": "date-time" - }, - "batchSize": { "type": "integer" }, - "batchWindowMinutes": { "type": "integer" }, - "total": { "type": "integer" }, - "sent": { "type": "integer" }, - "delivered": { "type": "integer" }, - "opened": { "type": "integer" }, - "clicked": { "type": "integer" }, - "unsubscribed": { "type": "integer" }, - "bounced": { "type": "integer" }, - "hardBounced": { "type": "integer" }, - "complained": { "type": "integer" }, - "replyTo": { - "type": "array", - "items": { "type": "string" } - }, - "cc": { "type": "array", "items": { "type": "string" } }, - "bcc": { "type": "array", "items": { "type": "string" } }, - "createdAt": { "type": "string", "format": "date-time" }, - "updatedAt": { "type": "string", "format": "date-time" } - }, - "required": [ - "id", - "name", - "from", - "subject", - "previewText", - "contactBookId", - "html", - "content", - "status", - "scheduledAt", - "batchSize", - "batchWindowMinutes", - "total", - "sent", - "delivered", - "opened", - "clicked", - "unsubscribed", - "bounced", - "hardBounced", - "complained", - "replyTo", - "cc", - "bcc", - "createdAt", - "updatedAt" - ] - } - } - } - } - } - } - }, - "/v1/campaigns/{campaignId}/schedule": { - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "example": "cmp_123" - }, - "required": true, - "name": "campaignId", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "scheduledAt": { - "type": "string", - "description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" - }, - "batchSize": { - "type": "integer", - "minimum": 1, - "maximum": 100000 - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Schedule a campaign", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "success": { "type": "boolean" } }, - "required": ["success"] - } - } - } - } - } - } - }, - "/v1/campaigns/{campaignId}/pause": { - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "example": "cmp_123" - }, - "required": true, - "name": "campaignId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Pause a campaign", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "success": { "type": "boolean" } }, - "required": ["success"] - } - } - } - } - } - } - }, - "/v1/campaigns/{campaignId}/resume": { - "post": { - "parameters": [ - { - "schema": { - "type": "string", - "minLength": 1, - "example": "cmp_123" - }, - "required": true, - "name": "campaignId", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Resume a campaign", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "success": { "type": "boolean" } }, - "required": ["success"] - } - } - } - } - } - } - }, - "/v1/contact-books": { - "get": { - "responses": { - "200": { - "description": "Retrieve contact books accessible by the API key", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the contact book", - "example": "clx1234567890" - }, - "name": { - "type": "string", - "description": "The name of the contact book", - "example": "Newsletter Subscribers" - }, - "teamId": { - "type": "number", - "description": "The ID of the team", - "example": 1 - }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" }, - "description": "Custom properties for the contact book", - "example": { "customField1": "value1" } - }, - "emoji": { - "type": "string", - "description": "The emoji associated with the contact book", - "example": "📙" - }, - "createdAt": { - "type": "string", - "description": "The creation timestamp" - }, - "updatedAt": { - "type": "string", - "description": "The last update timestamp" - }, - "_count": { - "type": "object", - "properties": { - "contacts": { - "type": "number", - "description": "The number of contacts in the contact book" - } - } - } - }, - "required": [ - "id", - "name", - "teamId", - "properties", - "emoji", - "createdAt", - "updatedAt" - ] - } - } - } - } - } - } - }, - "post": { - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { "type": "string", "minLength": 1 }, - "emoji": { "type": "string" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - } - }, - "required": ["name"] - } - } - } - }, - "responses": { - "200": { - "description": "Create a new contact book", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "teamId": { "type": "number" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "emoji": { "type": "string" }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" } - }, - "required": [ - "id", - "name", - "teamId", - "properties", - "emoji", - "createdAt", - "updatedAt" - ] - } - } - } - } - } - } - }, - "/v1/contact-books/{id}": { - "get": { - "parameters": [ - { - "schema": { "type": "string", "example": "clx1234567890" }, - "required": true, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Retrieve the contact book", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "teamId": { "type": "number" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "emoji": { "type": "string" }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" }, - "_count": { - "type": "object", - "properties": { - "contacts": { "type": "number" } - } - } - }, - "required": [ - "id", - "name", - "teamId", - "properties", - "emoji", - "createdAt", - "updatedAt" - ] - } - } - } - }, - "403": { - "description": "Forbidden - API key doesn't have access", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - }, - "404": { - "description": "Contact book not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - } - } - }, - "patch": { - "parameters": [ - { - "schema": { "type": "string", "example": "clx1234567890" }, - "required": true, - "name": "id", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { "type": "string", "minLength": 1 }, - "emoji": { "type": "string" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Update the contact book", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "teamId": { "type": "number" }, - "properties": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "emoji": { "type": "string" }, - "createdAt": { "type": "string" }, - "updatedAt": { "type": "string" } - }, - "required": [ - "id", - "name", - "teamId", - "properties", - "emoji", - "createdAt", - "updatedAt" - ] - } - } - } - }, - "403": { - "description": "Forbidden - API key doesn't have access", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - }, - "404": { - "description": "Contact book not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - } - } - }, - "delete": { - "parameters": [ - { - "schema": { "type": "string", "example": "clx1234567890" }, - "required": true, - "name": "id", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Contact book deleted successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "success": { "type": "boolean" }, - "message": { "type": "string" } - }, - "required": ["id", "success", "message"] - } - } - } - }, - "403": { - "description": "Forbidden - API key doesn't have access", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - }, - "404": { - "description": "Contact book not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "error": { "type": "string" } }, - "required": ["error"] - } - } - } - } - } - } - } - } + "openapi": "3.0.0", + "info": { "version": "1.0.0", "title": "useSend API" }, + "servers": [{ "url": "https://app.usesend.com/api" }], + "components": { + "securitySchemes": { "Bearer": { "type": "http", "scheme": "bearer" } }, + "schemas": {}, + "parameters": {} + }, + "paths": { + "/v1/domains": { + "get": { + "responses": { + "200": { + "description": "Retrieve domains accessible by the API key", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the domain", + "example": 1 + }, + "name": { + "type": "string", + "description": "The name of the domain", + "example": "example.com" + }, + "teamId": { + "type": "number", + "description": "The ID of the team", + "example": 1 + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "region": { "type": "string", "default": "us-east-1" }, + "clickTracking": { "type": "boolean", "default": false }, + "openTracking": { "type": "boolean", "default": false }, + "publicKey": { "type": "string" }, + "dkimStatus": { "type": "string", "nullable": true }, + "spfDetails": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "dmarcAdded": { "type": "boolean", "default": false }, + "isVerifying": { "type": "boolean", "default": false }, + "errorMessage": { "type": "string", "nullable": true }, + "subdomain": { "type": "string", "nullable": true }, + "verificationError": { + "type": "string", + "nullable": true + }, + "lastCheckedTime": { "type": "string", "nullable": true }, + "dnsRecords": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["MX", "TXT"], + "description": "DNS record type", + "example": "TXT" + }, + "name": { + "type": "string", + "description": "DNS record name", + "example": "mail" + }, + "value": { + "type": "string", + "description": "DNS record value", + "example": "v=spf1 include:amazonses.com ~all" + }, + "ttl": { + "type": "string", + "description": "DNS record TTL", + "example": "Auto" + }, + "priority": { + "type": "string", + "nullable": true, + "description": "DNS record priority", + "example": "10" + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "recommended": { + "type": "boolean", + "description": "Whether the record is recommended" + } + }, + "required": ["type", "name", "value", "ttl", "status"] + } + } + }, + "required": [ + "id", + "name", + "teamId", + "status", + "publicKey", + "createdAt", + "updatedAt", + "dnsRecords" + ] + } + } + } + } + } + } + }, + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "region": { "type": "string" } + }, + "required": ["name", "region"] + } + } + } + }, + "responses": { + "200": { + "description": "Create a new domain", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the domain", + "example": 1 + }, + "name": { + "type": "string", + "description": "The name of the domain", + "example": "example.com" + }, + "teamId": { + "type": "number", + "description": "The ID of the team", + "example": 1 + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "region": { "type": "string", "default": "us-east-1" }, + "clickTracking": { "type": "boolean", "default": false }, + "openTracking": { "type": "boolean", "default": false }, + "publicKey": { "type": "string" }, + "dkimStatus": { "type": "string", "nullable": true }, + "spfDetails": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "dmarcAdded": { "type": "boolean", "default": false }, + "isVerifying": { "type": "boolean", "default": false }, + "errorMessage": { "type": "string", "nullable": true }, + "subdomain": { "type": "string", "nullable": true }, + "verificationError": { "type": "string", "nullable": true }, + "lastCheckedTime": { "type": "string", "nullable": true }, + "dnsRecords": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["MX", "TXT"], + "description": "DNS record type", + "example": "TXT" + }, + "name": { + "type": "string", + "description": "DNS record name", + "example": "mail" + }, + "value": { + "type": "string", + "description": "DNS record value", + "example": "v=spf1 include:amazonses.com ~all" + }, + "ttl": { + "type": "string", + "description": "DNS record TTL", + "example": "Auto" + }, + "priority": { + "type": "string", + "nullable": true, + "description": "DNS record priority", + "example": "10" + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "recommended": { + "type": "boolean", + "description": "Whether the record is recommended" + } + }, + "required": ["type", "name", "value", "ttl", "status"] + } + } + }, + "required": [ + "id", + "name", + "teamId", + "status", + "publicKey", + "createdAt", + "updatedAt", + "dnsRecords" + ] + } + } + } + } + } + } + }, + "/v1/domains/{id}/verify": { + "put": { + "parameters": [ + { + "schema": { "type": "number", "nullable": true, "example": 1 }, + "required": false, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Verify domain", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "message": { "type": "string" } }, + "required": ["message"] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access to this domain", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Domain not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/v1/domains/{id}": { + "get": { + "parameters": [ + { + "schema": { "type": "number", "nullable": true, "example": 1 }, + "required": false, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Retrieve the domain", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the domain", + "example": 1 + }, + "name": { + "type": "string", + "description": "The name of the domain", + "example": "example.com" + }, + "teamId": { + "type": "number", + "description": "The ID of the team", + "example": 1 + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "region": { "type": "string", "default": "us-east-1" }, + "clickTracking": { "type": "boolean", "default": false }, + "openTracking": { "type": "boolean", "default": false }, + "publicKey": { "type": "string" }, + "dkimStatus": { "type": "string", "nullable": true }, + "spfDetails": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "dmarcAdded": { "type": "boolean", "default": false }, + "isVerifying": { "type": "boolean", "default": false }, + "errorMessage": { "type": "string", "nullable": true }, + "subdomain": { "type": "string", "nullable": true }, + "verificationError": { "type": "string", "nullable": true }, + "lastCheckedTime": { "type": "string", "nullable": true }, + "dnsRecords": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["MX", "TXT"], + "description": "DNS record type", + "example": "TXT" + }, + "name": { + "type": "string", + "description": "DNS record name", + "example": "mail" + }, + "value": { + "type": "string", + "description": "DNS record value", + "example": "v=spf1 include:amazonses.com ~all" + }, + "ttl": { + "type": "string", + "description": "DNS record TTL", + "example": "Auto" + }, + "priority": { + "type": "string", + "nullable": true, + "description": "DNS record priority", + "example": "10" + }, + "status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "PENDING", + "SUCCESS", + "FAILED", + "TEMPORARY_FAILURE" + ] + }, + "recommended": { + "type": "boolean", + "description": "Whether the record is recommended" + } + }, + "required": ["type", "name", "value", "ttl", "status"] + } + } + }, + "required": [ + "id", + "name", + "teamId", + "status", + "publicKey", + "createdAt", + "updatedAt", + "dnsRecords" + ] + } + } + } + } + } + }, + "delete": { + "parameters": [ + { + "schema": { "type": "number", "nullable": true, "example": 1 }, + "required": false, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Domain deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "success": { "type": "boolean" }, + "message": { "type": "string" } + }, + "required": ["id", "success", "message"] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Domain not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/v1/emails/{emailId}": { + "get": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 3, + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "emailId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Retrieve the email", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "teamId": { "type": "number" }, + "to": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "replyTo": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "cc": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "bcc": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "from": { "type": "string" }, + "subject": { "type": "string" }, + "html": { "type": "string", "nullable": true }, + "text": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "emailEvents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "status": { + "type": "string", + "enum": [ + "SCHEDULED", + "QUEUED", + "SENT", + "DELIVERY_DELAYED", + "BOUNCED", + "REJECTED", + "RENDERING_FAILURE", + "DELIVERED", + "OPENED", + "CLICKED", + "COMPLAINED", + "FAILED", + "CANCELLED", + "SUPPRESSED" + ] + }, + "createdAt": { "type": "string" }, + "data": { "nullable": true } + }, + "required": ["emailId", "status", "createdAt"] + } + } + }, + "required": [ + "id", + "teamId", + "to", + "from", + "subject", + "html", + "text", + "createdAt", + "updatedAt", + "emailEvents" + ] + } + } + } + } + } + }, + "patch": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 3, + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "emailId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "scheduledAt": { "type": "string", "format": "date-time" } + }, + "required": ["scheduledAt"] + } + } + } + }, + "responses": { + "200": { + "description": "Retrieve the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "emailId": { "type": "string" } } + } + } + } + } + } + } + }, + "/v1/emails": { + "get": { + "parameters": [ + { + "schema": { "type": "string", "default": "1", "example": "1" }, + "required": false, + "name": "page", + "in": "query" + }, + { + "schema": { "type": "string", "default": "50", "example": "50" }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "example": "2024-01-01T00:00:00Z" + }, + "required": false, + "name": "startDate", + "in": "query" + }, + { + "schema": { + "type": "string", + "format": "date-time", + "example": "2024-01-31T23:59:59Z" + }, + "required": false, + "name": "endDate", + "in": "query" + }, + { + "schema": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "example": "123" + }, + "required": false, + "name": "domainId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Retrieve a list of emails", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "to": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "replyTo": { + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + }, + { "nullable": true } + ] + }, + "cc": { + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + }, + { "nullable": true } + ] + }, + "bcc": { + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + }, + { "nullable": true } + ] + }, + "from": { "type": "string" }, + "subject": { "type": "string" }, + "html": { "type": "string", "nullable": true }, + "text": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "latestStatus": { + "type": "string", + "nullable": true, + "enum": [ + "SCHEDULED", + "QUEUED", + "SENT", + "DELIVERY_DELAYED", + "BOUNCED", + "REJECTED", + "RENDERING_FAILURE", + "DELIVERED", + "OPENED", + "CLICKED", + "COMPLAINED", + "FAILED", + "CANCELLED", + "SUPPRESSED" + ] + }, + "scheduledAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "domainId": { "type": "number", "nullable": true } + }, + "required": [ + "id", + "to", + "from", + "subject", + "html", + "text", + "createdAt", + "updatedAt", + "latestStatus", + "scheduledAt", + "domainId" + ] + } + }, + "count": { "type": "number" } + }, + "required": ["data", "count"] + } + } + } + } + } + }, + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Pass the optional Idempotency-Key header to make the request safe to retry. The key can be up to 256 characters. The server stores the canonical request body and behaves as follows:\n\n- Same key + same request body → returns the original emailId with 200 OK without re-sending.\n- Same key + different request body → returns 409 Conflict with code: NOT_UNIQUE so you can detect the mismatch.\n- Same key while another request is still being processed → returns 409 Conflict; retry after a short delay or once the first request completes.\n\nEntries expire after 24 hours. Use a unique key per logical send (for example, an order or signup ID)." + }, + "required": false, + "name": "Idempotency-Key", + "in": "header" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "to": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "from": { "type": "string" }, + "subject": { + "type": "string", + "minLength": 1, + "description": "Optional when templateId is provided" + }, + "templateId": { + "type": "string", + "description": "ID of a template from the dashboard" + }, + "variables": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "replyTo": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "cc": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "bcc": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "text": { + "type": "string", + "nullable": true, + "minLength": 1 + }, + "html": { + "type": "string", + "nullable": true, + "minLength": 1 + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + }, + "description": "Custom headers to included with the emails" + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "filename": { "type": "string", "minLength": 1 }, + "content": { "type": "string", "minLength": 1 } + }, + "required": ["filename", "content"] + }, + "maxItems": 10 + }, + "scheduledAt": { "type": "string", "format": "date-time" }, + "inReplyToId": { "type": "string", "nullable": true } + }, + "required": ["to", "from"] + } + } + } + }, + "responses": { + "200": { + "description": "Retrieve the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "emailId": { "type": "string" } } + } + } + } + } + } + } + }, + "/v1/emails/batch": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "Pass the optional Idempotency-Key header to make the request safe to retry. The key can be up to 256 characters. The server stores the canonical request body and behaves as follows:\n\n- Same key + same request body → returns the original emailId with 200 OK without re-sending.\n- Same key + different request body → returns 409 Conflict with code: NOT_UNIQUE so you can detect the mismatch.\n- Same key while another request is still being processed → returns 409 Conflict; retry after a short delay or once the first request completes.\n\nEntries expire after 24 hours. Use a unique key per logical send (for example, an order or signup ID)." + }, + "required": false, + "name": "Idempotency-Key", + "in": "header" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "to": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "from": { "type": "string" }, + "subject": { + "type": "string", + "minLength": 1, + "description": "Optional when templateId is provided" + }, + "templateId": { + "type": "string", + "description": "ID of a template from the dashboard" + }, + "variables": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "replyTo": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "cc": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "bcc": { + "anyOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "text": { + "type": "string", + "nullable": true, + "minLength": 1 + }, + "html": { + "type": "string", + "nullable": true, + "minLength": 1 + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1 + }, + "description": "Custom headers to included with the emails" + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "filename": { "type": "string", "minLength": 1 }, + "content": { "type": "string", "minLength": 1 } + }, + "required": ["filename", "content"] + }, + "maxItems": 10 + }, + "scheduledAt": { "type": "string", "format": "date-time" }, + "inReplyToId": { "type": "string", "nullable": true } + }, + "required": ["to", "from"] + }, + "maxItems": 100 + } + } + } + }, + "responses": { + "200": { + "description": "List of successfully created email IDs", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { "emailId": { "type": "string" } }, + "required": ["emailId"] + } + } + }, + "required": ["data"] + } + } + } + } + } + } + }, + "/v1/emails/{emailId}/cancel": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 3, + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "emailId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Retrieve the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "emailId": { "type": "string" } } + } + } + } + } + } + } + }, + "/v1/contactBooks/{contactBookId}/contacts": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 3, + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactBookId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { "type": "string" }, + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "subscribed": { "type": "boolean" } + }, + "required": ["email"] + } + } + } + }, + "responses": { + "200": { + "description": "Retrieve the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "contactId": { "type": "string" } } + } + } + } + } + } + }, + "get": { + "parameters": [ + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { "type": "string" }, + "required": false, + "name": "emails", + "in": "query" + }, + { + "schema": { "type": "number" }, + "required": false, + "name": "page", + "in": "query" + }, + { + "schema": { "type": "number" }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { "type": "string" }, + "required": false, + "name": "ids", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Retrieve multiple contacts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "firstName": { "type": "string", "nullable": true }, + "lastName": { "type": "string", "nullable": true }, + "email": { "type": "string" }, + "subscribed": { "type": "boolean", "default": true }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "contactBookId": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "id", + "email", + "properties", + "contactBookId", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + } + }, + "/v1/contactBooks/{contactBookId}/contacts/{contactId}": { + "patch": { + "parameters": [ + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "subscribed": { "type": "boolean" } + } + } + } + } + }, + "responses": { + "200": { + "description": "Retrieve the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "contactId": { "type": "string" } } + } + } + } + } + } + }, + "get": { + "parameters": [ + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Retrieve the contact", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "firstName": { "type": "string", "nullable": true }, + "lastName": { "type": "string", "nullable": true }, + "email": { "type": "string" }, + "subscribed": { "type": "boolean", "default": true }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "contactBookId": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "id", + "email", + "properties", + "contactBookId", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + }, + "put": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 3, + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactBookId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { "type": "string" }, + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "subscribed": { "type": "boolean" } + }, + "required": ["email"] + } + } + } + }, + "responses": { + "200": { + "description": "Contact upserted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "contactId": { "type": "string" } }, + "required": ["contactId"] + } + } + } + } + } + }, + "delete": { + "parameters": [ + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { "type": "string", "example": "cuiwqdj74rygf74" }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Contact deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "success": { "type": "boolean" } }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/campaigns": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "from": { "type": "string", "minLength": 1 }, + "subject": { "type": "string", "minLength": 1 }, + "previewText": { "type": "string" }, + "contactBookId": { "type": "string", "minLength": 1 }, + "content": { "type": "string", "minLength": 1 }, + "html": { "type": "string", "minLength": 1 }, + "replyTo": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + ] + }, + "cc": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + ] + }, + "bcc": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + ] + }, + "sendNow": { "type": "boolean" }, + "scheduledAt": { + "type": "string", + "description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" + }, + "batchSize": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + } + }, + "required": ["name", "from", "subject", "contactBookId"] + } + } + } + }, + "responses": { + "200": { + "description": "Create a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "from": { "type": "string" }, + "subject": { "type": "string" }, + "previewText": { "type": "string", "nullable": true }, + "contactBookId": { "type": "string", "nullable": true }, + "html": { "type": "string", "nullable": true }, + "content": { "type": "string", "nullable": true }, + "status": { "type": "string" }, + "scheduledAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "batchSize": { "type": "integer" }, + "batchWindowMinutes": { "type": "integer" }, + "total": { "type": "integer" }, + "sent": { "type": "integer" }, + "delivered": { "type": "integer" }, + "opened": { "type": "integer" }, + "clicked": { "type": "integer" }, + "unsubscribed": { "type": "integer" }, + "bounced": { "type": "integer" }, + "hardBounced": { "type": "integer" }, + "complained": { "type": "integer" }, + "replyTo": { + "type": "array", + "items": { "type": "string" } + }, + "cc": { "type": "array", "items": { "type": "string" } }, + "bcc": { "type": "array", "items": { "type": "string" } }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": [ + "id", + "name", + "from", + "subject", + "previewText", + "contactBookId", + "html", + "content", + "status", + "scheduledAt", + "batchSize", + "batchWindowMinutes", + "total", + "sent", + "delivered", + "opened", + "clicked", + "unsubscribed", + "bounced", + "hardBounced", + "complained", + "replyTo", + "cc", + "bcc", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}": { + "get": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Get campaign details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "from": { "type": "string" }, + "subject": { "type": "string" }, + "previewText": { "type": "string", "nullable": true }, + "contactBookId": { "type": "string", "nullable": true }, + "html": { "type": "string", "nullable": true }, + "content": { "type": "string", "nullable": true }, + "status": { "type": "string" }, + "scheduledAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "batchSize": { "type": "integer" }, + "batchWindowMinutes": { "type": "integer" }, + "total": { "type": "integer" }, + "sent": { "type": "integer" }, + "delivered": { "type": "integer" }, + "opened": { "type": "integer" }, + "clicked": { "type": "integer" }, + "unsubscribed": { "type": "integer" }, + "bounced": { "type": "integer" }, + "hardBounced": { "type": "integer" }, + "complained": { "type": "integer" }, + "replyTo": { + "type": "array", + "items": { "type": "string" } + }, + "cc": { "type": "array", "items": { "type": "string" } }, + "bcc": { "type": "array", "items": { "type": "string" } }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": [ + "id", + "name", + "from", + "subject", + "previewText", + "contactBookId", + "html", + "content", + "status", + "scheduledAt", + "batchSize", + "batchWindowMinutes", + "total", + "sent", + "delivered", + "opened", + "clicked", + "unsubscribed", + "bounced", + "hardBounced", + "complained", + "replyTo", + "cc", + "bcc", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}/schedule": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "scheduledAt": { + "type": "string", + "description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" + }, + "batchSize": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Schedule a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "success": { "type": "boolean" } }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}/pause": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Pause a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "success": { "type": "boolean" } }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}/resume": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Resume a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "success": { "type": "boolean" } }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/contactBooks": { + "get": { + "responses": { + "200": { + "description": "Retrieve contact books accessible by the API key", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the contact book", + "example": "clx1234567890" + }, + "name": { + "type": "string", + "description": "The name of the contact book", + "example": "Newsletter Subscribers" + }, + "teamId": { + "type": "number", + "description": "The ID of the team", + "example": 1 + }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" }, + "description": "Custom properties for the contact book", + "example": { "customField1": "value1" } + }, + "emoji": { + "type": "string", + "description": "The emoji associated with the contact book", + "example": "📙" + }, + "createdAt": { + "type": "string", + "description": "The creation timestamp" + }, + "updatedAt": { + "type": "string", + "description": "The last update timestamp" + }, + "_count": { + "type": "object", + "properties": { + "contacts": { + "type": "number", + "description": "The number of contacts in the contact book" + } + } + } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "emoji": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "Create a new contact book", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "teamId": { "type": "number" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "emoji": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/contactBooks/{id}": { + "get": { + "parameters": [ + { + "schema": { "type": "string", "example": "clx1234567890" }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Retrieve the contact book", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "teamId": { "type": "number" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "emoji": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" }, + "_count": { + "type": "object", + "properties": { + "contacts": { "type": "number" } + } + } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Contact book not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "parameters": [ + { + "schema": { "type": "string", "example": "clx1234567890" }, + "required": true, + "name": "id", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "emoji": { "type": "string" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Update the contact book", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "teamId": { "type": "number" }, + "properties": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "emoji": { "type": "string" }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "id", + "name", + "teamId", + "properties", + "emoji", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Contact book not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "parameters": [ + { + "schema": { "type": "string", "example": "clx1234567890" }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Contact book deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "success": { "type": "boolean" }, + "message": { "type": "string" } + }, + "required": ["id", "success", "message"] + } + } + } + }, + "403": { + "description": "Forbidden - API key doesn't have access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "Contact book not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + } + } } diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 4df07063..d9ffef25 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -67,11 +67,11 @@ { "group": "Contact Books", "pages": [ - "api-reference/contact-books/list-contact-books", - "api-reference/contact-books/get-contact-book", - "api-reference/contact-books/create-contact-book", - "api-reference/contact-books/update-contact-book", - "api-reference/contact-books/delete-contact-book" + "api-reference/contacts/list-contacts", + "api-reference/contacts/get-contact-book", + "api-reference/contacts/create-contact-book", + "api-reference/contacts/update-contact-book", + "api-reference/contacts/delete-contact-book" ] }, { diff --git a/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts b/apps/web/src/server/public-api/api/contacts/create-contact-book.ts similarity index 98% rename from apps/web/src/server/public-api/api/contact-books/create-contact-book.ts rename to apps/web/src/server/public-api/api/contacts/create-contact-book.ts index f32a5473..9ac0a12c 100644 --- a/apps/web/src/server/public-api/api/contact-books/create-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/create-contact-book.ts @@ -8,7 +8,7 @@ import { const route = createRoute({ method: "post", - path: "/v1/contact-books", + path: "/v1/contactBooks", request: { body: { required: true, diff --git a/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts b/apps/web/src/server/public-api/api/contacts/delete-contact-book.ts similarity index 89% rename from apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts rename to apps/web/src/server/public-api/api/contacts/delete-contact-book.ts index 629ea7f5..1b0c3378 100644 --- a/apps/web/src/server/public-api/api/contact-books/delete-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/delete-contact-book.ts @@ -5,12 +5,12 @@ import { getContactBook } from "../../api-utils"; const route = createRoute({ method: "delete", - path: "/v1/contact-books/{id}", + path: "/v1/contactBooks/{contactBookId}", request: { params: z.object({ - id: z.string().openapi({ + contactBookId: z.string().openapi({ param: { - name: "id", + name: "contactBookId", in: "path", }, example: "clx1234567890", @@ -56,7 +56,7 @@ const route = createRoute({ function deleteContactBook(app: PublicAPIApp) { app.openapi(route, async (c) => { const team = c.var.team; - const contactBookId = c.req.valid("param").id; + const contactBookId = c.req.valid("param").contactBookId; await getContactBook(c, team.id); diff --git a/apps/web/src/server/public-api/api/contact-books/get-contact-book.ts b/apps/web/src/server/public-api/api/contacts/get-contact-book.ts similarity index 90% rename from apps/web/src/server/public-api/api/contact-books/get-contact-book.ts rename to apps/web/src/server/public-api/api/contacts/get-contact-book.ts index 09f54663..b4149241 100644 --- a/apps/web/src/server/public-api/api/contact-books/get-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/get-contact-book.ts @@ -6,12 +6,12 @@ import { UnsendApiError } from "../../api-error"; const route = createRoute({ method: "get", - path: "/v1/contact-books/{id}", + path: "/v1/contactBooks/{contactBookId}", request: { params: z.object({ - id: z.string().openapi({ + contactBookId: z.string().openapi({ param: { - name: "id", + name: "contactBookId", in: "path", }, example: "clx1234567890", @@ -54,7 +54,7 @@ const route = createRoute({ function getContactBook(app: PublicAPIApp) { app.openapi(route, async (c) => { const team = c.var.team; - const contactBookId = c.req.valid("param").id; + const contactBookId = c.req.valid("param").contactBookId; const contactBook = await db.contactBook.findFirst({ where: { diff --git a/apps/web/src/server/public-api/api/contact-books/get-contact-books.ts b/apps/web/src/server/public-api/api/contacts/get-contact-books.ts similarity index 97% rename from apps/web/src/server/public-api/api/contact-books/get-contact-books.ts rename to apps/web/src/server/public-api/api/contacts/get-contact-books.ts index fcda76d6..cc164a93 100644 --- a/apps/web/src/server/public-api/api/contact-books/get-contact-books.ts +++ b/apps/web/src/server/public-api/api/contacts/get-contact-books.ts @@ -5,7 +5,7 @@ import { getContactBooks as getContactBooksService } from "~/server/service/cont const route = createRoute({ method: "get", - path: "/v1/contact-books", + path: "/v1/contactBooks", responses: { 200: { content: { diff --git a/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts similarity index 89% rename from apps/web/src/server/public-api/api/contact-books/update-contact-book.ts rename to apps/web/src/server/public-api/api/contacts/update-contact-book.ts index 1d9f7041..d0e65855 100644 --- a/apps/web/src/server/public-api/api/contact-books/update-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts @@ -6,12 +6,12 @@ import { getContactBook } from "../../api-utils"; const route = createRoute({ method: "patch", - path: "/v1/contact-books/{id}", + path: "/v1/contactBooks/{contactBookId}", request: { params: z.object({ - id: z.string().openapi({ + contactBookId: z.string().openapi({ param: { - name: "id", + name: "contactBookId", in: "path", }, example: "clx1234567890", @@ -66,11 +66,13 @@ const route = createRoute({ function updateContactBook(app: PublicAPIApp) { app.openapi(route, async (c) => { const team = c.var.team; - const contactBookId = c.req.valid("param").id; + const contactBookId = c.req.valid("param").contactBookId; const body = c.req.valid("json"); await getContactBook(c, team.id); + console.log({ contactBookId }); + const updated = await updateContactBookService(contactBookId, body); return c.json({ diff --git a/apps/web/src/server/public-api/index.ts b/apps/web/src/server/public-api/index.ts index 3823c1b3..e2d5c0e8 100644 --- a/apps/web/src/server/public-api/index.ts +++ b/apps/web/src/server/public-api/index.ts @@ -21,11 +21,11 @@ import getCampaign from "./api/campaigns/get-campaign"; import scheduleCampaign from "./api/campaigns/schedule-campaign"; import pauseCampaign from "./api/campaigns/pause-campaign"; import resumeCampaign from "./api/campaigns/resume-campaign"; -import getContactBooks from "./api/contact-books/get-contact-books"; -import createContactBook from "./api/contact-books/create-contact-book"; -import getContactBook from "./api/contact-books/get-contact-book"; -import updateContactBook from "./api/contact-books/update-contact-book"; -import deleteContactBook from "./api/contact-books/delete-contact-book"; +import getContactBooks from "./api/contacts/get-contact-books"; +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"; export const app = getApp(); From 9d55babe64554d14a00da16dd87eac55a444cb34 Mon Sep 17 00:00:00 2001 From: David Stockley Date: Sun, 4 Jan 2026 18:48:13 +0000 Subject: [PATCH 4/7] docs: update docs --- apps/docs/api-reference/openapi.json | 18 ++++++++++++++---- apps/docs/docs.json | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 573b1edb..6b1a7abb 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -1288,6 +1288,16 @@ "required": true, "name": "contactBookId", "in": "path" + }, + { + "schema": { + "type": "string", + "minLength": 3, + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactId", + "in": "path" } ], "requestBody": { @@ -1822,13 +1832,13 @@ } } }, - "/v1/contactBooks/{id}": { + "/v1/contactBooks/{contactBookId}": { "get": { "parameters": [ { "schema": { "type": "string", "example": "clx1234567890" }, "required": true, - "name": "id", + "name": "contactBookId", "in": "path" } ], @@ -1901,7 +1911,7 @@ { "schema": { "type": "string", "example": "clx1234567890" }, "required": true, - "name": "id", + "name": "contactBookId", "in": "path" } ], @@ -1986,7 +1996,7 @@ { "schema": { "type": "string", "example": "clx1234567890" }, "required": true, - "name": "id", + "name": "contactBookId", "in": "path" } ], diff --git a/apps/docs/docs.json b/apps/docs/docs.json index d9ffef25..a04d5b8f 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -67,7 +67,7 @@ { "group": "Contact Books", "pages": [ - "api-reference/contacts/list-contacts", + "api-reference/contacts/list-contact-books", "api-reference/contacts/get-contact-book", "api-reference/contacts/create-contact-book", "api-reference/contacts/update-contact-book", From b3306e9b7af6bad1e73afdef142f81f8a8b522e4 Mon Sep 17 00:00:00 2001 From: Dave Stockley Date: Sun, 4 Jan 2026 19:17:28 +0000 Subject: [PATCH 5/7] refactor: update apps/web/src/server/public-api/api/contacts/update-contact-book.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/server/public-api/api/contacts/update-contact-book.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/server/public-api/api/contacts/update-contact-book.ts b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts index d0e65855..d77d73bf 100644 --- a/apps/web/src/server/public-api/api/contacts/update-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts @@ -71,7 +71,7 @@ function updateContactBook(app: PublicAPIApp) { await getContactBook(c, team.id); - console.log({ contactBookId }); + const updated = await updateContactBookService(contactBookId, body); const updated = await updateContactBookService(contactBookId, body); From 3e9035ae3bffe0ee3b4428d379577b364e98cdf6 Mon Sep 17 00:00:00 2001 From: Dave Stockley Date: Sun, 4 Jan 2026 19:23:40 +0000 Subject: [PATCH 6/7] fix: update apps/web/src/server/public-api/api/contacts/update-contact-book.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/server/public-api/api/contacts/update-contact-book.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/server/public-api/api/contacts/update-contact-book.ts b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts index d77d73bf..c9fbbac7 100644 --- a/apps/web/src/server/public-api/api/contacts/update-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts @@ -73,7 +73,7 @@ function updateContactBook(app: PublicAPIApp) { const updated = await updateContactBookService(contactBookId, body); - const updated = await updateContactBookService(contactBookId, body); + return c.json({ return c.json({ ...updated, From 6f524abbaa5617cd1ca6bef94ecf9e71fa3d0641 Mon Sep 17 00:00:00 2001 From: David Stockley Date: Sun, 4 Jan 2026 19:25:24 +0000 Subject: [PATCH 7/7] fix: code rabbit format error --- .../src/server/public-api/api/contacts/update-contact-book.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/server/public-api/api/contacts/update-contact-book.ts b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts index c9fbbac7..0adc2847 100644 --- a/apps/web/src/server/public-api/api/contacts/update-contact-book.ts +++ b/apps/web/src/server/public-api/api/contacts/update-contact-book.ts @@ -73,8 +73,6 @@ function updateContactBook(app: PublicAPIApp) { const updated = await updateContactBookService(contactBookId, body); - return c.json({ - return c.json({ ...updated, properties: updated.properties as Record,