> ## Documentation Index
> Fetch the complete documentation index at: https://invoiceapi.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Stamping your first invoice

> This walks through the full path from nothing to a SAT-stamped CFDI 4.0. It runs entirely on the

This walks through the full path from nothing to a SAT-stamped CFDI 4.0. It runs entirely on the
**simulated PAC** — no PAC credentials — so you can copy-paste it against `pnpm dev`.

## 0. Start the API

```bash theme={null}
pnpm install && pnpm dev     # http://localhost:3000, simulated PAC, PGlite
```

## 1. Get an API key

`POST /v1/organizations` is open in dev (gated by `ALLOW_ORG_BOOTSTRAP`) and returns your first
test + live keys. Save the test key; keys are shown once.

```bash theme={null}
API_KEY=$(curl -s -X POST localhost:3000/v1/organizations \
  -H 'content-type: application/json' -d '{"name":"Acme"}' | jq -r '.api_keys.test')
```

Every `/v1` call from here needs `Authorization: Bearer $API_KEY`.

## 2. Register a fiscal profile (emitter + CSD)

An invoice is issued by a **fiscal profile**: an RFC plus its CSD (Certificado de Sello Digital).
Upload the `.cer` and `.key` files directly (multipart) with the private-key password; the RFC,
serial and validity window are read from the certificate. The key material is AES-256-GCM encrypted
at rest and never returned.

The repo bundles a public SAT **test CSD** at `test/cfdi/fixtures/` (RFC `AAA010101AAA`, password
`12345678a`). Just upload the files:

```bash theme={null}
FP_ID=$(curl -s -X POST localhost:3000/v1/fiscal_profiles \
  -H "authorization: Bearer $API_KEY" \
  -F "legal_name=ACCEM SERVICIOS EMPRESARIALES SC" \
  -F "tax_regime=601" -F "zip=26015" -F "csd_password=12345678a" \
  -F "csd_cer=@test/cfdi/fixtures/csd.cer" \
  -F "csd_key=@test/cfdi/fixtures/csd.key" \
  | jq -r '.id')
```

Multipart field names: `csd_cer`, `csd_key`, `csd_password` for the CSD, and `fiel_cer`,
`fiel_key`, `fiel_password` if you also attach a FIEL. Prefer JSON? Send the same data as
`application/json` with `csd: { cer_base64, key_base64, password }` — both content types are accepted.

> The fixture CSD **expired in 2021**. Test/simulated mode tolerates expired CSDs; **live mode
> rejects them** (422 `fiscal_profile.csd_expired`). Use a current CSD to go live.

The profile comes back **on hold**. A brand-new profile has not yet been authorized to stamp, so
its response carries `status: "requires_action"` and a single `next_actions` entry telling you
exactly what to do next:

```json theme={null}
{
  "id": "fp_...",
  "rfc": "AAA010101AAA",
  "status": "requires_action",
  "manifest_status": "pending_signature",
  "next_actions": [
    { "type": "sign_manifest",
      "message": "Upload the taxpayer's FIEL (e.firma) and sign the carta manifiesto to authorize CFDI stamping for this RFC.",
      "endpoint": "POST /v1/fiscal_profiles/fp_.../manifest",
      "requires": ["fiel"] }
  ]
}
```

On retrieve or list the same pending action appears as `pending_actions`. When the profile is
authorized both arrays are `[]` and `status` is `"active"`.

## 3. Sign the carta manifiesto

Prodigia requires a signed **carta manifiesto** per RFC to authorize *timbrado* (invoice stamping)
for that taxpayer — this is the SAT/PAC authorization, not the CSD. This API signs it for you; there
is no browser portal to visit. **This gate applies in test/simulated mode too** (the simulated PAC
signs the manifiesto so onboarding works end to end), so you sign it once here before stamping.

The manifiesto is signed with the taxpayer's **FIEL (e.firma)**. In simulated mode the bundled
fixture CSD doubles as the FIEL, so you can reuse the same `test/cfdi/fixtures/` files:

```bash theme={null}
curl -s -X POST localhost:3000/v1/fiscal_profiles/$FP_ID/manifest \
  -H "authorization: Bearer $API_KEY" \
  -F "cer=@test/cfdi/fixtures/csd.cer" \
  -F "key=@test/cfdi/fixtures/csd.key" \
  -F "password=12345678a" \
  -F "email=legal@example.mx" | jq '.status'
```

