Skip to content

Invoice Lifecycle

A complete guide to the invoice state machine — from draft to paid, with every transition explained.

Status Overview

Every managed invoice has a status field that controls what actions are available.

draft --finalize--> finalized --mark-sent--> sent --mark-paid--> paid
                        |    \                  ^  \               |
                        |     \--- mark-paid ---|---\----> paid    |
                        |                       |                  |
                        |                       +--- mark-unpaid --+
                        |
       void:     finalized, sent            --void-->    void
       archive:  finalized, sent, paid, void --archive--> archived --restore--> finalized
       delete:   draft only                  --DELETE-->  (removed)

Note: mark-paid is allowed from both finalized and sent. mark-unpaid moves paid back to sent. void is only allowed from finalized or sent.

Statuses

Status Description Editable?
draft Invoice is being prepared. Fully editable. Yes
finalized Locked and ready to send. Calculations are frozen. No
sent Delivered to the customer (via email or manually marked). No
paid Payment received. No
void Cancelled. Cannot be un-voided. No
archived Hidden from default views. Can be restored. No

Transitions

Draft to Finalized

POST /api/v1/documents/{id}/finalize

Finalizing an invoice:

  • Locks all fields (line items, discounts, taxes, etc.)
  • Freezes the calculation — totals won't change
  • Makes the invoice available for rendering and sending

Irreversible

Once finalized, an invoice cannot be returned to draft. If you need to make changes, void the invoice and create a new one.

When to finalize: After all line items, discounts, taxes, and details have been reviewed and confirmed.

Finalized to Sent

Only POST .../mark-sent moves the status to sent. It is allowed only from finalized.

POST /api/v1/documents/{id}/mark-sent

Use this to record that you delivered the invoice — whether InvoicePDFs emailed it or you delivered it through your own channels (printed, uploaded to a portal, etc.).

send does not change the status

POST /api/v1/documents/{id}/send
{
  "to": ["client@example.com"],
  "subject": "Invoice INV-2026-001",
  "message": "Please find your invoice attached.",
  "attach_pdf": true
}

The send endpoint emails the document and creates a delivery record, but it does not transition the document's status. It works from any non-draft status (finalized, sent, or paid) and returns 409 Conflict on a draft. If attach_pdf is true, a render must already exist (render the document first). To move the status to sent, call mark-sent explicitly.

Marking as Paid

POST /api/v1/documents/{id}/mark-paid

Mark an invoice as paid when you've received payment. This is a manual status update — InvoicePDFs does not process payments. mark-paid is allowed from finalized or sent (you do not have to mark it sent first).

POST /api/v1/documents/{id}/mark-unpaid

If a payment bounces or is reversed, you can move the invoice back to sent status.

Voiding an Invoice

POST /api/v1/documents/{id}/void

Voiding permanently cancels an invoice. You can void an invoice that is finalized or sent. A paid invoice cannot be voided.

Use cases:

  • Invoice was sent in error
  • Customer dispute — issuing a corrected invoice
  • Duplicate invoice

Note

Voided invoices remain in the system for audit purposes. They cannot be deleted or un-voided.

Archiving and Restoring

POST /api/v1/documents/{id}/archive
POST /api/v1/documents/{id}/restore

Archiving hides an invoice from default list views. Archiving is allowed from finalized, sent, paid, or void (a draft cannot be archived — delete it instead).

Restoring brings it back to finalized status, regardless of what status it was in before archiving.

Allowed Transitions Table

From To Endpoint
draft finalized POST .../finalize
draft (deleted) DELETE .../
finalized sent POST .../mark-sent
finalized paid POST .../mark-paid
finalized void POST .../void
finalized archived POST .../archive
sent paid POST .../mark-paid
sent void POST .../void
sent archived POST .../archive
paid sent POST .../mark-unpaid
paid archived POST .../archive
void archived POST .../archive
archived finalized POST .../restore

POST .../send emails the document and creates a delivery record but is not a status transition — it does not appear in this table because it never changes the document's status.

Common Workflows

Standard Flow

1. Create draft invoice
2. Add line items, discounts, taxes
3. Finalize
4. Render PDF
5. Send via email
6. Mark as paid when payment received

Quick Render (No Lifecycle)

If you don't need status tracking, use the Documents API for stateless rendering:

1. POST /api/v1/documents/render with all data
2. Download PDF

Correction Flow

1. Void the incorrect invoice
2. Create a new draft with corrected data
3. Reference the voided invoice in notes
4. Finalize and send the new invoice

Batch Processing

import httpx

client = httpx.Client(
    base_url="https://invoicepdfs.com",
    headers={"Authorization": "Bearer ip_live_..."},
)

# Create and finalize invoices in batch
for invoice_data in monthly_invoices:
    resp = client.post("/api/v1/documents", json=invoice_data)
    invoice_id = resp.json()["data"]["id"]

    client.post(f"/api/v1/documents/{invoice_id}/finalize")
    # Note: send emails the PDF but does not change status; call mark-sent to
    # record the document as sent. A render must exist before send with attach_pdf.
    client.post(f"/api/v1/documents/{invoice_id}/renders", json={"template_id": "tpl_modern"})
    client.post(f"/api/v1/documents/{invoice_id}/send", json={
        "to": [invoice_data["customer_email"]],
        "attach_pdf": True,
    })
    client.post(f"/api/v1/documents/{invoice_id}/mark-sent")

Error Handling

Invalid transitions return 409 Conflict:

{
  "error": {
    "status": 409,
    "code": "conflict",
    "message": "Cannot finalize a document in 'sent' status",
    "request_id": "38f24dafcf099a38a8eb287afef6b3c0"
  }
}

Only draft invoices can be edited. Attempting to PATCH a non-draft invoice returns:

{
  "error": {
    "status": 409,
    "code": "conflict",
    "message": "Only draft documents can be edited",
    "request_id": "38f24dafcf099a38a8eb287afef6b3c0"
  }
}