Midday / apps /api /src /schemas /transactions.ts
Jules
Final deployment with all fixes and verified content
c09f67c
import { z } from "@hono/zod-openapi";
export const getTransactionsSchema = z.object({
cursor: z
.string()
.nullable()
.optional()
.openapi({
description:
"Cursor for pagination, representing the last item from the previous page",
example: "eyJpZCI6IjEyMyJ9",
param: {
in: "query",
},
}),
sort: z
.array(z.string().min(1))
.max(2)
.min(2)
.nullable()
.optional()
.openapi({
description:
"Sorting order as a tuple: [field, direction]. Example: ['date', 'desc'] or ['amount', 'asc']",
example: ["date", "desc"],
param: {
in: "query",
},
}),
pageSize: z.coerce
.number()
.min(1)
.max(10000)
.optional()
.openapi({
description: "Number of transactions to return per page (1-10000)",
example: 50,
param: {
in: "query",
},
}),
q: z
.string()
.nullable()
.optional()
.openapi({
description:
"Search query string to filter transactions by name, description, or other text fields",
example: "office supplies",
param: {
in: "query",
},
}),
categories: z
.array(z.string())
.nullable()
.optional()
.openapi({
description:
"Array of category slugs to filter transactions by specific categories",
example: ["office-supplies", "travel"],
param: {
in: "query",
},
}),
tags: z
.array(z.string())
.nullable()
.optional()
.openapi({
description: "Array of tag IDs to filter transactions by specific tags",
example: ["tag-1", "tag-2"],
param: {
in: "query",
},
}),
start: z
.string()
.nullable()
.optional()
.openapi({
description:
"Start date (inclusive) for filtering transactions in ISO 8601 format",
example: "2024-04-01T00:00:00.000Z",
param: {
in: "query",
},
}),
end: z
.string()
.nullable()
.optional()
.openapi({
description:
"End date (inclusive) for filtering transactions in ISO 8601 format",
example: "2024-04-30T23:59:59.999Z",
param: {
in: "query",
},
}),
accounts: z
.array(z.string())
.nullable()
.optional()
.openapi({
description:
"Array of bank account IDs to filter transactions by specific accounts",
example: ["account-1", "account-2"],
param: {
in: "query",
},
}),
assignees: z
.array(z.string())
.nullable()
.optional()
.openapi({
description: "Array of user IDs to filter transactions by assigned users",
example: ["user-1", "user-2"],
param: {
in: "query",
},
}),
statuses: z
.array(z.string())
.nullable()
.optional()
.openapi({
description:
"Array of transaction statuses to filter by. Available statuses: 'pending', 'completed', 'archived', 'posted', 'excluded'",
example: ["pending", "completed"],
param: {
in: "query",
},
}),
recurring: z
.array(z.string())
.nullable()
.optional()
.openapi({
description:
"Array of recurring frequency values to filter by. Available frequencies: 'weekly', 'monthly', 'annually', 'irregular'",
example: ["monthly", "annually"],
param: {
in: "query",
},
}),
attachments: z
.enum(["include", "exclude"])
.nullable()
.optional()
.openapi({
description:
"Filter transactions based on attachment presence. 'include' returns only transactions with attachments, 'exclude' returns only transactions without attachments",
example: "include",
param: {
in: "query",
},
}),
amountRange: z
.array(z.coerce.number())
.nullable()
.optional()
.openapi({
description:
"Amount range as [min, max] to filter transactions by monetary value",
example: [100, 1000],
param: {
in: "query",
},
}),
amount: z
.array(z.string())
.nullable()
.optional()
.openapi({
description:
"Array of specific amounts (as strings) to filter transactions by exact values",
example: ["150.75", "299.99"],
param: {
in: "query",
},
}),
type: z
.enum(["income", "expense"])
.nullable()
.optional()
.openapi({
description:
"Filter by transaction type. 'income' for money received, 'expense' for money spent",
example: "expense",
param: {
in: "query",
},
}),
manual: z
.enum(["include", "exclude"])
.nullable()
.optional()
.openapi({
description:
"Filter transactions based on whether they were manually imported. 'include' returns only manual transactions, 'exclude' returns only non-manual transactions",
example: "include",
param: {
in: "query",
},
}),
exported: z
.boolean()
.nullable()
.optional()
.openapi({
description:
"Filter by export status. true = only exported transactions, false = only NOT exported transactions, undefined = no filter",
example: false,
param: {
in: "query",
},
}),
fulfilled: z
.boolean()
.nullable()
.optional()
.openapi({
description:
"Filter by fulfillment status. true = transactions ready for review (has attachments OR status=completed), false = not ready, undefined = no filter",
example: true,
param: {
in: "query",
},
}),
});
export const transactionResponseSchema = z
.object({
id: z.string().uuid().openapi({
description: "Unique identifier of the transaction",
example: "b3b7c8e2-1f2a-4c3d-9e4f-5a6b7c8d9e0f",
}),
name: z.string().openapi({
description: "Name or description of the transaction",
example: "Office Supplies Purchase",
}),
amount: z.number().openapi({
description: "Monetary amount of the transaction",
example: 150.75,
}),
taxAmount: z.number().nullable().openapi({
description: "Tax amount of the transaction",
example: 15.07,
}),
taxRate: z.number().nullable().openapi({
description: "Tax rate of the transaction",
example: 10,
}),
taxType: z.string().nullable().openapi({
description: "Tax type of the transaction",
example: "VAT",
}),
currency: z.string().openapi({
description: "Currency code of the transaction in ISO 4217 format",
example: "USD",
}),
counterpartyName: z.string().nullable().openapi({
description: "Name of the counterparty",
example: "Spotify AB",
}),
date: z.string().openapi({
description: "Date and time of the transaction in ISO 8601 format",
example: "2024-05-01T12:00:00.000Z",
}),
category: z
.object({
id: z.string().openapi({
description: "Unique identifier of the category",
example: "office-supplies",
}),
name: z.string().openapi({
description: "Display name of the category",
example: "Office Supplies",
}),
color: z.string().openapi({
description:
"Hex color code associated with the category for UI display",
example: "#FF5733",
}),
taxRate: z.number().nullable().openapi({
description: "Tax rate of the category",
example: 10,
}),
taxType: z.string().nullable().openapi({
description: "Tax type of the category",
example: "VAT",
}),
slug: z.string().openapi({
description: "URL-friendly slug of the category",
example: "office-supplies",
}),
})
.nullable()
.openapi({
description:
"Category information assigned to the transaction for organization",
example: {
id: "office-supplies",
name: "Office Supplies",
color: "#FF5733",
slug: "office-supplies",
taxRate: 10,
taxType: "VAT",
},
}),
status: z.string().openapi({
description: "Current status of the transaction",
example: "completed",
}),
internal: z.boolean().nullable().openapi({
description: "Whether the transaction is internal (between own accounts)",
example: false,
}),
recurring: z.boolean().nullable().openapi({
description: "Whether the transaction is part of a recurring series",
example: false,
}),
manual: z.boolean().nullable().openapi({
description:
"Whether the transaction was created manually (via API/form) rather than imported from bank connections",
example: false,
}),
frequency: z.string().nullable().openapi({
description:
"Frequency of the recurring transaction if applicable (weekly, monthly, annually, irregular)",
example: "monthly",
}),
isFulfilled: z.boolean().openapi({
description: "Whether the transaction has been fulfilled or processed",
example: true,
}),
note: z.string().nullable().openapi({
description: "Optional note or memo attached to the transaction",
example: "Paid with company credit card for office renovation",
}),
account: z
.object({
id: z.string().openapi({
description: "Unique identifier of the bank account",
example: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
}),
name: z.string().openapi({
description: "Display name of the bank account",
example: "Company Credit Card",
}),
currency: z.string().openapi({
description: "Currency of the bank account in ISO 4217 format",
example: "USD",
}),
connection: z
.object({
id: z.string().openapi({
description: "Unique identifier of the bank connection",
example: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
}),
name: z.string().openapi({
description: "Name of the bank institution",
example: "Chase Bank",
}),
logoUrl: z.string().nullable().openapi({
description: "URL to the bank institution's logo image",
example: "https://cdn.midday.ai/logos/chase-bank.png",
}),
})
.openapi({
description:
"Bank connection information associated with the account",
example: {
id: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
name: "Chase Bank",
logoUrl: "https://cdn.midday.ai/logos/chase-bank.png",
},
}),
})
.openapi({
description: "Bank account information associated with the transaction",
example: {
id: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
name: "Company Credit Card",
currency: "USD",
connection: {
id: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
name: "Chase Bank",
logoUrl: "https://cdn.midday.ai/logos/chase-bank.png",
},
},
}),
tags: z
.array(
z.object({
id: z.string().openapi({
description: "Unique identifier of the tag",
example: "b7e2f8c1-3d4a-4e2b-9f1a-2c3d4e5f6a7b",
}),
name: z.string().nullable().openapi({
description: "Display name of the tag",
example: "invoice",
}),
}),
)
.nullable()
.openapi({
description:
"Array of tags associated with the transaction for categorization and filtering",
example: [
{ id: "b7e2f8c1-3d4a-4e2b-9f1a-2c3d4e5f6a7b", name: "invoice" },
{ id: "c8e2f8c1-3d4a-4e2b-9f1a-2c3d4e5f6a7c", name: "travel" },
],
}),
attachments: z
.array(
z.object({
id: z.string().openapi({
description: "Unique identifier of the attachment",
example: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
}),
path: z.array(z.string()).openapi({
description:
"Array of path segments for the attachment file storage location",
example: [
"dd6a039e-d071-423a-9a4d-9ba71325d890",
"transactions",
"1d2c3753-79d7-45b0-9c40-60f482bac8e8",
"receipt.pdf",
],
}),
size: z.number().openapi({
description: "Size of the attachment file in bytes",
example: 1928716,
}),
type: z.string().openapi({
description: "MIME type of the attachment file",
example: "application/pdf",
}),
filename: z.string().nullable().openapi({
description: "Original filename of the attachment when uploaded",
example: "receipt.pdf",
}),
}),
)
.nullable()
.openapi({
description:
"Array of file attachments associated with the transaction (receipts, invoices, etc.)",
example: [
{
id: "b7e2f8c1-3d4a-4e2b-9f1a-2c3d4e5f6a7b",
path: [
"e1f2d3c4-b5a6-7d8e-9f0a-1b2c3d4e5f6a",
"transactions",
"9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"receipt.pdf",
],
size: 1928716,
type: "application/pdf",
filename: "receipt.pdf",
},
],
}),
})
.openapi("TransactionResponse");
export const transactionsResponseSchema = z.object({
meta: z
.object({
cursor: z.string().optional().openapi({
description:
"Cursor for the next page of results, undefined if no more pages",
example: "eyJpZCI6IjQ1NiJ9",
}),
hasPreviousPage: z.boolean().openapi({
description:
"Whether there are more transactions available on the previous page",
example: false,
}),
hasNextPage: z.boolean().openapi({
description:
"Whether there are more transactions available on the next page",
example: true,
}),
})
.openapi({
description: "Pagination metadata for the transactions response",
}),
data: z.array(transactionResponseSchema).openapi({
description: "Array of transactions matching the query criteria",
}),
});
export const deleteTransactionsSchema = z
.array(z.string().uuid())
.max(100)
.min(1)
.openapi({
description: "List of transaction IDs to delete.",
});
export const deleteTransactionResponseSchema = z.object({
id: z.string().uuid().openapi({
description: "Transaction ID (UUID).",
}),
});
export const deleteTransactionsResponseSchema = z.array(
deleteTransactionResponseSchema,
);
export const deleteTransactionSchema = z.object({
id: z
.string()
.uuid()
.openapi({
description: "Transaction ID (UUID).",
param: {
in: "path",
name: "id",
},
}),
});
export const getTransactionByIdSchema = z.object({
id: z
.string()
.uuid()
.openapi({
description: "Transaction ID (UUID).",
param: {
in: "path",
name: "id",
},
}),
});
export const updateTransactionSchema = z.object({
id: z
.string()
.uuid()
.openapi({
description: "Transaction ID (UUID).",
param: {
in: "path",
name: "id",
},
}),
name: z.string().optional().openapi({
description: "Name/description of the transaction.",
}),
amount: z.number().optional().openapi({
description: "Amount of the transaction.",
}),
currency: z.string().optional().openapi({
description: "Currency of the transaction.",
}),
date: z.string().optional().openapi({
description: "Date of the transaction (ISO 8601).",
}),
bankAccountId: z.string().optional().openapi({
description: "Bank account ID associated with the transaction.",
}),
categorySlug: z.string().nullable().optional().openapi({
description: "Category slug for the transaction.",
}),
status: z
.enum([
"pending",
"archived",
"completed",
"posted",
"excluded",
"exported",
])
.nullable()
.optional()
.openapi({
description: "Status of the transaction.",
}),
internal: z.boolean().optional().openapi({
description: "Whether the transaction is internal.",
}),
recurring: z.boolean().optional().openapi({
description: "Whether the transaction is recurring.",
}),
frequency: z
.enum(["weekly", "monthly", "annually", "irregular"])
.nullable()
.optional()
.openapi({
description: "Recurring frequency of the transaction.",
}),
note: z.string().nullable().optional().openapi({
description: "Note for the transaction.",
}),
assignedId: z.string().nullable().optional().openapi({
description: "Assigned user ID for the transaction.",
}),
taxRate: z.number().nullable().optional().openapi({
description:
"Tax rate as a percentage (e.g., 25 for 25% VAT). Only set when tax is calculated from a percentage.",
}),
taxAmount: z.number().nullable().optional().openapi({
description:
"Tax amount in the transaction currency. Always set when tax is present.",
}),
});
export const updateTransactionsSchema = z.object({
ids: z.array(z.string()).openapi({
description: "Array of transaction IDs to update.",
}),
categorySlug: z.string().nullable().optional().openapi({
description: "Category slug for the transactions.",
}),
status: z
.enum([
"pending",
"archived",
"completed",
"posted",
"excluded",
"exported",
])
.nullable()
.optional()
.openapi({
description: "Status to set for the transactions.",
}),
frequency: z
.enum(["weekly", "monthly", "annually", "irregular"])
.nullable()
.optional()
.openapi({
description: "Recurring frequency to set for the transactions.",
}),
internal: z.boolean().optional().openapi({
description: "Whether the transactions are internal.",
}),
note: z.string().nullable().optional().openapi({
description: "Note to set for the transactions.",
}),
assignedId: z.string().nullable().optional().openapi({
description: "Assigned user ID for the transactions.",
}),
recurring: z.boolean().optional().openapi({
description: "Whether the transactions are recurring.",
}),
tagId: z.string().nullable().optional().openapi({
description: "Tag ID to set for the transactions.",
}),
});
export const getSimilarTransactionsSchema = z.object({
name: z.string().openapi({
description: "Name of the transaction.",
param: {
in: "query",
},
}),
categorySlug: z
.string()
.optional()
.openapi({
description: "Category slug to filter similar transactions.",
param: {
in: "query",
},
}),
frequency: z
.enum(["weekly", "monthly", "annually", "irregular"])
.optional()
.openapi({
description: "Recurring frequency to filter similar transactions.",
param: {
in: "query",
},
}),
transactionId: z.string().uuid().optional().openapi({
description: "Transaction ID to exclude from results.",
}),
minSimilarityScore: z
.number()
.min(0.1)
.max(1.0)
.optional()
.default(0.8)
.openapi({
description:
"Minimum similarity score (0.1-1.0) for transactions to be considered similar.",
param: {
in: "query",
},
}),
});
export const searchTransactionMatchSchema = z.object({
query: z
.string()
.optional()
.openapi({
description: "Search query for matching transactions.",
param: {
in: "query",
},
}),
inboxId: z
.string()
.uuid()
.optional()
.openapi({
description: "Inbox ID to search within.",
param: {
in: "query",
},
}),
maxResults: z
.number()
.optional()
.openapi({
description: "Maximum number of results to return.",
param: {
in: "query",
},
}),
minConfidenceScore: z
.number()
.optional()
.openapi({
description: "Minimum confidence score for matches.",
param: {
in: "query",
},
}),
includeAlreadyMatched: z
.boolean()
.optional()
.openapi({
description: "Whether to include already matched transactions.",
param: {
in: "query",
},
}),
});
export const createTransactionSchema = z.object({
name: z.string().openapi({
description: "Name of the transaction.",
}),
amount: z.number().openapi({
description: "Amount of the transaction.",
}),
currency: z.string().openapi({
description: "Currency of the transaction.",
}),
date: z.string().openapi({
description: "Date of the transaction (ISO 8601).",
}),
bankAccountId: z.string().openapi({
description: "Bank account ID associated with the transaction.",
}),
assignedId: z.string().optional().openapi({
description: "Assigned user ID for the transaction.",
}),
categorySlug: z.string().optional().openapi({
description: "Category slug for the transaction.",
}),
note: z.string().optional().openapi({
description: "Note for the transaction.",
}),
internal: z.boolean().optional().openapi({
description: "Whether the transaction is internal.",
}),
attachments: z
.array(
z.object({
path: z.array(z.string()).openapi({
description: "Path(s) of the attachment file(s).",
}),
name: z.string().openapi({
description: "Name of the attachment file.",
}),
size: z.number().openapi({
description: "Size of the attachment file in bytes.",
}),
type: z.string().openapi({
description: "MIME type of the attachment file.",
}),
}),
)
.optional()
.openapi({
description: "Array of attachments for the transaction.",
}),
});
export const createTransactionsSchema = z
.array(createTransactionSchema)
.max(100)
.min(1)
.openapi({
description: "List of transactions to create.",
});
export const getTransactionAttachmentPreSignedUrlSchema = z.object({
transactionId: z
.string()
.uuid()
.openapi({
description: "Unique identifier of the transaction",
example: "b3b7c1e2-4c2a-4e7a-9c1a-2b7c1e24c2a4",
param: {
in: "path",
name: "transactionId",
},
}),
attachmentId: z
.string()
.uuid()
.openapi({
description:
"Unique identifier of the attachment to generate a pre-signed URL for",
example: "a43dc3a5-6925-4d91-ac9c-4c1a34bdb388",
param: {
in: "path",
name: "attachmentId",
},
}),
download: z.coerce
.boolean()
.optional()
.openapi({
description:
"Whether to force download the file. If true, the file will be downloaded. If false or omitted, the file will be displayed in the browser if possible.",
example: true,
param: {
in: "query",
name: "download",
},
}),
});
export const transactionAttachmentPreSignedUrlResponseSchema = z.object({
url: z.string().url().openapi({
description:
"Pre-signed URL for accessing the attachment, valid for 60 seconds",
example:
"https://service.midday.ai/storage/v1/object/sign/vault/transactions/receipt.pdf?token=abc123&expires=1640995200",
}),
expiresAt: z.string().datetime().openapi({
description: "ISO 8601 timestamp when the URL expires",
example: "2024-04-15T10:01:00.000Z",
}),
fileName: z.string().nullable().openapi({
description: "Original filename of the attachment",
example: "receipt.pdf",
}),
});
export const createTransactionsResponseSchema = z.array(
transactionResponseSchema,
);
export const exportTransactionsSchema = z.object({
transactionIds: z.array(z.string().uuid()).min(1),
dateFormat: z.string().optional(),
locale: z.string().optional().default("en"),
exportSettings: z
.object({
csvDelimiter: z.string(),
includeCSV: z.boolean(),
includeXLSX: z.boolean(),
sendEmail: z.boolean(),
accountantEmail: z.string().optional(),
})
.refine(
(data) => {
// Only validate email if sendEmail is true
if (data.sendEmail) {
if (!data.accountantEmail || data.accountantEmail.trim() === "") {
return false;
}
return z.string().email().safeParse(data.accountantEmail.trim())
.success;
}
return true;
},
{
message: "Invalid email address",
path: ["accountantEmail"],
},
)
.optional(),
});
export const importTransactionsSchema = z.object({
filePath: z.array(z.string()).optional(),
bankAccountId: z.string().uuid(),
currency: z.string(),
currentBalance: z.string().optional(),
inverted: z.boolean(),
mappings: z.object({
amount: z.string(),
date: z.string(),
description: z.string(),
balance: z.string().optional(),
}),
});
export const moveToReviewSchema = z.object({
transactionId: z.string().uuid().openapi({
description: "Transaction ID to move back to review.",
}),
});