The manifest endpoint accepts the FIEL inline as `-F cer= -F key= -F password=` (or `fiel: { cer_base64,
key_base64, password }` in JSON), or reuses a FIEL already stored on the profile. On success the
manifiesto is signed once and for all: the profile flips to `status: "active"` (`manifest_status:
"signed"`), `next_actions`/`pending_actions` empty out, and a `fiscal_profile.manifest_signed`
webhook fires. Signing is idempotent — an "already signed" result is treated as success.

<Warning>
  Skip this step and the very next stamp attempt returns **409 `fiscal_profile.manifest_required`**
  (in both test and live), with remediation pointing back to this endpoint. Every stamp call checks
  the manifiesto, so you only ever sign it once per profile.
</Warning>

## 4. Create + stamp an invoice

Send friendly JSON. You do **not** send the issuer — it comes from the profile. Omit taxes and
each item defaults to 16% IVA traslado; `subtotal`/`total` are computed and rounded to the
currency's decimals.

```bash theme={null}
curl -s -X POST localhost:3000/v1/invoices \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -H "idempotency-key: first-invoice-1" \
  -d "{
    \"fiscal_profile_id\": \"$FP_ID\",
    \"type\": \"I\",
    \"payment_form\": \"03\",
    \"payment_method\": \"PUE\",
    \"use\": \"G03\",
    \"customer\": { \"rfc\": \"URE180429TM6\", \"name\": \"UNIVERSIDAD ROBOTICA ESPANOLA\", \"zip\": \"65000\", \"tax_regime\": \"601\" },
    \"items\": [ { \"product_key\": \"01010101\", \"unit_key\": \"H87\", \"description\": \"Servicio de consultoría\", \"quantity\": 1, \"unit_price\": \"100.00\" } ]
  }" | jq
```

You get back `status: "stamped"`, a real `uuid` (folio fiscal), `total: "116.00"`, `serie: "A"`,
`folio: "1"`, the `sello_cfd`/`sello_sat`, and `xml_url`/`pdf_url`.

### Required fields, briefly

| Field            | Notes                                                                                                     |
| ---------------- | --------------------------------------------------------------------------------------------------------- |
| `type`           | `I` ingreso (default), `E` egreso/nota de crédito, `P` pago (complemento 2.0).                            |
| `payment_form`   | c\_FormaPago (`03` transferencia, `01` efectivo…). Required for I/E.                                      |
| `payment_method` | `PUE` (una exhibición) or `PPD` (parcialidades/diferido). Required for I/E.                               |
| `use`            | UsoCFDI (`G03` gastos en general…). Set here or on `customer.use`.                                        |
| `customer`       | `rfc`, `name`, `zip`, `tax_regime` — must match the receptor's Constancia de Situación Fiscal.            |
| `items[]`        | `product_key` (8-digit ClaveProdServ), `unit_key` (ClaveUnidad), `description`, `quantity`, `unit_price`. |

Common coherence rules the API enforces (returned as 422 `cfdi.invalid_input` with a `path`):

* `payment_method: "PPD"` forces `payment_form: "99"` (Por definir).
* Type `P` must not carry items or `payment_method`; it needs `payments`.
* Type `E` should carry `relation` (type `01` nota de crédito or `03` devolución) + original UUID.
* Receptor `XAXX010101000` (público en general) requires `global_information`.
* Non-MXN `currency` requires `exchange_rate`.

## 5. Draft, then stamp later (optional)

Pass `"draft": true` to build and sign without stamping; the invoice returns `status: "draft"`.
Stamp it when ready:

```bash theme={null}
curl -s -X POST localhost:3000/v1/invoices/$INV_ID/stamp \
  -H "authorization: Bearer $API_KEY" | jq '.status,.uuid'
```

## 6. Fetch the artifacts

```bash theme={null}
curl -s localhost:3000/v1/invoices/$INV_ID/xml -H "authorization: Bearer $API_KEY" -o invoice.xml
curl -s localhost:3000/v1/invoices/$INV_ID/pdf -H "authorization: Bearer $API_KEY" -o invoice.pdf
```

## Idempotency

Send `Idempotency-Key: <unique>` on POSTs. A retry with the same key + identical body replays the
stored response (`idempotent-replayed: true`) instead of stamping twice; a different body under
the same key → 409 `idempotency.key_reuse`. The PAC's own duplicate code 307 is also resolved as
a success-replay of the prior stamp, so double submits never mint two CFDIs.

## Next

* [Cancellation](/guides/cancellation) · [Webhooks](/guides/webhooks) · [Going live](/guides/going-live)
* Full field reference: [`/openapi.json`](/api-reference) · error codes: [`../errors.md`](/concepts/errors)
