Billing API - Integration Reference

Operational documentation for synchronous Moloni document issuance.

Base URL: https://billing.theinventors.io FR: POST /api/v1/invoices/ FT: POST /api/v1/documents/invoices/ RC: POST /api/v1/documents/receipts/ Auth: API Key + Secret Target timeout: 10s

1. Endpoint Contract

All endpoints are synchronous: each request returns either a final issuance result or a final error.

DocumentPathUse case
Invoice receipt (FR)POST /api/v1/invoices/Immediate payment; existing backward-compatible endpoint.
Invoice (FT)POST /api/v1/documents/invoices/Issue invoice now, payment later.
Receipt (RC)POST /api/v1/documents/receipts/Register payment against a previously issued invoice.

2. Authentication

2.1 Required headers

The credentials below are for demonstration only and do not grant real access.

X-Client-Id: airtable_ops_prod
X-Api-Key: ak_demo_9f83b7d1a5c40c22
X-Api-Secret: as_demo_5baf4f0f613949f0863b83de8f4c4f90

3. Idempotency-Key

Idempotency-Key is a required request header generated by the integrator. It protects document issuance from accidental duplicates when a request is retried because of a timeout, network failure, or uncertain response.

How it works

How to generate it

Use a stable unique value from the source system when possible, prefixed by the operation type. A UUID is also valid. Keep the same key only for retries of the exact same operation and exact same JSON body.

FR immediate payment: Idempotency-Key: fr-order-74291
FT invoice issue:     Idempotency-Key: ft-school-2026-0001
RC receipt issue:     Idempotency-Key: rc-school-2026-0001
UUID example:         Idempotency-Key: 2878e76a-6654-4e52-a11b-dcc525eef985

Retry examples

ScenarioCorrect key behaviorResult
Retry the same FT after a timeoutReuse ft-school-2026-0001 with the same bodyReturns the original FT response.
Create the RC for that FTUse a new key, for example rc-school-2026-0001Creates a separate receipt operation.
Send same key with changed amount/customer/itemsDo not do thisReturns 409 IDEMPOTENCY_CONFLICT.

4. Shared Sales Payload

The sales payload below is used by FR and FT. Optional fields can be omitted. If VAT number is missing, the system applies final-consumer VAT number internally.

{
  "reference": "TI-2026-05-18-0007",
  "customer": {
    "name": "Eduardo Costa",
    "vat_number": "509999999",
    "email": "[email protected]",
    "address": "Rua do Campo Grande 45, Lisboa"
  },
  "items": [
    {
      "product": "mensalidade TheInventors",
      "gross_price": 10.00,
      "vat_rate": 23
    }
  ]
}

Relevant rules

Optional fields (accepted and processed)

FieldTypeBehavior
customer.vat_numberstringCustomer matching uses this value first. FT requires a valid PT VAT number. FR may fall back to the final-consumer customer when the supplied VAT number is invalid.
customer.addressstringUsed when creating a new customer. Existing customers are reused without updating their profile.
customer.emailstringIf present, validated as email and forwarded to customer flow in Moloni.

Customer resolution

5. Invoice Receipt (FR)

Use this when the customer pays immediately and the issued document must include payment.

Request

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://billing.theinventors.io"
PATH_URL="/api/v1/invoices/"
IDEMP="fr-escola-2026-0001"
CLIENT_ID="airtable_ops_prod"
API_KEY="ak_demo_9f83b7d1a5c40c22"
API_SECRET="as_demo_5baf4f0f613949f0863b83de8f4c4f90"

BODY='{
  "reference": "FR-ESCOLA-2026-0001",
  "customer": {
    "name": "Escola Exemplo, Lda",
    "vat_number": "509999999",
    "email": "[email protected]",
    "address": "Rua Exemplo 1, Lisboa"
  },
  "items": [
    {
      "product": "Mensalidade Julho",
      "gross_price": 500.00,
      "vat_rate": 23
    },
    {
      "product": "Kit aluno",
      "gross_price": 120.00,
      "vat_rate": 23
    }
  ]
}'

curl -sS -X POST "${BASE_URL}${PATH_URL}" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: ${CLIENT_ID}" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "X-Api-Secret: ${API_SECRET}" \
  -H "Idempotency-Key: ${IDEMP}" \
  --data "$BODY"

Success response

