Billing — Invoices (Write)
Create, update, and change the status of invoices in the GeoTareas billing system.
All endpoints on this page require a valid JWT token, API key, and tenant header. See Authentication for details.
These endpoints are documented but not yet implemented in the public API (/apidev/v1/billing/*). The internal billing services exist in geotareas/facturacion/ but have not been wired to the API developers module. This documentation reflects the planned contract — DTOs, controller routes, permissions, and tests are still pending.
Create Invoice
Create an invoice manually (without automation). Requires a valid service ID and at least one concept.
/apidev/v1/billing/invoicesRequest Headers
Every request to a protected endpoint requires these headers:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer token obtained from the Login endpoint. Format: Bearer <token> |
X-API-Key | Yes | Company integration key provided during onboarding. Format: gtk_xxx... |
tenant | Yes | Your company hostname (e.g., yourcompany.geotareas.com) |
Content-Type | Conditional | application/json — required for POST and PUT requests |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
service_id | string | Yes | Service/task ID |
type | string | Yes | Invoice type: PAGAR or COBRAR |
currency_id | string | Yes | Currency ID (see Catalogs) |
tariff_id | string | No | Tariff ID. If omitted, prices in concepts are used directly |
number_manual | string | No | Custom invoice number |
date | string | No | Invoice date ISO 8601 (default: now) |
notes | string | No | Observations (max 2000 chars) |
impute_to | object | No | Imputation flags |
impute_to.provider | boolean | No | Impute to provider |
impute_to.account | boolean | No | Impute to account |
impute_to.origin | boolean | No | Impute to origin |
concepts | array | Yes | At least one concept line |
concepts[].concept_id | string | Yes | Concept ID (see Catalogs) |
concepts[].quantity | number | Yes | Quantity (must be > 0) |
concepts[].unit_price | number | Yes | Unit price |
concepts[].tax_percent | number | No | Tax percentage (default: 0) |
Code Examples
- cURL
- JavaScript
- Python
curl -s -X POST "https://api.example.com/apidev/v1/billing/invoices" \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: $APIKEY" \
-H "tenant: $TENANT" \
-H "Content-Type: application/json" \
-d '{
"service_id": "103878",
"type": "PAGAR",
"currency_id": "1",
"tariff_id": "1",
"number_manual": "001-0001",
"date": "2026-04-03T15:02:00",
"notes": "Manual invoice",
"impute_to": { "provider": true, "origin": true },
"concepts": [
{
"concept_id": "3",
"quantity": 1.00,
"unit_price": 900.00,
"tax_percent": 22.00
}
]
}'
const response = await fetch(
'https://api.example.com/apidev/v1/billing/invoices',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${TOKEN}`,
'X-API-Key': APIKEY,
'tenant': TENANT,
'Content-Type': 'application/json',
},
body: JSON.stringify({
service_id: '103878',
type: 'PAGAR',
currency_id: '1',
tariff_id: '1',
concepts: [
{ concept_id: '3', quantity: 1, unit_price: 900, tax_percent: 22 }
],
}),
}
);
const { data } = await response.json();
console.log('Invoice created:', data.id);
import requests
response = requests.post(
'https://api.example.com/apidev/v1/billing/invoices',
headers={
'Authorization': f'Bearer {TOKEN}',
'X-API-Key': APIKEY,
'tenant': TENANT,
},
json={
'service_id': '103878',
'type': 'PAGAR',
'currency_id': '1',
'concepts': [
{'concept_id': '3', 'quantity': 1, 'unit_price': 900.00, 'tax_percent': 22}
],
}
)
data = response.json()['data']
print('Invoice created:', data['id'])
Response — 201 Created
{
"success": true,
"data": {
"id": "7234567890123456789",
"subtotal": 900.00,
"tax": 198.00,
"total": 1098.00,
"status": { "code": "PENDIENTE" }
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
BILLING_SERVICE_NOT_FOUND | 404 | Service does not exist or belongs to another tenant |
BILLING_INVOICE_ALREADY_EXISTS | 409 | An invoice already exists for this service with the same type |
BILLING_TARIFF_INVALID | 422 | Tariff does not apply to this service |
BILLING_CONCEPT_NOT_FOUND | 422 | concept_id does not exist |
| Code | HTTP Status | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | 400 | Request body or query parameters failed validation | Check the error.details field for specific validation failures |
UNAUTHORIZED | 401 | Missing, expired, or invalid JWT token or API key | Re-authenticate via Login to get a fresh token |
RATE_LIMITED | 429 | Too many requests — rate limit exceeded | Wait until the Retry-After header time elapses. See Rate Limits |
INTERNAL_ERROR | 500 | Unexpected server error | Retry after a brief delay. If persistent, contact support |
Update Invoice
Update editable fields of an invoice in PENDIENTE status. Does not allow changing concepts — use Replace Concepts for that.
/apidev/v1/billing/invoices/{id}Request Body (all fields optional)
| Field | Type | Description |
|---|---|---|
number_manual | string | Updated invoice number |
date | string | Updated invoice date (ISO 8601) |
notes | string | Updated observations |
impute_to | object | Updated imputation flags |
{
"number_manual": "001-0002",
"date": "2026-04-03T16:00:00",
"notes": "Corrected invoice",
"impute_to": {
"provider": true,
"vehicle": true
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
BILLING_INVOICE_NOT_FOUND | 404 | Invoice not found |
BILLING_INVOICE_NOT_EDITABLE | 422 | Invoice is in a non-editable status (Closed, Cancelled) |
Replace Concepts
Replace all line items of an invoice. Totals are automatically recalculated. Only works on invoices in PENDIENTE status.
/apidev/v1/billing/invoices/{id}/conceptsThis endpoint replaces all existing concepts. It is not a partial update — send the complete list of concepts you want the invoice to have.
Request Body
{
"concepts": [
{
"concept_id": "3",
"quantity": 1.00,
"unit_price": 950.00,
"tax_percent": 22.00
},
{
"concept_id": "5",
"quantity": 15.00,
"unit_price": 50.00,
"tax_percent": 22.00
}
]
}
Response — 200 OK
{
"success": true,
"data": {
"invoice_id": "7234567890123456789",
"subtotal": 1700.00,
"tax": 374.00,
"total": 2074.00,
"concepts_count": 2
}
}
Change Invoice Status
Change the status of an invoice. Only valid transitions are allowed.
/apidev/v1/billing/invoices/{id}/statusRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
status_code | string | Yes | Target status code (see Billing Statuses) |
notes | string | No | Reason for the status change |
{
"status_code": "CONCILIADO",
"notes": "Reconciled with settlement #456"
}
Response — 200 OK
{
"success": true,
"data": {
"invoice_id": "7234567890123456789",
"previous_status": "PENDIENTE",
"new_status": "CONCILIADO",
"changed_at": "2026-04-03T16:00:00"
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
BILLING_INVOICE_NOT_FOUND | 404 | Invoice not found |
BILLING_INVALID_STATUS | 400 | Target status does not exist |
BILLING_INVALID_STATUS_TRANSITION | 422 | Transition from current to target status is not allowed |
BILLING_INVOICE_LOCKED | 422 | Cancelled invoices cannot change status |
Related
- Invoices — Read
- Billing Catalogs — status codes, concept IDs, tariff IDs
- Automation Rules — automated invoice generation