Tutorial: Issue a bill from a USD transaction

When a corporate card transaction is in USD (or any non-local currency), most Chilean buyers also need to issue a "factura de compra" (buy-invoice) at the SII — both for VAT recovery and for FX-cost accounting. This tutorial shows how to do that programmatically from a card transaction.

The shape:

  1. Find the source transaction (card_transactions index).
  2. Read the transaction's exchange rate snapshot (conversion_rates).
  3. Create a bill referencing the transaction.
  4. Pre-issue, sign, and emit the bill.

1. Find the source transaction

curl 'https://api.cardda.com/v1/card_transactions?currency[$ne]=CLP&_order=desc&_field=created_at&_end=20' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"

Pick a transaction that does not already have an associated bill (fiscal_invoice_id is null):

{
  "id": "tx-uuid",
  "amount": 9900,
  "currency": "USD",
  "amount_in_clp": 9270000,
  "merchant": { "name": "GitHub", "category": "software" },
  "fiscal_invoice_id": null,
  "occurred_at": "2026-04-15T10:00:00Z"
}

2. Get the exchange-rate snapshot

The bill must use the same USD→CLP rate that Cardda used to settle the transaction (otherwise the SII will flag a discrepancy at the next audit).

curl 'https://api.cardda.com/v1/exchange_rates/USD?at=2026-04-15' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"
{ "currency": "USD", "rate": 936.36, "effective_at": "2026-04-15T00:00:00Z" }

3. Create the bill

curl -X POST 'https://api.cardda.com/v1/bills' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "card_transaction_id": "tx-uuid",
    "kind": "buy_invoice",
    "supplier": {
      "tax_id": "59001520-9",
      "name": "GitHub Inc",
      "country": "US"
    },
    "items": [
      {
        "description": "GitHub Enterprise — annual subscription",
        "quantity": 1,
        "unit_price_usd": 9900,
        "tax_rate": 0
      }
    ],
    "exchange_rate": 936.36,
    "issue_date": "2026-04-15"
  }'
{
  "id": "bill-uuid",
  "status": "draft",
  "kind": "buy_invoice",
  "folio": null,
  "total_clp": 9270000,
  ...
}

4. Pre-issue → sign → issue

The SII flow is three calls:

# (a) Pre-issue: validates the bill against SII rules locally
curl -X POST 'https://api.cardda.com/v1/bills/BILL_ID/preissue' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"

# (b) Sign with the company's electronic certificate (must be uploaded once via the dashboard)
curl -X POST 'https://api.cardda.com/v1/bills/BILL_ID/sign' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"

# (c) Submit to the SII
curl -X POST 'https://api.cardda.com/v1/bills/BILL_ID/issue' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"

After (c), the bill's status becomes accepted (or rejected with details in sii_response). A folio is assigned by the SII and returned in the response body.

End-to-end snippet (Node.js)

import fetch from "node-fetch";

const api = "https://api.cardda.com/v1";
const H = {
  Authorization: `Bearer ${process.env.API_KEY}`,
  "company-id": process.env.COMPANY_ID,
  "Content-Type": "application/json",
};

async function call(method, path, body) {
  const res = await fetch(api + path, { method, headers: H, body: body && JSON.stringify(body) });
  if (!res.ok) throw new Error(`${method} ${path} → ${res.status} ${await res.text()}`);
  return res.json();
}

// 1. Find a USD transaction without a bill
const [tx] = await call("GET", "/card_transactions?currency=USD&fiscal_invoice_id=null&_end=1");

// 2. Get the rate
const rate = await call("GET", `/exchange_rates/USD?at=${tx.occurred_at.slice(0, 10)}`);

// 3. Create the bill
const bill = await call("POST", "/bills", {
  card_transaction_id: tx.id,
  kind: "buy_invoice",
  supplier: { tax_id: tx.merchant.tax_id, name: tx.merchant.name, country: "US" },
  items: [{
    description: tx.merchant.name,
    quantity: 1,
    unit_price_usd: tx.amount,
    tax_rate: 0,
  }],
  exchange_rate: rate.rate,
  issue_date: tx.occurred_at.slice(0, 10),
});

// 4. Pre-issue → sign → issue
await call("POST", `/bills/${bill.id}/preissue`);
await call("POST", `/bills/${bill.id}/sign`);
const final = await call("POST", `/bills/${bill.id}/issue`);

console.log({ id: final.id, folio: final.folio, status: final.status });

Bulk path

For month-end reconciliation, use:

curl -X POST 'https://api.cardda.com/v1/bills/issue_all' \
  -H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
  -H "Content-Type: application/json" \
  -d '{ "bill_ids": ["...", "...", "..."] }'

Cardda processes each bill sequentially and returns a per-id status map. Failures don't roll back the successful ones.

Common pitfalls

  • Mismatched exchange rate. If your rate differs from the one Cardda used at settlement, the SII may accept the bill but month-end recon will flag a delta. Always pull from /v1/exchange_rates/USD?at=<settlement_date>.
  • Missing electronic certificate. Step (b) fails with 422 missing_certificate if the company has not uploaded its SII signing certificate. Upload via dashboard → Settings → Tax → SII keys.
  • Re-issuing. Once a bill has status: accepted, you cannot re-issue. To "fix" an issued bill, emit a credit note that points to the original folio.

Related