{
  "success": true,
  "request_id": "req_4ad9b2ec1d9f3a71",
  "reference": "FR-ESCOLA-2026-0001",
  "ref_interna": "FR-ESCOLA-2026-0001",
  "document": {
    "type": "invoice_receipt",
    "moloni_document_id": 974989624,
    "number": "FR M/32",
    "series": "M",
    "total_gross": 620,
    "currency": "EUR",
    "pdf_url": "https://...",
    "created_at": "2026-07-28T08:15:00Z"
  },
  "documento": {
    "tipo": "fatura_recibo",
    "moloni_document_id": 974989624,
    "numero": "FR M/32",
    "serie": "M",
    "total_com_iva": 620,
    "moeda": "EUR",
    "pdf_url": "https://...",
    "created_at": "2026-07-28T08:15:00Z"
  },
  "email": {
    "status": "sent",
    "error": null
  }
}

When FR uses the final-consumer fallback, the success response also includes:

"customer_resolution": {
  "fallback": "final_consumer_invalid_vat"
}

6. Invoice (FT)

Use this when the customer will pay later. This issues an invoice without payment attached.

Optional alternate billing address (FT only)

customer.alternate_address_id is an optional positive JSON integer identifying an existing Moloni alternate address. Omit it to retain the current main-address behavior. Strings, decimal numbers, null, zero, negative values and booleans are rejected.

"customer": {
  "name": "Escola Exemplo",
  "vat_number": "500766460",
  "alternate_address_id": 2051743
}

With this field, a valid VAT number and an existing, uniquely matched customer are required. The API resolves the customer by VAT and queries customerAlternateAddresses/getAll in the selected Moloni environment, checking both address and customer IDs before invoices/insert. It never creates or updates a customer or address in this flow. IDs are environment-specific: a PROD ID must not be reused in STG.

Malformed, unavailable or other-customer addresses return 422 INVALID_ALTERNATE_ADDRESS, with error.details.field = customer.alternate_address_id, without issuing an invoice. An upstream validation failure returns 503 MOLONI_UNAVAILABLE (or 504 MOLONI_TIMEOUT), not an invalid-address result. Ambiguous VAT matching keeps 422 CUSTOMER_AMBIGUOUS; invalid VAT keeps the existing validation error.

The field is included in the original body hash. Changing it with the same Idempotency-Key returns 409 IDEMPOTENCY_CONFLICT. Identical-body retries return the stored response, including stored errors. FR and RC are unchanged; this option is supported only on FT. The legacy alias cliente.alternate_address_id is also accepted; do not combine both customer object aliases when requesting an alternate address.

Request

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://billing.theinventors.io"
PATH_URL="/api/v1/documents/invoices/"
IDEMP="ft-escola-2026-0001"
CLIENT_ID="airtable_ops_prod"
API_KEY="ak_demo_9f83b7d1a5c40c22"
API_SECRET="as_demo_5baf4f0f613949f0863b83de8f4c4f90"

BODY='{
  "reference": "FT-ESCOLA-2026-0001",
  "customer": {
    "name": "Escola Exemplo, Lda",
    "vat_number": "509999999",
    "email": "[email protected]",
    "address": "Rua Exemplo 1, Lisboa"
  },
  "items": [
    {
      "product": "Mensalidade Julho",
      "gross_price": 500.00,
      "vat_rate": 23
    },
    {
      "product": "Kit aluno",
      "gross_price": 120.00,
      "vat_rate": 23
    }
  ]
}'

curl -sS -X POST "${BASE_URL}${PATH_URL}" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: ${CLIENT_ID}" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "X-Api-Secret: ${API_SECRET}" \
  -H "Idempotency-Key: ${IDEMP}" \
  --data "$BODY"

Success response

{
  "success": true,
  "request_id": "req_9f0b4c7e2d1a6b35",
  "reference": "FT-ESCOLA-2026-0001",
  "ref_interna": "FT-ESCOLA-2026-0001",
  "document": {
    "type": "invoice",
    "moloni_document_id": 998320760,
    "number": "FT ESC/123",
    "series": "ESC",
    "total_gross": 620,
    "currency": "EUR",
    "pdf_url": "https://...",
    "created_at": "2026-07-28T08:20:00Z"
  },
  "documento": {
    "tipo": "fatura",
    "moloni_document_id": 998320760,
    "numero": "FT ESC/123",
    "serie": "ESC",
    "total_com_iva": 620,
    "moeda": "EUR",
    "pdf_url": "https://...",
    "created_at": "2026-07-28T08:20:00Z"
  }
}

