---
title: "Issue Po Invoice"
method: POST
path: "/api/v1/admin/payments/{payment_id}/issue-invoice"
tags: ["admin-payments"]
---

# Issue Po Invoice

`POST /api/v1/admin/payments/{payment_id}/issue-invoice`

Issue a pre-payment invoice for a Purchase Order.

When a customer pays by PO, the business needs to send them an
invoice BEFORE payment arrives so their AP team can cut the check.
This endpoint generates the invoice (status=ISSUED) without touching
the payment's status or the project's state. The customer will see
the invoice in their dashboard and can forward to AP. When payment
later arrives, ``mark-received`` flips the same invoice to PAID.

``exclude_tax=true`` renders the invoice tax-free (e.g. tax-exempt org,
or tax accidentally added to the quote) without touching the quote.

Optional ``additional_line_items_post_tax`` (request body) adds admin-entered
post-tax extras (shipping, …) to the invoice — billed beyond the quote,
summed into the total after tax, quote untouched.

``acknowledge_total_drift=true`` bypasses the reconciliation gate below.
Use it when the recorded settlement genuinely differs from the quote (the
normal case for a MANUAL out-of-band payment); leave it false so a stale
``quote.total_amount`` can never silently become a bill.

Gates: PO method, payment PENDING, PO number submitted, no invoice
already issued, recorded amount reconciles with the quote (400
``quote_total_drift`` otherwise). Requires ``payments:edit`` permission.

## Path parameters

- `payment_id` string, uuid, required

## Query parameters

- `exclude_tax` boolean
- `acknowledge_total_drift` boolean

## Request body

- InvoiceLineItemsBody — Optional request body for issue / regenerate-invoice endpoints carrying admin-entered post-tax line items (shipping, expedite, …). ``additional_line_items_post_tax`` semantics on **regenerate**: ``None`` (field omitted) leaves the invoice's stored items unchanged; ``[]`` clears them; a non-empty list replaces them. On **issue**, ``None``/omitted = no extras.
  - `additional_line_items_post_tax` AdditionalLineItemInput[], nullable
    - `label` string, required
    - `quantity` integer, nullable — Optional display quantity for admin-entered misc line items. When set, the PDF shows the quantity alongside the label. The amount remains the line amount, not a unit price.
    - `amount` union, required
      - number
      - string

## Response `200`

Successful Response

