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

id

string (UUID)

Yes

Unique invoice identifier

inv_550e8400-e29b-41d4-a716-446655440000

Request ParametersCopied!

Updatable Fields

Field

Type

Description

Business Rules

name

string

Invoice name/title

Can be updated until invoice is paid

email

string

Customer email address

Valid email format required

customer_id

string (UUID)

Customer identifier

Must be valid existing customer

walletId

string (UUID)

Wallet for payments

Must belong to your application

items

array

Invoice line items

Cannot be empty, products must exist

due_date

string (date)

Payment due date (YYYY-MM-DD)

Cannot be in the past

send_date

string (date)

Invoice send date (YYYY-MM-DD)

Cannot be after due date

delivery

enum

Delivery method

"EMAIL" or "MANUALLY"

payment_link

boolean

Generate payment link

Can be enabled/disabled

payment_methods

array

Accepted payment methods

At least one method required

status

enum

Invoice status

See status transition rules

partial_payment

boolean

Allow partial payments

Can be changed until first payment

address

string

Customer address

Optional field

phone_number

string

Customer phone

Optional field

logo

string (URL)

Company logo URL

Must be valid URL

taxId

string (UUID)

Tax configuration

Must be valid tax ID

Status Transition Rules

From Status

Allowed Transitions

Restrictions

DRAFT

OPEN, DRAFT

Full edit capability

OPEN

PAID, PASTDUE, PARTIALLYPAID, DRAFT

Limited edits after sending

PARTIALLYPAID

PAID, PASTDUE

Cannot modify amounts

PAID

None

Read-only except for metadata

PASTDUE

PAID, PARTIALLYPAID, OPEN

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

  1. Due Date: Cannot be in the past

  2. Send Date: Cannot be after due date

  3. Creation Date: Immutable after creation

  4. Payment Date: Set automatically when payment received

Item Management Rules

  1. Draft Invoices: Full item editing allowed

  2. Open Invoices: Items can be modified if no payments received

  3. Paid Invoices: Items are locked

  4. Partial Payments: Items cannot be changed after first payment

Customer Update Rules

  1. Email Changes: Allowed but triggers notification

  2. Address Updates: Always permitted for billing accuracy

  3. 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