> ## Documentation Index
> Fetch the complete documentation index at: https://docs.newinvoice.dev/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 against the hosted API at https://api.newinvoice.dev.

This walks through the full path from nothing to a SAT-stamped CFDI 4.0 against the hosted API at
`https://api.newinvoice.dev`. Do it all in **test mode** (an `sk_test_...` key) — test-mode stamps
go to the PAC flagged `prueba=true`, so they are not reported to the SAT and no real CFDI is issued.
To stamp a test invoice right away with no profile at all, see
[Stamp with no setup](https://docs.newinvoice.dev/guides/testing.md#stamp-with-no-setup). When you're ready for real invoices, swap the test key for a
live one (see [Going live](https://docs.newinvoice.dev/guides/going-live.md)).

## 0. Get your API key

Sign up at [`https://api.newinvoice.dev/signup`](https://api.newinvoice.dev/signup) and open the
**dashboard**. Each organization gets a **test** key (`sk_test_...`) and a **live** key
(`sk_live_...`); copy the test key — it's shown once.

```bash theme={null}
API_KEY=sk_test_...            # from the dashboard
```

Every `/v1` call needs `Authorization: Bearer $API_KEY`. The key's mode scopes all data: a test key
never sees live resources and vice-versa, and `livemode` appears on every resource.

## 1. 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.

```bash theme={null}
FP_ID=$(curl -s -X POST https://api.newinvoice.dev/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=YOUR_CSD_PASSWORD" \
  -F "csd_cer=@/path/to/your.cer" \
  -F "csd_key=@/path/to/your.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.

> **Testing without a production CSD?** Test mode tolerates **expired** and SAT test certificates, so
> you can onboard with a free SAT test CSD. **Live mode rejects expired CSDs** (422
> `fiscal_profile.csd_expired`) — use a current CSD to go live.

A **test** profile can stamp straight away, so continue at [step 3](#3-create--stamp-an-invoice).
A **live** profile comes back **on hold**. It 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"`.

## 2. Sign the carta manifiesto (live mode)

The PAC 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. **The gate applies in live mode only**, so skip this step in test
mode.

> **Warning:** the manifiesto has no test mode. Signing with a real FIEL creates a real authorization
> with the PAC for that RFC, even with a `sk_test_` key. See [Testing](https://docs.newinvoice.dev/guides/testing.md).

The manifiesto is signed with the taxpayer's **FIEL (e.firma)**:

```bash theme={null}
curl -s -X POST https://api.newinvoice.dev/v1/fiscal_profiles/$FP_ID/manifest \
  -H "authorization: Bearer $API_KEY" \
  -F "cer=@/path/to/fiel.cer" \
  -F "key=@/path/to/fiel.key" \
  -F "password=YOUR_FIEL_PASSWORD" \
  -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.

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

## 3. 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 https://api.newinvoice.dev/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`.

## 4. 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 https://api.newinvoice.dev/v1/invoices/$INV_ID/stamp \
  -H "authorization: Bearer $API_KEY" | jq '.status,.uuid'
```

## 5. Fetch the artifacts

```bash theme={null}
curl -s https://api.newinvoice.dev/v1/invoices/$INV_ID/xml -H "authorization: Bearer $API_KEY" -o invoice.xml
curl -s https://api.newinvoice.dev/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 after a
successful (`2xx`) response replays it (`idempotent-replayed: true`) instead of stamping twice; a
different body under that key → 409 `idempotency.key_reuse`. A non-`2xx` releases the key, so you
can fix the cause and retry with the same key (see
[Idempotency and retries](https://docs.newinvoice.dev/guides/testing.md#idempotency-and-retries) for the three errors that stay held).
The PAC's own duplicate code 307 is also resolved as a success-replay of the prior stamp.

## Next

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.