7. Receipt (RC)

Use this when the invoice has been paid. If the FT was created by this API, send its moloni_document_id as invoice_document_id. The system reuses the stored customer and invoice total unless an explicit payment.amount is provided.

Request against an invoice created by this API

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://billing.theinventors.io"
PATH_URL="/api/v1/documents/receipts/"
IDEMP="rc-escola-2026-0001"
CLIENT_ID="airtable_ops_prod"
API_KEY="ak_demo_9f83b7d1a5c40c22"
API_SECRET="as_demo_5baf4f0f613949f0863b83de8f4c4f90"

BODY='{
  "reference": "RC-ESCOLA-2026-0001",
  "invoice_document_id": 998320760,
  "payment": {
    "date": "2026-07-28",
    "amount": 620.00,
    "method": "bank_transfer"
  }
}'

curl -sS -X POST "${BASE_URL}${PATH_URL}" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: ${CLIENT_ID}" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "X-Api-Secret: ${API_SECRET}" \
  -H "Idempotency-Key: ${IDEMP}" \
  --data "$BODY"

Minimal JSON body

{
  "reference": "RC-ESCOLA-2026-0001",
  "invoice_document_id": 998320760,
  "payment": {
    "date": "2026-07-28",
    "amount": 620.00,
    "method": "bank_transfer"
  }
}

Success response

{
  "success": true,
  "request_id": "req_a2c7d1e9f0b3c4d5",
  "reference": "RC-ESCOLA-2026-0001",
  "ref_interna": "RC-ESCOLA-2026-0001",
  "document": {
    "type": "receipt",
    "moloni_document_id": 998421111,
    "number": "RC ESC/45",
    "series": "ESC",
    "total_gross": 620,
    "currency": "EUR",
    "pdf_url": "https://...",
    "created_at": "2026-07-28T08:30:00Z",
    "associated_invoice_document_id": 998320760
  },
  "documento": {
    "tipo": "recibo",
    "moloni_document_id": 998421111,
    "numero": "RC ESC/45",
    "serie": "ESC",
    "total_com_iva": 620,
    "moeda": "EUR",
    "pdf_url": "https://...",
    "created_at": "2026-07-28T08:30:00Z",
    "fatura_associada_moloni_document_id": 998320760
  }
}

External invoice fallback

If the invoice was not created by this API, include customer and payment.amount, because the system cannot reuse stored customer/total data.

When the invoice exists in this API, the receipt reuses the Moloni environment recorded on that invoice. School FT and RC documents use the dedicated ESC series in PROD and the registered M2026 series in STG. The FR configuration remains independent.

8. Error Response

All endpoints return the same error envelope.

{
  "success": false,
  "request_id": "req_62d03a9dabc7e8f4",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "items[0].vat_rate is invalid.",
    "details": null
  }
}

9. HTTP and Functional Codes

HTTPCodeWhen it happens
201-Document issued successfully.
400VALIDATION_ERRORInvalid payload or missing Idempotency-Key.
401AUTH_ERRORMissing or invalid API client credentials.
403IP_NOT_ALLOWEDSource IP is outside the key whitelist.
409IDEMPOTENCY_CONFLICTSame Idempotency-Key used with a different body.
422MOLONI_BUSINESS_ERRORBusiness rule rejected by Moloni API.
422CUSTOMER_AMBIGUOUSAmbiguous customer during resolution process.
422INVALID_ALTERNATE_ADDRESSFT-only: invalid alternate_address_id, missing VAT/existing customer, nonexistent or other-customer address. No invoice issued.
502MOLONI_BAD_GATEWAYInvalid/intermediary response from Moloni.
503MOLONI_UNAVAILABLEMoloni service unavailable.
504MOLONI_TIMEOUTTimeout while communicating with Moloni.

10. Integration Notes

Backoffice request type

The Type of Request column identifies the requested fiscal document and, when issuance succeeded, includes its Moloni number:

If issuance fails before Moloni assigns a number, the column displays only Invoice Receipt, Invoice, or Receipt. The failure reason remains in the separate Error column.

Backoffice: https://billing.theinventors.io/admin/