import { readFile } from "node:fs/promises";
export interface DocuSealClientConfig {
baseUrl: string;
apiKey: string;
}
export class DocuSealApiError extends Error {
status: number;
body: unknown;
constructor(message: string, status: number, body: unknown) {
super(message);
this.name = "DocuSealApiError";
this.status = status;
this.body = body;
}
}
export class DocuSealClient {
private readonly baseUrl: string;
private readonly apiKey: string;
constructor(config: DocuSealClientConfig) {
this.baseUrl = config.baseUrl.replace(/\/$/, "");
this.apiKey = config.apiKey;
}
private buildUrl(path: string, query?: Record<string, unknown>): string {
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`);
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null || value === "") continue;
url.searchParams.set(key, String(value));
}
}
return url.toString();
}
private async parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
if (contentType.includes("application/json")) {
return response.json();
}
return response.text();
}
private async request<T>(
method: string,
path: string,
opts?: {
query?: Record<string, unknown>;
body?: unknown;
headers?: Record<string, string>;
}
): Promise<T> {
const url = this.buildUrl(path, opts?.query);
const headers: Record<string, string> = {
"X-Auth-Token": this.apiKey,
...(opts?.headers ?? {}),
};
let body: BodyInit | undefined;
if (opts?.body !== undefined) {
if (opts.body instanceof FormData) {
body = opts.body;
} else {
headers["Content-Type"] = "application/json";
body = JSON.stringify(opts.body);
}
}
const response = await fetch(url, { method, headers, body });
const payload = await this.parseResponseBody(response);
if (!response.ok) {
const message =
typeof payload === "object" && payload && "error" in payload
? `DocuSeal API error ${response.status}: ${String((payload as { error: unknown }).error)}`
: `DocuSeal API error ${response.status}`;
throw new DocuSealApiError(message, response.status, payload);
}
return payload as T;
}
listTemplates(params: { limit?: number; after?: number; before?: number; q?: string; archived?: boolean }) {
return this.request("GET", "/api/templates", { query: params });
}
getTemplate(id: number) {
return this.request("GET", `/api/templates/${id}`);
}
async createTemplateFromPdf(input: {
name?: string;
filePath?: string;
fileBase64?: string;
filename?: string;
}) {
const formData = new FormData();
let fileBuffer: Buffer;
let filename = input.filename ?? "template.pdf";
if (input.filePath) {
fileBuffer = await readFile(input.filePath);
filename = input.filename ?? input.filePath.split("/").pop() ?? "template.pdf";
} else if (input.fileBase64) {
fileBuffer = Buffer.from(input.fileBase64, "base64");
} else {
throw new Error("Either filePath or fileBase64 must be provided");
}
const pdfBlob = new Blob([new Uint8Array(fileBuffer)], { type: "application/pdf" });
formData.append("file", pdfBlob, filename);
if (input.name) {
formData.append("name", input.name);
}
return this.request("POST", "/api/templates/pdf", { body: formData });
}
createSubmission(body: {
template_id: number;
submitters: Array<Record<string, unknown>>;
message?: string | { subject?: string; body?: string };
send_email?: boolean;
[key: string]: unknown;
}) {
const payload: Record<string, unknown> = { ...body };
if (typeof body.message === "string") {
payload.message = { body: body.message };
}
return this.request("POST", "/api/submissions", { body: payload });
}
listSubmissions(params: {
template_id?: number;
status?: "pending" | "completed" | "declined" | "expired";
q?: string;
slug?: string;
template_folder?: string;
archived?: boolean;
limit?: number;
after?: number;
before?: number;
}) {
return this.request("GET", "/api/submissions", { query: params });
}
getSubmission(id: number) {
return this.request("GET", `/api/submissions/${id}`);
}
getSubmissionDocuments(id: number, merge?: boolean) {
return this.request<{ id: number; documents: Array<{ name?: string; url: string }> }>(
"GET",
`/api/submissions/${id}/documents`,
{ query: { merge } }
);
}
listSubmitters(params: { submission_id?: number; limit?: number; after?: number; before?: number }) {
return this.request("GET", "/api/submitters", { query: params });
}
getSubmitter(id: number) {
return this.request("GET", `/api/submitters/${id}`);
}
updateSubmitter(id: number, body: Record<string, unknown>) {
return this.request("PUT", `/api/submitters/${id}`, { body });
}
}