---
title: "Create a proforma"
method: POST
path: "/companies/{company}/proformas"
tags: ["Proformas"]
---

# Create a proforma

`POST /companies/{company}/proformas`

## Request

- Base URL: `https://api.factuarea.com/v1`
- URL: `https://api.factuarea.com/v1/companies/{company}/proformas`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-API-Key`

Create a new proforma invoice in `draft` status. Proformas can later be converted to final invoices.

## Path parameters

- `company` string, uuid, required

## Headers

- `Idempotency-Key` string
- `Factuarea-Version` string, date

## Request body

- CreateProformaRequest
  - `client_id` string, uuid, required
  - `series_id` string, uuid, nullable
  - `price_list_id` string, uuid, nullable
  - `reprice_strategy` 'existing_catalog_lines' | 'future_lines_only' | 'null', nullable
  - `issued_on` string, date, required
  - `valid_until` string, date, nullable
  - `validity_days` integer, nullable
  - `notes` string, nullable
  - `terms_and_conditions` string, nullable
  - `reference` string, nullable
  - `payment_method` string, nullable
  - `payment_terms` integer, nullable
  - `shipping_cost` number, nullable
  - `delivery_terms` string, nullable
  - `estimated_delivery_date` string, date, nullable
  - `external_id` string, nullable
  - `currency` 'EUR' | 'null', nullable
  - `metadata` Metadata, nullable: A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs.
  - `tags` string[], nullable
  - `custom_fields` object[], nullable
    - `field` string, required
    - `value` string, required
  - `lines` object[], required
    - `description` string, nullable
    - `additional_description` string, nullable
    - `quantity` number, required
    - `unit_price` union: Unit price. Send a JSON number or a decimal string with up to 4 decimals (`"0.0125"`, `"100.0000"`): the string is the exact form the response returns, so a line read from the API can be sent back unchanged. Derived amounts are computed from it and keep 2 decimals.
      - number
      - string
    - `tax_rate_id` string, uuid, nullable
    - `tax_rate` number, nullable: VAT rate of the line (0–100). Optional: when you omit it (or send `null`) the line takes, in this order, the tax it references or the tax assigned to its product, and otherwise the DEFAULT TAX of your company for this kind of document (tax settings), inherited as a whole: its rate, its indirect-tax regime (IVA, IGIC or IPSI) and its AEAT qualification; an exempt or not-subject default also declares its cause (`exemption_reason`) on the line unless the line sends its own. The line ends up exactly as if you had chosen that tax. If none of them exists the request is rejected with 422 `missing_required_param`, with `error.param` = `lines.N.tax_rate` and `error.line_index` = N (the zero-based index of the line): the API never guesses a rate. A `0` you send is honoured as a real 0 % rate, never confused with «not stated».
    - `retention_rate` number, nullable
    - `surcharge_rate` number, nullable
    - `retention_rate_id` string, uuid, nullable
    - `surcharge_rate_id` string, uuid, nullable
    - `product_id` string, uuid, nullable
    - `variant_id` string, uuid, nullable
    - `presentation_id` string, uuid, nullable
    - `confirmed_base_quantity` number, nullable
    - `discount_percent` number, nullable
    - `indirect_tax_regime` 'iva' | 'igic' | 'ipsi' | 'null', nullable
    - `configuration_uuid` string, uuid, nullable
    - `counted_quantity` number, nullable
    - `options` object[], nullable
      - `group_uuid` string, uuid, required
      - `value_uuid` string, uuid, required
    - `configuration_signature` string, nullable
    - `configuration_name` string, nullable
    - `price_source` 'price_list' | 'configuration' | 'presentation' | 'variant' | 'product' | 'manual' | 'supplier_offer' | 'pack_snapshot' | 'null', nullable
    - `price_semantics` 'per_base_unit' | 'per_commercial_unit' | 'null', nullable
    - `price_adjustment_total` number, nullable
    - `option_adjustments_absorbed` boolean, nullable

## Response `201`

Proforma created successfully. The `Location` header contains the canonical URL of the newly created resource.

- object
  - `data` Proforma, required: A proforma invoice that can be converted to a final invoice.
    - `id` string, required
    - `object` 'proforma', required
    - `number` string, required
    - `series` SeriesRef, required
      - `id` string, nullable, required
      - `code` string, nullable, required
    - `client` ClientRef, required
      - `id` string, nullable, required
      - `name` string, nullable, required
    - `status` string, required: Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted).
    - `price_list_id` string, nullable, required: UUID of the price list selected for this document.
    - `price_list_name` string, nullable, required: Price-list name snapshot frozen on the document.
    - `issued_on` string, date, required
    - `valid_until` string, date, nullable, required
    - `validity_days` integer, nullable, required: Number of validity days of the proforma since its issuance. `null` if not applicable.
    - `reference` string, nullable, required: Free reference of the document (e.g. the customer order number).
    - `operation_regime` string, required: VAT operation regime of the proforma (`general`, `intracomunitaria`, `importacion_exportacion`, `isp`).
    - `converted_to_id` string, nullable, required: UUID v7 of the invoice this proforma was converted into, if applicable.
    - `converted_invoice_number` string, nullable, required: Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted.
    - `subtotal` string, required
    - `taxes_total` string, required: NET aggregate of the header taxes: `total_vat + total_surcharge − total_retention`. It is the amount that, added to `subtotal`, yields `total` (`total === subtotal + taxes_total`), so it must NOT be combined with `total_retention`: subtracting the withholding again on top of the aggregate produces a false total (4,320.00 + 259.20 − 648.00 = 3,931.20 against a real total of 4,579.20). It is NOT the VAT figure of the Spanish Modelo 303 — read `total_vat` for that. Beware that on an expense the same field name carries a DIFFERENT meaning (VAT only), which is why the identity that holds across all five document families is the explicit one: `total === subtotal + total_vat + total_surcharge − total_retention`.
    - `total_vat` string, required: Sum of the VAT of all lines.
    - `total_retention` string, required: Sum of the withholding (IRPF) of all lines.
    - `total_surcharge` string, required: Sum of the equivalence surcharge of all lines.
    - `total` string, required: Total of the document. Two equivalent ways to reconstruct it from the published amounts, and only these two: the EXPLICIT one, identical in the five document families - `total = subtotal + total_vat + total_surcharge - total_retention` - or the AGGREGATE one, specific to the sales-side families - `total = subtotal + taxes_total`. NEVER reconstruct it as `subtotal + taxes_total - total_retention`: `taxes_total` ALREADY has the withholding netted out, so that combination subtracts it twice and yields a false total (4,320.00 + 259.20 - 648.00 = 3,931.20 against a real 4,579.20).
    - `shipping_cost` string, required: Shipping cost, expressed WITH VAT INCLUDED, added to the amount to pay. It is not part of `subtotal`, `total_vat` or `total`. When the proforma is converted to an invoice it becomes a "Gastos de envío" line whose base is `shipping_cost / (1 + r)` with VAT `r`, so the invoice total equals `total_with_shipping`.
    - `total_with_shipping` string, required: Final total including the shipping cost (= total + shipping_cost). The invoice generated from the proforma has exactly this total.
    - `currency` string, required
    - `payment_method` string, nullable, required: Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`).
    - `payment_terms_days` integer, nullable, required: Payment term in days (Net X). Valid range: 0-365.
    - `delivery_terms` string, nullable, required: Delivery terms, free-form text.
    - `estimated_delivery_date` string, date, nullable, required: Estimated delivery date (YYYY-MM-DD).
    - `notes` string, nullable, required
    - `terms_and_conditions` string, nullable, required: Terms and conditions rendered in the proforma PDF.
    - `external_id` string, nullable, required: External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`.
    - `lines` ProformaLine[], required
      - `object` 'proforma_line', required
      - `description` string, nullable, required
      - `additional_description` string, nullable, required: Optional secondary description rendered below the line concept on document PDFs.
      - `product` ProductRef, required
        - `id` string, required
        - `name` string, nullable, required
      - `quantity` number, required
      - `unit_price` string, required: Unit price as a decimal STRING with the 4-decimal scale of its column (e.g. `"0.0125"`, `"100.0000"`): it is returned exactly as it was accepted and stored, so reading a line and sending it back never changes its price. Derived amounts (`subtotal`, `taxes`, `total`) keep 2 decimals.
      - `tax_rate` number, required
      - `retention_rate` number, required: IRPF withholding percentage applied to the line (0–100). Default 0. Its amount is already aggregated into `taxes`.
      - `surcharge_rate` number, required: Equivalence surcharge (recargo de equivalencia) percentage applied to the line (0–100). Default 0. Its amount is already aggregated into `taxes`.
      - `discount_percent` number, required
      - `subtotal` string, required
      - `taxes` string, required
      - `total` string, required
      - `indirect_tax_regime` 'iva' | 'igic' | 'ipsi' | 'null', nullable: Indirect tax regime of the line: the per-document override (`iva`/`igic`/`ipsi`) when the user set it (precedence override>zone), otherwise `null` (derived from the establishment AEAT zone). Writable per-document input on create/update; the document must be homogeneous (a single non-null regime across all lines, 422 otherwise).
      - `variant` object, nullable, required
        - `id` string, required
        - `name` string, nullable, required
      - `presentation` object, nullable, required
        - `id` string, required
        - `name` string, nullable, required
        - `mode` string, nullable, required
      - `item_kind` string, nullable, required
      - `commercial_unit_code` 'C62' | 'KGM' | 'GRM' | 'LTR' | 'MLT' | 'MTR' | 'MTK' | 'HUR' | 'DAY' | 'H87' | 'XPK' | 'XBX' | 'XDS' | 'XBG' | 'XBO' | 'XDJ' | 'XCA' | 'XTB' | 'XCI' | 'XJY' | 'XEN' | 'XCS' | 'null', nullable, required
      - `base_unit_code` 'C62' | 'KGM' | 'GRM' | 'LTR' | 'MLT' | 'MTR' | 'MTK' | 'HUR' | 'DAY' | 'null', nullable, required
      - `conversion_factor` string, nullable, required
      - `base_quantity` string, nullable, required
      - `price_list_id` string, nullable, required
      - `price_list_name` string, nullable, required
      - `price_source` string, nullable, required
      - `price_unit_code` 'C62' | 'KGM' | 'GRM' | 'LTR' | 'MLT' | 'MTR' | 'MTK' | 'HUR' | 'DAY' | 'H87' | 'XPK' | 'XBX' | 'XDS' | 'XBG' | 'XBO' | 'XDJ' | 'XCA' | 'XTB' | 'XCI' | 'XJY' | 'XEN' | 'XCS' | 'null', nullable, required
      - `counted_quantity` string, nullable, required: Pieces the customer asked for on a variable-weight line sold by count (decimal string, scale 4, greater than 0), e.g. `"4.0000"` for four pieces weighing `quantity` kg in total. Informational: the line is charged and the stock is discounted by `quantity` (the real weight), never by the pieces. `null` on any other line. Only accepted on a `variable_measure` presentation whose unit is a count unit.
      - `counted_unit` 'H87' | 'XPK' | 'XBX' | 'XDS' | 'XBG' | 'XBO' | 'XDJ' | 'XCA' | 'XTB' | 'XCI' | 'XJY' | 'XEN' | 'XCS' | 'null', nullable, required: UN/ECE code of the count unit of `counted_quantity` (`H87` piece, `XPK` pack…). `null` exactly when `counted_quantity` is `null`.
      - `configuration` object, nullable, required: Commercial combination frozen on the line, or `null` when the line was not sold or bought through one (legacy product, manual line).
        - `id` string, required
        - `name` string, nullable, required
        - `signature` string, nullable, required
      - `options` object[], required: Configurable options frozen on the line, in printing order. Always an array, `[]` included.
        - `group` object, required
          - `id` string, required
          - `name` string, nullable, required
        - `value` object, required
          - `id` string, required
          - `name` string, nullable, required
        - `price_adjustment` string, nullable, required: Adjustment of the chosen value per commercial unit. `null` means the value does not change the price — NOT the same as adjusting by `0`.
        - `applied` boolean, required: Whether that amount was actually ADDED to the unit price. `false` when an exact price (price-list entry, combination price or a manually typed unit price) already absorbed it.
      - `price_semantics` 'per_base_unit' | 'per_commercial_unit' | 'null', nullable, required: Monetary semantics of the resolved price: `per_base_unit` (product/variant own price, converted once by the presentation factor) or `per_commercial_unit` (presentation, combination or price-list entry, never converted). `null` on a line with no catalog price context.
      - `price_adjustment_total` string, nullable, required: Sum of the adjustments of the chosen option values, as a decimal string. Informational when an exact price already absorbed them.
      - `option_adjustments_absorbed` boolean, nullable, required: TRI-STATE, and `null` is NOT `false`. `true`: the resolved price already includes the option adjustments. `false`: they were added on top. `null`: it could not be determined — the assembler refuses such a line with 422 rather than risking charging the adjustments twice, so `null` only ever reaches a reader on a legacy line with no configurable options at all.
    - `metadata` Metadata, nullable, required: A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs.
    - `tags` string[], required: Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none.
    - `custom_fields` CustomField[], required: Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none.
      - `field` string, required: Field key (non-empty, ≤ 60 chars).
      - `value` string, required: Field value (≤ 500 chars).
    - `link_expires_at` string, date-time, nullable, required: Expiration date of the shareable public link, or `null` if unlimited.
    - `link_is_active` boolean, required: Indicates whether the shareable public link is currently active.
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required

## Other responses

- `400`: `parameter_unknown` with `subcode` `retired_active_profile_header`: the retired `X-Active-Profile` header was sent; the company goes in the path. `parameter_invalid_format` on `Factuarea-Version`: the header is not a `YYYY-MM-DD` date. `unsupported_api_version`: the `Factuarea-Version` date is not a supported version.
- `401`: Missing or invalid API key.
- `402`: The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, the account lacks the plan or add-on this operation bills against, the annual document limit is reached (`plan_limit_exceeded`, with `error.details.period_end` indicating the end of the period — wait until it has ended or upgrade the plan to continue now), or the account has reached its maximum usage (`storage_quota_exceeded`, raised by upload operations such as signing a delivery note or attaching a file to an expense — contact support before retrying). Nothing was created or modified — resolve the cause and retry the same request. Version note: `error.type` is `payment_required_error` from `Factuarea-Version: 2026-09-01` onwards; earlier versions receive `invalid_request_error` for the three codes that predate that cut (`payment_method_required`, `employee_seat_payment_method_required` and `employee_seat_charge_failed`; every other 402 code, such as `addon_required`, `plan_limit_exceeded` or the additional tax ID codes, is newer and always carries `payment_required_error`). `error.code` is stable across every version.
- `403`: The request is authenticated but not allowed: the API key lacks the required scope (`insufficient_scope`), the plan does not include the public API or this feature (`addon_not_active`, `feature_not_available_in_plan`), or the module is not available with a test key (`module_not_available_in_sandbox`).
- `404`: The requested resource does not exist or belongs to another company. `profile_not_found`: the company in the path does not exist, is not in the API key scope or belongs to the other environment (live or test).
- `409`: The request conflicts with data that already exists or with a request running at the same time. `resource_conflict`: another request was reserving identifiers for the same kind of record at that moment and this one could not complete its reservation. Nothing was written; send the request again after a few seconds.
- `422`: Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). The `error.param` field identifies which input is invalid, if any.
- `429`: Rate limit exceeded. Retry after the duration in `Retry-After`.
- `500`: Unexpected server error.

---

[API](https://skmtc.dev/factuarea/apis/factuarea-public-api.md) · [All operations](https://skmtc.dev/factuarea/apis/factuarea-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/factuarea/apis/factuarea-public-api/revisions/718e77934422?raw)
