Update Invoice
The Update Invoice endpoint enables you to modify existing invoice information including customer details, line items, payment terms, and status. The endpoint supports both full and partial updates with comprehensive validation to ensure data integrity and business rule compliance. Certain restrictions apply based on invoice status and payment history.
Endpoint DetailsCopied!
-
Method:
PUT
-
URL:
/api/v0/invoices/{id}
-
Content-Type:
application/json
-
Authentication: Required (API Key & Secret)
-
Rate Limiting: 100 requests per minute
-
Idempotency: Supported (recommended for update operations)
Path ParametersCopied!
Parameter |
Type |
Required |
Description |
Example |
---|---|---|---|---|
|
string (UUID) |
Yes |
Unique invoice identifier |
|
Request ParametersCopied!
Updatable Fields
Field |
Type |
Description |
Business Rules |
---|---|---|---|
|
string |
Invoice name/title |
Can be updated until invoice is paid |
|
string |
Customer email address |
Valid email format required |
|
string (UUID) |
Customer identifier |
Must be valid existing customer |
|
string (UUID) |
Wallet for payments |
Must belong to your application |
|
array |
Invoice line items |
Cannot be empty, products must exist |
|
string (date) |
Payment due date (YYYY-MM-DD) |
Cannot be in the past |
|
string (date) |
Invoice send date (YYYY-MM-DD) |
Cannot be after due date |
|
enum |
Delivery method |
|
|
boolean |
Generate payment link |
Can be enabled/disabled |
|
array |
Accepted payment methods |
At least one method required |
|
enum |
Invoice status |
See status transition rules |
|
boolean |
Allow partial payments |
Can be changed until first payment |
|
string |
Customer address |
Optional field |
|
string |
Customer phone |
Optional field |
|
string (URL) |
Company logo URL |
Must be valid URL |
|
string (UUID) |
Tax configuration |
Must be valid tax ID |
Status Transition Rules
From Status |
Allowed Transitions |
Restrictions |
---|---|---|
|
|
Full edit capability |
|
|
Limited edits after sending |
|
|
Cannot modify amounts |
|
None |
Read-only except for metadata |
|
|
Limited edits |
Update Restrictions
Invoice Status |
Allowed Updates |
Prohibited Updates |
---|---|---|
DRAFT |
All fields |
None |
OPEN |
Customer info, dates, payment methods |
Line items (with restrictions) |
PARTIALLYPAID |
Due date, payment methods |
Items, amounts, customer |
PAID |
Metadata only |
All core fields |
PASTDUE |
Due date, payment methods |
Items, amounts |
Request ExamplesCopied!
Basic Invoice Update
curl -X PUT "https://api.devdraft.com/api/v0/invoices/inv_550e8400-e29b-41d4-a716-446655440000" \
-H "x-client-key: YOUR_CLIENT_KEY" \
-H "x-client-secret: YOUR_CLIENT_SECRET" \
-H "x-idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Website Development Services",
"due_date": "2024-03-15",
"payment_methods": ["CRYPTO", "BANK_TRANSFER", "CREDIT_CARD"],
"partial_payment": true
}'
Status Update (Draft to Open)
curl -X PUT "https://api.devdraft.com/api/v0/invoices/inv_550e8400-e29b-41d4-a716-446655440000" \
-H "x-client-key: YOUR_CLIENT_KEY" \
-H "x-client-secret: YOUR_CLIENT_SECRET" \
-H "x-idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"status": "OPEN",
"send_date": "2024-01-20"
}'
Add Items to Existing Invoice
curl -X PUT "https://api.devdraft.com/api/v0/invoices/inv_550e8400-e29b-41d4-a716-446655440000" \
-H "x-client-key: YOUR_CLIENT_KEY" \
-H "x-client-secret: YOUR_CLIENT_SECRET" \
-H "x-idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"product_id": "prod_existing_001",
"quantity": 2
},
{
"product_id": "prod_new_002",
"quantity": 1
}
]
}'
Customer Information Update
curl -X PUT "https://api.devdraft.com/api/v0/invoices/inv_550e8400-e29b-41d4-a716-446655440000" \
-H "x-client-key: YOUR_CLIENT_KEY" \
-H "x-client-secret: YOUR_CLIENT_SECRET" \
-H "x-idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "updated@clientcompany.com",
"address": "456 New Business Ave, Updated City, UC 54321",
"phone_number": "+1-555-999-8888"
}'
JavaScript/Node.js Examples
// Basic invoice update function
const updateInvoice = async (invoiceId, updates) => {
try {
const response = await fetch(`https://api.devdraft.com/api/v0/invoices/${invoiceId}`, {
method: 'PUT',
headers: {
'x-client-key': 'YOUR_CLIENT_KEY',
'x-client-secret': 'YOUR_CLIENT_SECRET',
'x-idempotency-key': generateUUID(),
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const updatedInvoice = await response.json();
return updatedInvoice;
} catch (error) {
console.error('Error updating invoice:', error);
throw error;
}
};
// Smart invoice updater with validation
class InvoiceUpdater {
constructor(apiClient) {
this.apiClient = apiClient;
}
async updateInvoice(invoiceId, updates, options = {}) {
const { validateBeforeUpdate = true, forceUpdate = false } = options;
try {
// Fetch current invoice for validation
if (validateBeforeUpdate) {
const currentInvoice = await this.apiClient.fetchInvoice(invoiceId);
this.validateUpdate(currentInvoice, updates, forceUpdate);
}
return await updateInvoice(invoiceId, updates);
} catch (error) {
console.error('Invoice update failed:', error);
throw error;
}
}
validateUpdate(currentInvoice, updates, forceUpdate) {
const errors = [];
// Status-based validation
if (!this.canUpdateInvoice(currentInvoice.status) && !forceUpdate) {
errors.push(`Cannot update invoice with status: ${currentInvoice.status}`);
}
// Date validation
if (updates.due_date) {
const dueDate = new Date(updates.due_date);
if (dueDate < new Date()) {
errors.push('Due date cannot be in the past');
}
}
if (updates.send_date && updates.due_date) {
const sendDate = new Date(updates.send_date);
const dueDate = new Date(updates.due_date);
if (sendDate > dueDate) {
errors.push('Send date cannot be after due date');
}
}
// Items validation for non-draft invoices
if (updates.items && currentInvoice.status !== 'DRAFT' && !forceUpdate) {
if (currentInvoice.transactions && currentInvoice.transactions.length > 0) {
errors.push('Cannot modify items on invoice with existing transactions');
}
}
// Email validation
if (updates.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(updates.email)) {
errors.push('Invalid email format');
}
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
}
canUpdateInvoice(status) {
const readOnlyStatuses = ['PAID'];
return !readOnlyStatuses.includes(status);
}
// Specialized update methods
async updateStatus(invoiceId, newStatus, additionalData = {}) {
const allowedTransitions = {
'DRAFT': ['OPEN', 'DRAFT'],
'OPEN': ['PAID', 'PASTDUE', 'PARTIALLYPAID', 'DRAFT'],
'PARTIALLYPAID': ['PAID', 'PASTDUE'],
'PASTDUE': ['PAID', 'PARTIALLYPAID', 'OPEN'],
'PAID': []
};
const currentInvoice = await this.apiClient.fetchInvoice(invoiceId);
if (!allowedTransitions[currentInvoice.status]?.includes(newStatus)) {
throw new Error(`Invalid status transition from ${currentInvoice.status} to ${newStatus}`);
}
const updates = { status: newStatus, ...additionalData };
// Add send_date when moving to OPEN
if (newStatus === 'OPEN' && !additionalData.send_date) {
updates.send_date = new Date().toISOString().slice(0, 10);
}
return await this.updateInvoice(invoiceId, updates);
}
async extendDueDate(invoiceId, newDueDate, reason = '') {
const updates = {
due_date: newDueDate,
// Add audit note if your system supports it
notes: `Due date extended${reason ? ': ' + reason : ''}`
};
return await this.updateInvoice(invoiceId, updates);
}
async addPaymentMethod(invoiceId, paymentMethod) {
const currentInvoice = await this.apiClient.fetchInvoice(invoiceId);
const paymentMethods = [...currentInvoice.payment_methods];
if (!paymentMethods.includes(paymentMethod)) {
paymentMethods.push(paymentMethod);
return await this.updateInvoice(invoiceId, { payment_methods: paymentMethods });
}
return currentInvoice; // No change needed
}
async updateCustomerContact(invoiceId, contactInfo) {
const allowedFields = ['email', 'address', 'phone_number'];
const updates = {};
Object.keys(contactInfo).forEach(key => {
if (allowedFields.includes(key)) {
updates[key] = contactInfo[key];
}
});
if (Object.keys(updates).length === 0) {
throw new Error('No valid contact fields provided');
}
return await this.updateInvoice(invoiceId, updates);
}
}
// Usage examples
const updater = new InvoiceUpdater(apiClient);
try {
// Basic update
const updated = await updater.updateInvoice('inv_123', {
name: 'Updated Invoice Name',
due_date: '2024-03-01'
});
// Status change
const opened = await updater.updateStatus('inv_123', 'OPEN');
// Extend due date
const extended = await updater.extendDueDate('inv_123', '2024-04-01', 'Client request');
// Update contact info
const contactUpdated = await updater.updateCustomerContact('inv_123', {
email: 'new@email.com',
phone_number: '+1-555-123-4567'
});
console.log('All updates completed successfully');
} catch (error) {
console.error('Update failed:', error.message);
}
React Component Example
import React, { useState, useEffect } from 'react';
const InvoiceUpdateForm = ({ invoiceId, onUpdate, onCancel }) => {
const [invoice, setInvoice] = useState(null);
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState({});
useEffect(() => {
loadInvoice();
}, [invoiceId]);
const loadInvoice = async () => {
try {
const invoiceData = await fetchInvoice(invoiceId);
setInvoice(invoiceData);
setFormData({
name: invoiceData.name,
email: invoiceData.email,
due_date: invoiceData.due_date.slice(0, 10),
address: invoiceData.address || '',
phone_number: invoiceData.phone_number || '',
payment_methods: invoiceData.payment_methods,
partial_payment: invoiceData.partial_payment,
payment_link: invoiceData.payment_link
});
} catch (error) {
console.error('Failed to load invoice:', error);
} finally {
setLoading(false);
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.name?.trim()) {
newErrors.name = 'Invoice name is required';
}
if (!formData.email?.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
if (!formData.due_date) {
newErrors.due_date = 'Due date is required';
} else if (new Date(formData.due_date) < new Date()) {
newErrors.due_date = 'Due date cannot be in the past';
}
if (!formData.payment_methods || formData.payment_methods.length === 0) {
newErrors.payment_methods = 'At least one payment method is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setSaving(true);
try {
const updatedInvoice = await updateInvoice(invoiceId, formData);
onUpdate(updatedInvoice);
} catch (error) {
console.error('Update failed:', error);
setErrors({ submit: error.message });
} finally {
setSaving(false);
}
};
const handleFieldChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear field error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const canUpdateField = (field) => {
if (!invoice) return false;
const restrictedFields = {
'PAID': ['name', 'email', 'due_date', 'payment_methods', 'items'],
'PARTIALLYPAID': ['items'],
};
return !restrictedFields[invoice.status]?.includes(field);
};
if (loading) {
return <div className="loading">Loading invoice...</div>;
}
return (
<form onSubmit={handleSubmit} className="invoice-update-form">
<h2>Update Invoice {invoice?.invoice_number}</h2>
<div className="form-grid">
<div className="form-group">
<label>Invoice Name</label>
<input
type="text"
value={formData.name || ''}
onChange={(e) => handleFieldChange('name', e.target.value)}
disabled={!canUpdateField('name')}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-text">{errors.name}</span>}
</div>
<div className="form-group">
<label>Customer Email</label>
<input
type="email"
value={formData.email || ''}
onChange={(e) => handleFieldChange('email', e.target.value)}
disabled={!canUpdateField('email')}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-text">{errors.email}</span>}
</div>
<div className="form-group">
<label>Due Date</label>
<input
type="date"
value={formData.due_date || ''}
onChange={(e) => handleFieldChange('due_date', e.target.value)}
disabled={!canUpdateField('due_date')}
className={errors.due_date ? 'error' : ''}
/>
{errors.due_date && <span className="error-text">{errors.due_date}</span>}
</div>
<div className="form-group">
<label>Address</label>
<textarea
value={formData.address || ''}
onChange={(e) => handleFieldChange('address', e.target.value)}
rows="3"
/>
</div>
<div className="form-group">
<label>Phone Number</label>
<input
type="tel"
value={formData.phone_number || ''}
onChange={(e) => handleFieldChange('phone_number', e.target.value)}
/>
</div>
<div className="form-group">
<label>Payment Methods</label>
<div className="checkbox-group">
{['CRYPTO', 'BANK_TRANSFER', 'CREDIT_CARD', 'CASH'].map(method => (
<label key={method} className="checkbox-label">
<input
type="checkbox"
checked={formData.payment_methods?.includes(method) || false}
onChange={(e) => {
const methods = formData.payment_methods || [];
if (e.target.checked) {
handleFieldChange('payment_methods', [...methods, method]);
} else {
handleFieldChange('payment_methods', methods.filter(m => m !== method));
}
}}
disabled={!canUpdateField('payment_methods')}
/>
{method.replace('_', ' ')}
</label>
))}
</div>
{errors.payment_methods && <span className="error-text">{errors.payment_methods}</span>}
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={formData.partial_payment || false}
onChange={(e) => handleFieldChange('partial_payment', e.target.checked)}
/>
Allow Partial Payments
</label>
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={formData.payment_link || false}
onChange={(e) => handleFieldChange('payment_link', e.target.checked)}
/>
Generate Payment Link
</label>
</div>
</div>
{errors.submit && (
<div className="error-banner">
{errors.submit}
</div>
)}
<div className="form-actions">
<button type="button" onClick={onCancel} disabled={saving}>
Cancel
</button>
<button type="submit" disabled={saving} className="primary">
{saving ? 'Updating...' : 'Update Invoice'}
</button>
</div>
</form>
);
};
export default InvoiceUpdateForm;
Python Example
import requests
from typing import Dict, Any, Optional, List
from datetime import datetime, date
class InvoiceUpdater:
def __init__(self, client_key: str, client_secret: str, base_url: str = "https://api.devdraft.com"):
self.client_key = client_key
self.client_secret = client_secret
self.base_url = base_url
def _get_headers(self) -> Dict[str, str]:
return {
'x-client-key': self.client_key,
'x-client-secret': self.client_secret,
'x-idempotency-key': str(__import__('uuid').uuid4()),
'Content-Type': 'application/json'
}
def update_invoice(self, invoice_id: str, updates: Dict[str, Any]) -> Dict[str, Any]:
"""Update an invoice with the provided changes"""
url = f"{self.base_url}/api/v0/invoices/{invoice_id}"
try:
response = requests.put(url, headers=self._get_headers(), json=updates)
if response.status_code == 404:
raise ValueError(f"Invoice {invoice_id} not found")
elif response.status_code == 400:
raise ValueError(f"Invalid update data: {response.json().get('message', 'Unknown error')}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"Failed to update invoice: {e}")
def validate_update(self, current_invoice: Dict[str, Any], updates: Dict[str, Any]) -> List[str]:
"""Validate proposed updates against business rules"""
errors = []
# Status-based restrictions
status = current_invoice.get('status')
if status == 'PAID':
restricted_fields = ['name', 'email', 'due_date', 'items', 'payment_methods']
for field in restricted_fields:
if field in updates:
errors.append(f"Cannot update {field} on paid invoice")
# Date validations
if 'due_date' in updates:
try:
due_date = datetime.fromisoformat(updates['due_date'].replace('Z', '+00:00')).date()
if due_date < date.today():
errors.append("Due date cannot be in the past")
except ValueError:
errors.append("Invalid due date format")
if 'send_date' in updates and 'due_date' in updates:
try:
send_date = datetime.fromisoformat(updates['send_date'].replace('Z', '+00:00')).date()
due_date = datetime.fromisoformat(updates['due_date'].replace('Z', '+00:00')).date()
if send_date > due_date:
errors.append("Send date cannot be after due date")
except ValueError:
errors.append("Invalid date format")
# Email validation
if 'email' in updates:
import re
if not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', updates['email']):
errors.append("Invalid email format")
# Items validation
if 'items' in updates:
if not updates['items'] or len(updates['items']) == 0:
errors.append("Invoice must have at least one item")
# Check if invoice has transactions
if current_invoice.get('transactions') and len(current_invoice['transactions']) > 0:
errors.append("Cannot modify items on invoice with existing transactions")
return errors
def safe_update(self, invoice_id: str, updates: Dict[str, Any], fetch_current: bool = True) -> Dict[str, Any]:
"""Safely update an invoice with validation"""
if fetch_current:
# Fetch current invoice for validation
current_invoice = self.fetch_invoice(invoice_id)
errors = self.validate_update(current_invoice, updates)
if errors:
raise ValueError(f"Validation errors: {'; '.join(errors)}")
return self.update_invoice(invoice_id, updates)
def fetch_invoice(self, invoice_id: str) -> Dict[str, Any]:
"""Fetch current invoice data"""
url = f"{self.base_url}/api/v0/invoices/{invoice_id}"
headers = {
'x-client-key': self.client_key,
'x-client-secret': self.client_secret
}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
# Specialized update methods
def update_status(self, invoice_id: str, new_status: str, **kwargs) -> Dict[str, Any]:
"""Update invoice status with validation"""
current_invoice = self.fetch_invoice(invoice_id)
current_status = current_invoice['status']
# Define allowed transitions
allowed_transitions = {
'DRAFT': ['OPEN', 'DRAFT'],
'OPEN': ['PAID', 'PASTDUE', 'PARTIALLYPAID', 'DRAFT'],
'PARTIALLYPAID': ['PAID', 'PASTDUE'],
'PASTDUE': ['PAID', 'PARTIALLYPAID', 'OPEN'],
'PAID': []
}
if new_status not in allowed_transitions.get(current_status, []):
raise ValueError(f"Invalid status transition from {current_status} to {new_status}")
updates = {'status': new_status, **kwargs}
# Automatically set send_date when opening invoice
if new_status == 'OPEN' and 'send_date' not in kwargs:
updates['send_date'] = date.today().isoformat()
return self.update_invoice(invoice_id, updates)
def extend_due_date(self, invoice_id: str, new_due_date: str, reason: str = '') -> Dict[str, Any]:
"""Extend the due date of an invoice"""
updates = {'due_date': new_due_date}
return self.safe_update(invoice_id, updates)
def update_contact_info(self, invoice_id: str, **contact_info) -> Dict[str, Any]:
"""Update customer contact information"""
allowed_fields = ['email', 'address', 'phone_number']
updates = {k: v for k, v in contact_info.items() if k in allowed_fields}
if not updates:
raise ValueError("No valid contact fields provided")
return self.safe_update(invoice_id, updates)
def add_payment_method(self, invoice_id: str, payment_method: str) -> Dict[str, Any]:
"""Add a payment method to an invoice"""
current_invoice = self.fetch_invoice(invoice_id)
payment_methods = current_invoice.get('payment_methods', [])
if payment_method not in payment_methods:
payment_methods.append(payment_method)
return self.update_invoice(invoice_id, {'payment_methods': payment_methods})
return current_invoice # No change needed
# Usage examples
def main():
updater = InvoiceUpdater("YOUR_CLIENT_KEY", "YOUR_CLIENT_SECRET")
try:
# Basic update
updated = updater.safe_update("inv_123", {
"name": "Updated Invoice Name",
"due_date": "2024-03-15"
})
print(f"Updated invoice: {updated['invoice_number']}")
# Status update
opened = updater.update_status("inv_123", "OPEN")
print(f"Invoice status updated to: {opened['status']}")
# Extend due date
extended = updater.extend_due_date("inv_123", "2024-04-01", "Client requested extension")
print(f"Due date extended to: {extended['due_date']}")
# Update contact info
contact_updated = updater.update_contact_info(
"inv_123",
email="new@email.com",
phone_number="+1-555-123-4567"
)
print(f"Contact info updated for: {contact_updated['email']}")
except Exception as e:
print(f"Update failed: {e}")
if __name__ == "__main__":
main()
Response FormatCopied!
Success Response (200 OK)
{
"id": "inv_550e8400-e29b-41d4-a716-446655440000",
"invoice_number": "INV-000001",
"name": "Updated Website Development Services",
"app_id": "app_123456789",
"email": "updated@clientcompany.com",
"address": "456 New Business Ave, Updated City, UC 54321",
"phone_number": "+1-555-999-8888",
"logo": "https://mycompany.com/assets/logo.png",
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"walletId": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
"due_date": "2024-03-15T00:00:00.000Z",
"send_date": "2024-01-15T00:00:00.000Z",
"date_created": "2024-01-15T10:30:00.000Z",
"status": "OPEN",
"payment_methods": ["CRYPTO", "BANK_TRANSFER", "CREDIT_CARD"],
"delivery": "EMAIL",
"payment_link": true,
"partial_payment": true,
"reminder_sent": false,
"paidAt": null,
"paymentMetadata": null,
"paymentStatus": null,
"taxId": "tax_550e8400-e29b-41d4-a716-446655440123",
"currency": "USDC",
"app": {
"id": "app_123456789",
"name": "My Business App"
},
"customer": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "updated@clientcompany.com",
"name": "ABC Corporation",
"status": "ACTIVE"
},
"wallet": {
"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
"address": "0x742d35Cc6635C0532925a3b8d",
"blockchain": "ETHEREUM",
"type": "APP"
},
"tax": {
"id": "tax_550e8400-e29b-41d4-a716-446655440123",
"name": "Standard Sales Tax",
"rate": 8.5,
"type": "PERCENTAGE"
},
"items": [
{
"invoice_id": "inv_550e8400-e29b-41d4-a716-446655440000",
"productId": "prod_123456789",
"quantity": 2,
"product": {
"id": "prod_123456789",
"name": "Website Development",
"price": 2500.00,
"currency": "USD"
}
}
],
"transactions": []
}
Error ResponsesCopied!
Validation Error (400 Bad Request)
{
"statusCode": 400,
"message": [
"Due date cannot be in the past",
"Cannot modify items on invoice with existing transactions",
"Invalid email format"
],
"error": "Bad Request"
}
Invoice Not Found (404 Not Found)
{
"statusCode": 404,
"message": "Invoice not found",
"error": "Not Found"
}
Invalid Status Transition (400 Bad Request)
{
"statusCode": 400,
"message": "Invalid status transition from PAID to OPEN",
"error": "Bad Request"
}
Business Rule Violation (409 Conflict)
{
"statusCode": 409,
"message": "Cannot update paid invoice",
"error": "Conflict",
"details": {
"currentStatus": "PAID",
"attemptedChanges": ["items", "due_date"],
"allowedChanges": ["metadata"]
}
}
Customer Not Found (400 Bad Request)
{
"statusCode": 400,
"message": "Customer with ID '550e8400-invalid' not found",
"error": "Bad Request"
}
Authentication Error (401 Unauthorized)
{
"statusCode": 401,
"message": "Invalid or missing API credentials",
"error": "Unauthorized"
}
Rate Limit Error (429 Too Many Requests)
{
"statusCode": 429,
"message": "Rate limit exceeded. Maximum 100 requests per minute.",
"error": "Too Many Requests",
"retryAfter": 60
}
Business Logic & ValidationCopied!
Status-Based Restrictions
Status |
Editable Fields |
Read-Only Fields |
Notes |
---|---|---|---|
DRAFT |
All fields |
None |
Full editing capability |
OPEN |
Customer info, dates, payment settings |
Invoice number |
Limited item changes |
PARTIALLYPAID |
Due date, payment methods |
Items, amounts |
Cannot change billing |
PAID |
Metadata only |
All core fields |
Minimal changes allowed |
PASTDUE |
Due date, payment methods |
Items, amounts |
Focus on collection |
Date Validation Rules
-
Due Date: Cannot be in the past
-
Send Date: Cannot be after due date
-
Creation Date: Immutable after creation
-
Payment Date: Set automatically when payment received
Item Management Rules
-
Draft Invoices: Full item editing allowed
-
Open Invoices: Items can be modified if no payments received
-
Paid Invoices: Items are locked
-
Partial Payments: Items cannot be changed after first payment
Customer Update Rules
-
Email Changes: Allowed but triggers notification
-
Address Updates: Always permitted for billing accuracy
-
Customer Switching: Not allowed once invoice is sent
Advanced Use CasesCopied!
Bulk Invoice Updates
// Bulk update multiple invoices
const bulkUpdateInvoices = async (updates, options = {}) => {
const { batchSize = 10, validateAll = true, continueOnError = false } = options;
const results = [];
// Validate all updates first if requested
if (validateAll) {
for (const { invoiceId, changes } of updates) {
try {
const invoice = await fetchInvoice(invoiceId);
validateUpdate(invoice, changes);
} catch (error) {
if (!continueOnError) {
throw new Error(`Validation failed for invoice ${invoiceId}: ${error.message}`);
}
results.push({ invoiceId, success: false, error: error.message });
}
}
}
// Process updates in batches
for (let i = 0; i < updates.length; i += batchSize) {
const batch = updates.slice(i, i + batchSize);
const batchPromises = batch.map(async ({ invoiceId, changes }) => {
try {
const updated = await updateInvoice(invoiceId, changes);
return { invoiceId, success: true, invoice: updated };
} catch (error) {
return { invoiceId, success: false, error: error.message };
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Rate limiting delay
if (i + batchSize < updates.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return {
total: updates.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results
};
};
// Usage
const updateResults = await bulkUpdateInvoices([
{ invoiceId: 'inv_001', changes: { due_date: '2024-03-01' } },
{ invoiceId: 'inv_002', changes: { payment_methods: ['CRYPTO'] } },
{ invoiceId: 'inv_003', changes: { partial_payment: true } }
], { continueOnError: true });
console.log(`Updated ${updateResults.successful} of ${updateResults.total} invoices`);
Invoice Workflow Management
// Invoice workflow state machine
class InvoiceWorkflow {
constructor(apiClient) {
this.apiClient = apiClient;
this.workflows = {
'draft_to_open': this.draftToOpen.bind(this),
'open_to_paid': this.openToPaid.bind(this),
'mark_overdue': this.markOverdue.bind(this),
'extend_terms': this.extendTerms.bind(this)
};
}
async executeWorkflow(invoiceId, workflowName, params = {}) {
const workflow = this.workflows[workflowName];
if (!workflow) {
throw new Error(`Unknown workflow: ${workflowName}`);
}
const invoice = await this.apiClient.fetchInvoice(invoiceId);
return await workflow(invoice, params);
}
async draftToOpen(invoice, params) {
if (invoice.status !== 'DRAFT') {
throw new Error('Invoice must be in DRAFT status');
}
const updates = {
status: 'OPEN',
send_date: params.sendDate || new Date().toISOString().slice(0, 10)
};
// Validate invoice is complete
this.validateInvoiceCompleteness(invoice);
const updated = await this.apiClient.updateInvoice(invoice.id, updates);
// Trigger email sending if delivery is EMAIL
if (invoice.delivery === 'EMAIL') {
await this.sendInvoiceEmail(updated);
}
return updated;
}
async openToPaid(invoice, params) {
if (!['OPEN', 'PARTIALLYPAID', 'PASTDUE'].includes(invoice.status)) {
throw new Error('Invoice must be OPEN, PARTIALLYPAID, or PASTDUE');
}
const updates = {
status: 'PAID',
paidAt: params.paidAt || new Date().toISOString(),
paymentMetadata: params.paymentMetadata || {}
};
const updated = await this.apiClient.updateInvoice(invoice.id, updates);
// Trigger fulfillment processes
await this.triggerFulfillment(updated);
return updated;
}
async markOverdue(invoice, params) {
if (invoice.status !== 'OPEN') {
throw new Error('Only OPEN invoices can be marked overdue');
}
const dueDate = new Date(invoice.due_date);
const today = new Date();
if (dueDate >= today) {
throw new Error('Invoice is not yet overdue');
}
const updates = { status: 'PASTDUE' };
const updated = await this.apiClient.updateInvoice(invoice.id, updates);
// Send overdue notification
await this.sendOverdueNotification(updated);
return updated;
}
async extendTerms(invoice, params) {
const { newDueDate, reason, notifyCustomer = true } = params;
if (!newDueDate) {
throw new Error('New due date is required');
}
const updates = { due_date: newDueDate };
// If overdue, change status back to OPEN
if (invoice.status === 'PASTDUE') {
updates.status = 'OPEN';
}
const updated = await this.apiClient.updateInvoice(invoice.id, updates);
// Notify customer of extension
if (notifyCustomer) {
await this.sendExtensionNotification(updated, reason);
}
return updated;
}
validateInvoiceCompleteness(invoice) {
const required = ['name', 'email', 'items', 'due_date', 'payment_methods'];
const missing = required.filter(field => !invoice[field] ||
(Array.isArray(invoice[field]) && invoice[field].length === 0));
if (missing.length > 0) {
throw new Error(`Invoice missing required fields: ${missing.join(', ')}`);
}
}
async sendInvoiceEmail(invoice) {
// Implementation would send invoice via email
console.log(`Sending invoice ${invoice.invoice_number} to ${invoice.email}`);
}
async triggerFulfillment(invoice) {
// Implementation would trigger fulfillment processes
console.log(`Triggering fulfillment for paid invoice ${invoice.invoice_number}`);
}
async sendOverdueNotification(invoice) {
// Implementation would send overdue notification
console.log(`Sending overdue notification for ${invoice.invoice_number}`);
}
async sendExtensionNotification(invoice, reason) {
// Implementation would notify customer of extension
console.log(`Notifying customer of due date extension for ${invoice.invoice_number}`);
}
}
Automated Invoice Management
// Automated invoice management system
class AutomatedInvoiceManager {
constructor(apiClient) {
this.apiClient = apiClient;
this.rules = new Map();
}
addRule(name, condition, action) {
this.rules.set(name, { condition, action });
}
async processInvoice(invoiceId) {
const invoice = await this.apiClient.fetchInvoice(invoiceId);
const results = [];
for (const [ruleName, { condition, action }] of this.rules) {
try {
if (await condition(invoice)) {
const result = await action(invoice);
results.push({ rule: ruleName, success: true, result });
}
} catch (error) {
results.push({ rule: ruleName, success: false, error: error.message });
}
}
return results;
}
async processAllInvoices() {
const invoices = await this.apiClient.getAllInvoices();
const results = [];
for (const invoice of invoices) {
const invoiceResults = await this.processInvoice(invoice.id);
if (invoiceResults.length > 0) {
results.push({ invoiceId: invoice.id, rules: invoiceResults });
}
}
return results;
}
}
// Setup automated rules
const manager = new AutomatedInvoiceManager(apiClient);
// Auto-mark overdue invoices
manager.addRule('mark_overdue',
(invoice) => {
const dueDate = new Date(invoice.due_date);
const today = new Date();
return invoice.status === 'OPEN' && dueDate < today;
},
async (invoice) => {
return await updateInvoice(invoice.id, { status: 'PASTDUE' });
}
);
// Auto-extend due dates for good customers
manager.addRule('extend_for_good_customers',
async (invoice) => {
const customer = await fetchCustomer(invoice.customer_id);
const dueDate = new Date(invoice.due_date);
const today = new Date();
return invoice.status === 'PASTDUE' &&
customer.creditRating >= 'A' &&
(today - dueDate) / (1000 * 60 * 60 * 24) <= 7; // Within