- AdminPaymentDetailResponse — Full payment detail for admin view.
  - `id` string, uuid, required
  - `project_id` string, uuid, required
  - `quote_id` string, uuid, required
  - `user_id` string, uuid, required
  - `method` string, required
  - `status` string, required
  - `amount` string, required
  - `total_with_tax` string, nullable
  - `deposit_amount` string, nullable
  - `currency` string, required
  - `stripe_payment_intent_id` string, nullable
  - `po_number` string, nullable
  - `po_document_r2_key` string, nullable
  - `po_document_download_url` string, nullable
  - `shipping_address` string, nullable
  - `notes` string, nullable
  - `paid_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `user` UserSummary, required — Minimal user info for denormalized list views.
    - `id` string, uuid, required
    - `email` string, required
    - `first_name` string, required
    - `last_name` string, required
    - `company` string, nullable
  - `project_name` string, required
  - `invoice` InvoiceResponse — Invoice summary returned in payment detail views. ``invoice_number`` is the sequential accounting identifier (``INV-YYYY-NNNNN``) and stays stable in the DB for accounting reconciliation. ``display_number`` is the project-derived short form (``INV-{project_id_first8}``) shown in the UI and on PDFs so quote + invoice + project share a visually-consistent identifier. Backend resolves ``display_number`` at response build time — frontends should prefer it for display and only surface ``invoice_number`` if accounting context is needed.
    - `id` string, uuid, required
    - `payment_id` string, uuid, required
    - `project_id` string, uuid, required
    - `invoice_number` string, required
    - `display_number` string, nullable
    - `status` string, required
    - `amount` string, required
    - `subtotal` string, nullable
    - `tax_amount` string, nullable
    - `tax_rate` string, nullable
    - `tax_excluded` boolean
    - `additional_line_items_post_tax` AdditionalLineItemOutput[], nullable
      - `label` string, required
      - `quantity` integer, nullable — Optional display quantity for admin-entered misc line items. When set, the PDF shows the quantity alongside the label. The amount remains the line amount, not a unit price.
      - `amount` string, required
    - `currency` string, required
    - `issued_at` string, date-time, nullable
    - `paid_at` string, date-time, nullable
    - `invoice_pdf_url` string, nullable
    - `pdf_generated_at` string, date-time, nullable
    - `notes` string, nullable
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
  - `packing_slip` PackingSlipResponse — Packing slip summary returned in admin payment detail + standalone fetches. ``packing_slip_pdf_url`` is populated by the router via the storage chokepoint (5-minute presigned token, ``disposition=attachment``) when ``packing_slip_pdf_r2_key`` is present. Frontend uses it directly with ``window.open``.
    - `id` string, uuid, required
    - `project_id` string, uuid, required
    - `shipment_id` string, uuid, nullable
    - `packing_slip_number` string, required
    - `status` string, required
    - `packing_slip_pdf_url` string, nullable
    - `total_weight` string, nullable
    - `total_weight_unit` string, nullable
    - `package_count` integer, nullable
    - `package_snapshot` object[], nullable
    - `ship_date_override` string, date, nullable
    - `generated_at` string, date-time, nullable
    - `notes` string, nullable
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
  - `customer_name` string, required
  - `customer_email` string, required
  - `customer_billing_address` string, nullable
  - `submitted_by_admin_id` string, uuid, nullable
  - `submitted_by_label` string, nullable
  - `payment_terms_net_days` integer, nullable
  - `po_document_extraction` CommercialDocumentExtractionSummary — Compact extraction status for list/detail views.
    - `id` string, uuid, required
    - `status` string, required
    - `provider` string, required
    - `schema_version` string, required
    - `confidence` number, nullable
    - `aggregate_total_cents` integer, nullable
    - `currency` string, nullable
    - `max_lead_time_days` integer, nullable
    - `error_message` string, nullable
    - `started_at` string, date-time, nullable
    - `completed_at` string, date-time, nullable
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
  - `po_document_validation` POExtractionValidationSummary — Admin-only PO extraction validation summary.
    - `extraction_id` string, uuid, required
    - `amount` POAmountValidation, required — Comparison between the extracted PO total and the sell-side order total.
      - `status` 'match' | 'mismatch' | 'matches_pre_tax_only' | 'unknown', required — Canonical PO extraction validation statuses for admin review.
      - `extracted_total_cents` integer, nullable
      - `expected_total_cents` integer, nullable
      - `expected_pre_tax_cents` integer, nullable
      - `delta_cents` integer, nullable
      - `currency` string, nullable
      - `message` string, required
    - `shipping_address` POAddressValidation — Comparison between extracted PO address text and current order address.
      - `status` 'match' | 'mismatch' | 'matches_pre_tax_only' | 'unknown', required — Canonical PO extraction validation statuses for admin review.
      - `field` string, required
      - `extracted_text` string, nullable
      - `current_text` string, nullable
      - `suggested_override` string, nullable
      - `message` string, required
    - `billing_address` POAddressValidation — Comparison between extracted PO address text and current order address.
      - `status` 'match' | 'mismatch' | 'matches_pre_tax_only' | 'unknown', required — Canonical PO extraction validation statuses for admin review.
      - `field` string, required
      - `extracted_text` string, nullable
      - `current_text` string, nullable
      - `suggested_override` string, nullable
      - `message` string, required
    - `extracted_po_number` string, nullable
    - `extracted_order_date` string, date, nullable
    - `extracted_payment_terms_net_days` integer, nullable
    - `extracted_requester_name` string, nullable
    - `extracted_requester_email` string, nullable
  - `order_placed_at_override` string, date-time, nullable
  - `order_placed_at` string, date-time, nullable

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/prototyping/apis/prototyping-io-api.md) · [All operations](https://skmtc.dev/prototyping/apis/prototyping-io-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/prototyping/prototyping-io-api/revisions/f4a0079fbb57/schema)
