API Reference
Complete reference for all OneLibro API endpoints.
Table of Contents
- Overview
- Authentication
- Plaid APIs
- Transaction APIs
- Admin Auth APIs
- Admin Management APIs
- Error Handling
Overview
OneLibro uses Next.js API Routes (serverless functions) for all backend operations.
Base URL (Development):
http://localhost:3000/api
Base URL (Production):
https://finance.yatheeshnagella.com/api
API Categories
| Category | Base Path | Auth Required |
|---|---|---|
| Plaid Integration | /api/plaid/* | User JWT |
| Transactions | /api/transactions/* | User JWT |
| Admin Auth | /api/admin/auth/* | None (creates session) |
| Admin Management | /api/admin/* | Admin Session Token |
Authentication
User API Authentication
All user APIs require JWT token from Supabase:
Authorization: Bearer <supabase_jwt_token>
Example:
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch('/api/plaid/create-link-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({ userId: user.id }),
});
Admin API Authentication
Admin APIs require session token (from login):
Cookie: admin_session=<session_token>
Or via header:
Authorization: Bearer <session_token>
Plaid APIs
Create Link Token
Generate a Plaid Link token for connecting bank accounts.
Endpoint: POST /api/plaid/create-link-token
Authentication: User JWT required
Request Body:
{
"userId": "uuid-string"
}
Response (200 OK):
{
"link_token": "link-sandbox-12345678-1234-1234-1234-123456789012",
"expiration": "2024-12-04T12:00:00Z"
}
Response (401 Unauthorized):
{
"error": "Unauthorized"
}
Response (500 Error):
{
"error": "Failed to create link token"
}
Example Usage:
const createLinkToken = async (userId: string) => {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch('/api/plaid/create-link-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({ userId }),
});
const data = await response.json();
return data.link_token;
};
Exchange Public Token
Exchange Plaid's public token for an access token after user connects bank.
Endpoint: POST /api/plaid/exchange-token
Authentication: User JWT required
Request Body:
{
"publicToken": "public-sandbox-12345678-1234-1234-1234-123456789012",
"userId": "uuid-string",
"institutionId": "ins_3",
"institutionName": "Chase"
}
Response (200 OK):
{
"success": true,
"itemId": "uuid-of-plaid-item",
"accounts": [
{
"id": "uuid-of-account",
"name": "Chase Checking",
"type": "depository",
"subtype": "checking",
"mask": "0000",
"currentBalance": 250000,
"availableBalance": 245000
}
]
}
Response (400 Bad Request):
{
"error": "Missing required fields"
}
Response (500 Error):
{
"error": "Failed to exchange token"
}
Implementation Details:
- Exchanges public token with Plaid API
- Encrypts access token (AES-256-CBC)
- Stores encrypted token in
plaid_itemstable - Fetches and stores accounts in
accountstable - Returns account list to client
Example Usage:
const exchangeToken = async (publicToken: string, metadata: PlaidLinkMetadata) => {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({
publicToken,
userId: user.id,
institutionId: metadata.institution?.institution_id,
institutionName: metadata.institution?.name,
}),
});
return response.json();
};
Sync Transactions
Sync transactions from Plaid for a specific item.
Endpoint: POST /api/plaid/sync-transactions
Authentication: User JWT required
Request Body:
{
"itemId": "uuid-of-plaid-item"
}
Response (200 OK):
{
"success": true,
"added": 15,
"modified": 3,
"removed": 1,
"nextCursor": "cursor-string-for-next-sync"
}
Response (404 Not Found):
{
"error": "Plaid item not found"
}
Response (500 Error):
{
"error": "Failed to sync transactions"
}
Implementation Details:
- Retrieves encrypted access token from database
- Decrypts access token
- Calls Plaid
transactionsSyncAPI with cursor - Processes added/modified transactions (upsert to DB)
- Processes removed transactions (soft delete)
- Updates account balances
- Saves next cursor for incremental sync
Example Usage:
const syncTransactions = async (itemId: string) => {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch('/api/plaid/sync-transactions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({ itemId }),
});
return response.json();
};
Unlink Account
Disconnect a Plaid item and remove all associated accounts.
Endpoint: POST /api/plaid/unlink-account
Authentication: User JWT required
Request Body:
{
"itemId": "uuid-of-plaid-item"
}
Response (200 OK):
{
"success": true
}
Response (404 Not Found):
{
"error": "Item not found"
}
Example Usage:
const unlinkAccount = async (itemId: string) => {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch('/api/plaid/unlink-account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({ itemId }),
});
return response.json();
};
Plaid Webhook
Receive webhooks from Plaid for transaction updates, errors, etc.
Endpoint: POST /api/plaid/webhook
Authentication: Webhook signature verification
Request Body (Example - TRANSACTIONS_UPDATE):
{
"webhook_type": "TRANSACTIONS",
"webhook_code": "SYNC_UPDATES_AVAILABLE",
"item_id": "plaid-item-id-12345",
"initial_update_complete": true,
"historical_update_complete": true
}
Response (200 OK):
{
"success": true
}
Webhook Types Handled:
TRANSACTIONS.SYNC_UPDATES_AVAILABLE- New transactions availableITEM.ERROR- Item connection errorITEM.LOGIN_REQUIRED- User needs to re-authenticate
Implementation:
export async function POST(request: Request) {
const body = await request.json();
// Verify webhook signature
const signature = request.headers.get('plaid-verification');
const isValid = verifyWebhookSignature(signature, body);
if (!isValid) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// Handle webhook
switch (body.webhook_code) {
case 'SYNC_UPDATES_AVAILABLE':
// Trigger background sync
await triggerTransactionSync(body.item_id);
break;
case 'LOGIN_REQUIRED':
// Update item status
await updateItemStatus(body.item_id, 'login_required');
break;
}
return NextResponse.json({ success: true });
}
Transaction APIs
Create Transaction
Create a manual transaction (not from Plaid).
Endpoint: POST /api/transactions/create
Authentication: User JWT required
Request Body:
{
"userId": "uuid-string",
"accountId": "uuid-of-account",
"transactionDate": "2024-12-01",
"amount": 4567,
"merchantName": "Whole Foods",
"category": "Food and Drink",
"description": "Groceries"
}
Field Details:
amount: Integer in cents (4567 = $45.67)transactionDate: ISO date string (YYYY-MM-DD)category: String (optional)
Response (200 OK):
{
"success": true,
"transaction": {
"id": "uuid-of-transaction",
"user_id": "uuid-string",
"account_id": "uuid-of-account",
"transaction_date": "2024-12-01",
"amount": 4567,
"merchant_name": "Whole Foods",
"category": "Food and Drink",
"description": "Groceries",
"is_pending": false,
"created_at": "2024-12-04T12:00:00Z"
}
}
Response (400 Bad Request):
{
"error": "Missing required fields"
}
Example Usage:
const createTransaction = async (data: TransactionInput) => {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch('/api/transactions/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({
userId: user.id,
accountId: data.accountId,
transactionDate: data.date,
amount: dollarsToCents(data.amount), // Convert $45.67 to 4567
merchantName: data.merchant,
category: data.category,
description: data.description,
}),
});
return response.json();
};
Update Transaction
Update an existing transaction.
Endpoint: PUT /api/transactions/update
Authentication: User JWT required
Request Body:
{
"transactionId": "uuid-of-transaction",
"userId": "uuid-string",
"amount": 5000,
"merchantName": "Updated Merchant",
"category": "Updated Category",
"description": "Updated description"
}
Response (200 OK):
{
"success": true,
"transaction": {
"id": "uuid-of-transaction",
"amount": 5000,
"merchant_name": "Updated Merchant",
"category": "Updated Category",
"description": "Updated description",
"updated_at": "2024-12-04T12:00:00Z"
}
}
Response (403 Forbidden):
{
"error": "Cannot update Plaid-synced transactions"
}
Note: Only manual transactions (with plaid_transaction_id = null) can be updated.
Delete Transaction
Delete a manual transaction.
Endpoint: DELETE /api/transactions/delete
Authentication: User JWT required
Request Body:
{
"transactionId": "uuid-of-transaction",
"userId": "uuid-string"
}
Response (200 OK):
{
"success": true
}
Response (403 Forbidden):
{
"error": "Cannot delete Plaid-synced transactions"
}
Response (404 Not Found):
{
"error": "Transaction not found"
}
Admin Auth APIs
Create First Admin
Create the initial admin user (only works if no admins exist).
Endpoint: POST /api/admin/auth/create-first-admin
Authentication: None (public, but only works once)
Request Body:
{
"email": "admin@example.com",
"password": "strong-password-here",
"fullName": "Admin User"
}
Response (200 OK):
{
"success": true,
"admin": {
"id": "uuid-string",
"email": "admin@example.com",
"full_name": "Admin User",
"created_at": "2024-12-04T12:00:00Z"
}
}
Response (400 Bad Request):
{
"error": "Admin user already exists"
}
Security Note: This endpoint is disabled after the first admin is created.
Admin Login
Authenticate admin user (step 1 of 2FA).
Endpoint: POST /api/admin/auth/login
Authentication: None
Request Body:
{
"email": "admin@example.com",
"password": "password123"
}
Response (200 OK) - No 2FA:
{
"success": true,
"requiresTOTP": false,
"session": {
"id": "session-uuid",
"token": "64-char-hex-token",
"expires_at": "2024-12-04T20:00:00Z"
},
"user": {
"id": "admin-uuid",
"email": "admin@example.com",
"full_name": "Admin User",
"totp_enabled": false
}
}
Response (200 OK) - Requires 2FA:
{
"requiresTOTP": true,
"userId": "admin-uuid"
}
Response (401 Unauthorized):
{
"error": "Invalid email or password"
}
Response (423 Locked):
{
"error": "Account is temporarily locked. Please try again later."
}
Verify TOTP
Verify 2FA code and create session (step 2 of 2FA).
Endpoint: POST /api/admin/auth/verify-totp
Authentication: None (requires userId from login step)
Request Body:
{
"userId": "admin-uuid",
"code": "123456"
}
Response (200 OK):
{
"success": true,
"session": {
"id": "session-uuid",
"token": "64-char-hex-token",
"expires_at": "2024-12-04T20:00:00Z"
},
"user": {
"id": "admin-uuid",
"email": "admin@example.com",
"full_name": "Admin User",
"totp_enabled": true,
"totp_verified": true
}
}
Response (401 Unauthorized):
{
"error": "Invalid code"
}
Setup TOTP
Generate TOTP secret and QR code for 2FA setup.
Endpoint: POST /api/admin/auth/setup-totp
Authentication: Admin session required
Request Body:
{
"userId": "admin-uuid"
}
Response (200 OK):
{
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUrl": "otpauth://totp/OneLibro%20Admin:admin@example.com?secret=JBSWY3DPEHPK3PXP&issuer=OneLibro%20Admin"
}
Usage: Display QR code to user to scan with authenticator app.
Verify TOTP Setup
Verify first-time TOTP setup with code from authenticator app.
Endpoint: POST /api/admin/auth/verify-totp-setup
Authentication: Admin session required
Request Body:
{
"userId": "admin-uuid",
"code": "123456"
}
Response (200 OK):
{
"success": true
}
Response (400 Bad Request):
{
"error": "Invalid code. Please try again."
}
Verify Session
Check if admin session is still valid.
Endpoint: POST /api/admin/auth/verify-session
Authentication: Admin session token required
Request Body:
{
"token": "session-token-string"
}
Response (200 OK):
{
"valid": true,
"user": {
"id": "admin-uuid",
"email": "admin@example.com",
"full_name": "Admin User"
}
}
Response (401 Unauthorized):
{
"valid": false,
"error": "Session expired"
}
Logout
Invalidate admin session.
Endpoint: POST /api/admin/auth/logout
Authentication: Admin session token required
Request Body:
{
"token": "session-token-string"
}
Response (200 OK):
{
"success": true
}
Admin Management APIs
Get Users
Retrieve list of all users.
Endpoint: GET /api/admin/get-users
Authentication: Admin session required
Query Parameters:
limit(optional): Number of results (default: 50)offset(optional): Pagination offset (default: 0)
Response (200 OK):
{
"users": [
{
"id": "user-uuid",
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"invite_code": "WELCOME2024",
"invited_by": "admin-uuid",
"last_login_at": "2024-12-04T12:00:00Z",
"created_at": "2024-12-01T10:00:00Z"
}
],
"total": 125
}
Update User
Update user details or admin status.
Endpoint: PUT /api/admin/users/[id]
Authentication: Admin session required
Request Body:
{
"fullName": "Updated Name",
"isAdmin": true
}
Response (200 OK):
{
"success": true,
"user": {
"id": "user-uuid",
"email": "user@example.com",
"full_name": "Updated Name",
"is_admin": true,
"updated_at": "2024-12-04T12:00:00Z"
}
}
Create Invite Code
Generate a new invite code.
Endpoint: POST /api/admin/invites/create
Authentication: Admin session required
Request Body:
{
"maxUses": 100,
"expiresAt": "2024-12-31T23:59:59Z",
"createdBy": "admin-uuid"
}
Response (200 OK):
{
"success": true,
"invite": {
"id": "invite-uuid",
"code": "ABC123XYZ789",
"max_uses": 100,
"used_count": 0,
"expires_at": "2024-12-31T23:59:59Z",
"is_active": true,
"created_at": "2024-12-04T12:00:00Z"
}
}
Update Invite Code
Update invite code settings.
Endpoint: PUT /api/admin/invites/[id]
Authentication: Admin session required
Request Body:
{
"maxUses": 200,
"isActive": false
}
Response (200 OK):
{
"success": true,
"invite": {
"id": "invite-uuid",
"code": "ABC123XYZ789",
"max_uses": 200,
"is_active": false,
"updated_at": "2024-12-04T12:00:00Z"
}
}
Get Email Campaigns
Retrieve list of all email campaigns.
Endpoint: GET /api/admin/campaigns
Authentication: Admin session required
Query Parameters:
- None
Response (200 OK):
{
"campaigns": [
{
"id": "campaign-uuid",
"name": "Beta Launch Campaign",
"subject": "You're invited to join OneLibro",
"template_key": "invite_code_email",
"target_audience": {
"active_only": true,
"signup_after": "2025-01-01"
},
"status": "sent",
"scheduled_at": null,
"sent_at": "2025-01-20T10:00:00Z",
"total_recipients": 250,
"total_sent": 248,
"total_delivered": 245,
"total_opened": 120,
"total_clicked": 45,
"total_bounced": 3,
"created_by": "admin-uuid",
"created_at": "2025-01-19T15:00:00Z",
"updated_at": "2025-01-20T11:00:00Z",
"created_by_user": {
"id": "admin-uuid",
"email": "admin@example.com",
"full_name": "Admin User"
}
}
]
}
Create Campaign
Create a new email campaign.
Endpoint: POST /api/admin/campaigns
Authentication: Admin session required
Request Body:
{
"token": "admin-session-token",
"name": "January Newsletter",
"subject": "Your January financial insights are here",
"template_key": "newsletter_template",
"target_audience": {
"active_only": true,
"signup_after": "2024-12-01",
"signup_before": "2025-01-31"
},
"scheduled_at": null
}
Field Details:
name: Campaign name (internal use)subject: Email subject linetemplate_key: Foreign key to email_templates tabletarget_audience: JSON object with filter criteriaactive_only: Boolean (filter by is_active status)signup_after: ISO date (users who signed up after this date)signup_before: ISO date (users who signed up before this date)
scheduled_at: ISO timestamp (null = draft, future date = scheduled)
Response (200 OK):
{
"success": true,
"campaign": {
"id": "new-campaign-uuid",
"name": "January Newsletter",
"subject": "Your January financial insights are here",
"template_key": "newsletter_template",
"target_audience": {
"active_only": true,
"signup_after": "2024-12-01",
"signup_before": "2025-01-31"
},
"status": "draft",
"total_recipients": 125,
"created_by": "admin-uuid",
"created_at": "2025-01-24T10:00:00Z"
}
}
Response (400 Bad Request):
{
"error": "Template not found or inactive"
}
Get Campaign Details
Get detailed information about a specific campaign.
Endpoint: GET /api/admin/campaigns/[id]
Authentication: Admin session required
Response (200 OK):
{
"campaign": {
"id": "campaign-uuid",
"name": "January Newsletter",
"subject": "Your January financial insights are here",
"template_key": "newsletter_template",
"target_audience": {
"active_only": true,
"signup_after": "2024-12-01"
},
"status": "sent",
"sent_at": "2025-01-24T12:00:00Z",
"total_recipients": 125,
"total_sent": 123,
"total_delivered": 120,
"total_bounced": 3,
"created_by": "admin-uuid",
"created_at": "2025-01-24T10:00:00Z",
"created_by_user": {
"id": "admin-uuid",
"email": "admin@example.com",
"full_name": "Admin User"
},
"email_template": {
"template_key": "newsletter_template",
"template_name": "Monthly Newsletter",
"category": "marketing"
}
}
}
Response (404 Not Found):
{
"error": "Campaign not found"
}
Update Campaign
Update campaign details (only for draft/scheduled campaigns).
Endpoint: PUT /api/admin/campaigns/[id]
Authentication: Admin session required
Request Body:
{
"token": "admin-session-token",
"name": "Updated Campaign Name",
"subject": "Updated subject line",
"target_audience": {
"active_only": true,
"signup_after": "2024-12-15"
},
"scheduled_at": "2025-01-30T09:00:00Z"
}
Response (200 OK):
{
"success": true,
"campaign": {
"id": "campaign-uuid",
"name": "Updated Campaign Name",
"subject": "Updated subject line",
"status": "scheduled",
"scheduled_at": "2025-01-30T09:00:00Z",
"total_recipients": 98,
"updated_at": "2025-01-24T11:00:00Z"
}
}
Response (400 Bad Request):
{
"error": "Cannot update campaign that is already sent or sending"
}
Note: Only campaigns with status draft or scheduled can be updated.
Delete Campaign
Delete a draft campaign.
Endpoint: DELETE /api/admin/campaigns/[id]
Authentication: Admin session required
Response (200 OK):
{
"success": true
}
Response (400 Bad Request):
{
"error": "Cannot delete campaign that is not in draft status"
}
Note: Only campaigns with status draft can be deleted. Sent campaigns are permanent for audit purposes.
Send Campaign
Send campaign to all target recipients with batch processing.
Endpoint: POST /api/admin/campaigns/[id]/send
Authentication: Admin session required
Request Body (Production Mode):
{
"token": "admin-session-token",
"test_mode": false
}
Request Body (Test Mode):
{
"token": "admin-session-token",
"test_mode": true,
"test_email": "admin@example.com"
}
Response (200 OK) - Production:
{
"success": true,
"campaign_id": "campaign-uuid",
"total_recipients": 250,
"total_sent": 248,
"total_failed": 2
}
Response (200 OK) - Test:
{
"success": true,
"test_mode": true,
"email_id": "resend-email-id",
"message": "Test email sent to admin@example.com"
}
Response (400 Bad Request):
{
"error": "Campaign is already sent or sending"
}
Implementation Details:
- Batch size: 50 emails per batch
- Delay between batches: 1 second
- Status updates in real-time during sending
- Failed emails are logged but don't stop the campaign
- Campaign status changes:
draft→sending→sent
Example Usage:
// Send test email
const testResponse = await fetch(`/api/admin/campaigns/${campaignId}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
},
body: JSON.stringify({
token: sessionToken,
test_mode: true,
test_email: 'admin@example.com',
}),
});
// Send to all recipients
const sendResponse = await fetch(`/api/admin/campaigns/${campaignId}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
},
body: JSON.stringify({
token: sessionToken,
test_mode: false,
}),
});
Error Handling
Standard Error Response
All errors follow this format:
{
"error": "Error message describing what went wrong"
}
HTTP Status Codes
| Code | Meaning | When Used |
|---|---|---|
| 200 | OK | Request successful |
| 400 | Bad Request | Invalid request body or parameters |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 423 | Locked | Account temporarily locked |
| 500 | Internal Server Error | Server error occurred |
Error Handling Example
const response = await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 401:
// Redirect to login
router.push('/finance/login');
break;
case 400:
// Show validation error
setError(error.error);
break;
case 500:
// Show generic error
setError('Something went wrong. Please try again.');
break;
}
return;
}
const data = await response.json();
// Handle success
Next Steps:
- Review Helper Libraries for helper functions
- Check Authentication for auth implementation
- Read Security for API security best practices