Skip to main content

Create Invoices via API — Step by Step

This tutorial walks you through the complete invoice workflow using the docs101 REST API — from creating a customer to downloading a ZUGFeRD-compliant PDF and exporting to DATEV. By the end, you will have a working integration that creates EU-compliant invoices programmatically.

Prerequisites

  • An active Pro plan with an API key (see Authentication)
  • curl or Python 3.8+ with the requests library
  • Base URL: https://docs101.com/api/v1

Python Setup

All Python examples in this tutorial use a shared setup. Define these variables once at the top of your script:

import requests

API_KEY = "ak_live_YOUR_KEY_HERE"
BASE_URL = "https://docs101.com/api/v1"
HEADERS = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}

Step 1: Choose a Template

Every invoice needs a template that defines its visual layout. Fetch the available templates and note the id of the one you want to use.

curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/template/
response = requests.get(f"{BASE_URL}/template/", headers=HEADERS)
templates = response.json()

for tpl in templates:
print(f"ID: {tpl['id']}{tpl['name']}")

You can set a default template per customer (via default_ftl_template_id) or pass the template when creating each invoice.

Step 2: Create a Customer

Create a customer record that your invoices will be billed to.

curl -X POST https://docs101.com/api/v1/customer/ \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"organization_name": "Acme Corp",
"email": "billing@acme.com",
"contact_type": "B2B",
"vat_id": "DE123456789",
"language": "English"
}'
customer_data = {
"organization_name": "Acme Corp",
"email": "billing@acme.com",
"contact_type": "B2B",
"vat_id": "DE123456789",
"language": "English",
}

response = requests.post(f"{BASE_URL}/customer/", headers=HEADERS, json=customer_data)
result = response.json()
customer_id = result["id"]
print(f"Created customer: {customer_id}")

Expected response (201):

{
"message": "Customer successfully created",
"id": 42
}

Customer Fields

FieldTypeRequiredDescription
organization_nameStringYes (B2B)Company name
first_name / last_nameStringYes (B2C)Name for individual customers
emailStringNoEmail address
contact_typeStringNoB2B (default) or B2C
vat_idStringNoEU VAT identification number
languageStringNoInvoice language: English, German
payment_term_idIntegerNoPayment term ID
default_ftl_template_idIntegerNoDefault template for this customer
salutationStringNoMR, MRS, MS, OTHER
Salutation Values

The salutation field expects uppercase values: MR, MRS, MS, OTHER. Lowercase values will return a validation error.

Step 3: Add a Billing Address

Every invoice requires a billing address on the customer.

curl -X POST https://docs101.com/api/v1/customer/42/addresses \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"company": "Acme Corp",
"address_line_1": "Musterstraße 1",
"postal_code": "10115",
"city": "Berlin",
"country_code": "DE",
"is_default": true
}'
address_data = {
"company": "Acme Corp",
"address_line_1": "Musterstraße 1",
"postal_code": "10115",
"city": "Berlin",
"country_code": "DE",
"is_default": True,
}

response = requests.post(
f"{BASE_URL}/customer/{customer_id}/addresses",
headers=HEADERS,
json=address_data,
)
print(f"Address created: {response.status_code}")

Address Fields

FieldTypeRequiredDescription
address_line_1StringYesStreet and house number
cityStringYesCity
country_codeStringYesISO 3166-1 alpha-2 code (e.g. DE, AT, FR)
companyStringNoCompany name in the address
address_line_2StringNoAdditional address line
postal_codeStringNoPostal code
state_provinceStringNoState or region
is_defaultBooleanNoSet as default address

Step 4: Create an Invoice

Create a draft invoice linked to your customer.

curl -X POST https://docs101.com/api/v1/invoice/ \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 42,
"benefit_period_start": "2026-04-01",
"benefit_period_end": "2026-04-30",
"invoice_format": "ZUGFERD"
}'
invoice_data = {
"customer_id": customer_id,
"benefit_period_start": "2026-04-01",
"benefit_period_end": "2026-04-30",
"invoice_format": "ZUGFERD",
}

response = requests.post(f"{BASE_URL}/invoice/", headers=HEADERS, json=invoice_data)
result = response.json()
invoice_id = result["invoice_id"]
print(f"Created invoice: {invoice_id}")

Expected response (201):

{
"message": "Invoice successfully created",
"invoice_id": 17
}

Invoice Fields

FieldTypeRequiredDescription
customer_idIntegerYesCustomer ID from Step 2
benefit_period_startDateNoService period start (YYYY-MM-DD)
benefit_period_endDateNoService period end
invoice_formatStringNoPDF, ZUGFERD, FACTURAPA, PEPPOL, FACTURAE
invoice_numberStringNoAuto-generated if omitted
metaStringNoFree-text metadata
Invoice Date

The invoice_date is automatically set when the invoice is finalized (Step 7) — it is not part of the create payload.

Step 5: Add Line Items (Positions)

Add one or more line items to the invoice. Call this endpoint once per position.

curl -X POST https://docs101.com/api/v1/invoice/17/positions \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"title": "Pro Plan — Monthly Subscription",
"description": "April 2026",
"quantity": 1.0,
"unit_id": "HUR",
"unit_net_amount": 25.00,
"single_net_amount": 25.00,
"tax_rate": 0.19,
"tax_treatment_id": "standard"
}'
position_data = {
"title": "Pro Plan — Monthly Subscription",
"description": "April 2026",
"quantity": 1.0,
"unit_id": "HUR",
"unit_net_amount": 25.00,
"single_net_amount": 25.00,
"tax_rate": 0.19,
"tax_treatment_id": "standard",
}

response = requests.post(
f"{BASE_URL}/invoice/{invoice_id}/positions",
headers=HEADERS,
json=position_data,
)
print(f"Position added: {response.status_code}")

Position Fields

FieldTypeRequiredDescription
titleStringYesLine item description
quantityFloatYesQuantity
unit_idStringYesUnit code — e.g. HUR (hour), C62 (piece), MON (month). See Unit Codes
unit_net_amountFloatYesNet price per unit
single_net_amountFloatYesNet total for the position (quantity x unit price, before discount)
tax_rateFloatNoTax rate as decimal: 0.19 = 19%, 0.07 = 7%, 0 = tax-exempt
tax_treatment_idStringNoe.g. standard, reverse_charge, intra_community, export_outside_eu
descriptionStringNoAdditional description
discount_typeStringNoPERCENTAGE or FIXED_AMOUNT
discount_valueFloatNoDiscount value (e.g. 10 for 10% or 50 for 50 EUR)
Tax Treatment

For cross-border B2B invoices within the EU where the customer has a valid VAT ID, use tax_treatment_id: "intra_community" with tax_rate: 0 (reverse charge). For domestic invoices (same country), use tax_treatment_id: "standard" with the applicable local tax rate. Retrieve all available tax treatments via GET /api/v1/invoice/tax-treatments?locale=en.

Amounts

single_net_amount is the total net amount for the position (quantity x unit price). The server automatically recalculates invoice totals after each position is added.

Step 6: Validate the Invoice

Before finalizing, validate that all required fields are present.

curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/validate
response = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/validate", headers=HEADERS
)
validation = response.json()

if validation["valid"]:
print("Invoice is valid — ready to finalize")
else:
print("Validation errors:")
for error in validation["errors"]:
print(f" - {error}")

Successful response (200):

{
"valid": true,
"errors": [],
"checks": ["..."],
"vat_check": { "status": "valid" }
}
Always Validate

Validation checks all fields required for PDF generation — billing address, bank details, line items, and more. A failed validation will prevent finalization in Step 7.

Step 7: Finalize — Generate PDF (Async Job)

PDF generation is an asynchronous process. You submit a job and then poll for its completion.

Start the job:

curl -X POST https://docs101.com/api/v1/invoice/17/job \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"task": "process_invoice_flag_invoice_send_job"}'
job_response = requests.post(
f"{BASE_URL}/invoice/{invoice_id}/job",
headers=HEADERS,
json={"task": "process_invoice_flag_invoice_send_job"},
)
job_id = job_response.json()["job_id"]
print(f"Job started: {job_id}")

Expected response (201):

{
"message": "Job successfully added",
"job_id": "abc123-..."
}

Poll for completion:

curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/job/abc123-...
import time

while True:
status_response = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/job/{job_id}", headers=HEADERS
)
status = status_response.json()["status"]
print(f"Job status: {status}")

if status == "finished":
break
elif status == "failed":
print(f"Job failed: {status_response.json()}")
break

time.sleep(2)

Possible status values: queued, started, finished, failed

Async Processing

PDF generation typically takes 3 -- 10 seconds. Implement polling with a 2-second interval and a 60-second timeout. The task name process_invoice_flag_invoice_send_job generates the PDF and transitions the invoice status from DRAFT to OPEN.

Invoice Limit

If you exceed the monthly invoice limit (3/month on Free, 100/month on Pro), the endpoint responds with 402 and a structured error containing error_code: "INVOICE_LIMIT_REACHED", usage.used, usage.limit, and usage.reset_date.

Step 8: Download the PDF

Once the job completes, retrieve the download link.

curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/pdf-link
response = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/pdf-link", headers=HEADERS
)
pdf_data = response.json()
print(f"PDF URL: {pdf_data['url']}")
print(f"Filename: {pdf_data['filename']}")

Response:

{
"url": "https://...",
"filename": "INV-2026-000001.pdf"
}

The URL is a temporary (presigned) S3 URL, valid for a few minutes. To download the ZUGFeRD XML separately:

curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/invoice/17/xml-link

Step 9: Update Invoice Status

After sending the invoice to your customer, update its status.

Mark as sent:

curl -X PUT https://docs101.com/api/v1/invoice/17/status \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"status": "SENT"}'
requests.put(
f"{BASE_URL}/invoice/{invoice_id}/status",
headers=HEADERS,
json={"status": "SENT"},
)

Mark as paid:

curl -X PUT https://docs101.com/api/v1/invoice/17/status \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"status": "PAID",
"paid_date": "2026-04-15",
"payment_method": "BANK_TRANSFER",
"payment_reference": "SEPA-2026-04-15"
}'
requests.put(
f"{BASE_URL}/invoice/{invoice_id}/status",
headers=HEADERS,
json={
"status": "PAID",
"paid_date": "2026-04-15",
"payment_method": "BANK_TRANSFER",
"payment_reference": "SEPA-2026-04-15",
},
)
Invoice Lifecycle

The status flow is: DRAFTOPEN (after the job in Step 7) → SENTPAID. You can also cancel any non-finalized invoice via POST /api/v1/invoice/{id}/cancel.

Step 10: Export to DATEV (Optional)

For accounting handoff, you can export invoices in DATEV format. This involves a one-time configuration step followed by the actual export.

Configure DATEV (One-Time)

curl -X PUT https://docs101.com/api/v1/datev/config \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"consultant_number": 1234567,
"client_number": 12345,
"chart_of_accounts": "SKR03",
"fiscal_year_start_month": 1
}'
datev_config = {
"consultant_number": 1234567,
"client_number": 12345,
"chart_of_accounts": "SKR03",
"fiscal_year_start_month": 1,
}

requests.put(f"{BASE_URL}/datev/config", headers=HEADERS, json=datev_config)

Start the Export

curl -X POST https://docs101.com/api/v1/exports/job \
-H "X-API-Key: ak_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"task": "process_datev_export_job",
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"include_master_data": true,
"include_documents": true,
"only_new": true
}'
export_data = {
"task": "process_datev_export_job",
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"include_master_data": True,
"include_documents": True,
"only_new": True,
}

response = requests.post(f"{BASE_URL}/exports/job", headers=HEADERS, json=export_data)
export_job_id = response.json()["job_id"]

Poll the export job via GET /api/v1/exports/job/{job_id} until status: "finished". The finished response includes a result object with download_ids (an array of download IDs).

Download the Export

curl -H "X-API-Key: ak_live_YOUR_KEY_HERE" \
https://docs101.com/api/v1/downloads/{download_id}/download
# After polling confirms the job is finished:
result = export_status_response.json()["result"]
for download_id in result["download_ids"]:
response = requests.get(
f"{BASE_URL}/downloads/{download_id}/download", headers=HEADERS
)
download_url = response.json()["url"]
print(f"DATEV export: {download_url}")

The response contains a presigned S3 URL for the DATEV ZIP file.

Error Handling

StatusMeaningTypical Cause
200Success
201Resource createdCustomer, invoice, position, or job created
400Validation errorMissing required fields or invalid payload
401Not authenticatedAPI key missing or invalid
402Limit reachedMonthly invoice limit exceeded
404Not foundInvalid resource ID
422VAT override neededCustomer VAT ID invalid — resend with vat_override_confirmed: true

For async jobs (PDF generation, DATEV export), implement exponential backoff when polling. Check the status field — if it is failed, inspect the error_message in the job response for details.

Complete Example

This script runs the entire workflow from Steps 2 through 9:

import requests
import time

API_KEY = "ak_live_YOUR_KEY_HERE"
BASE_URL = "https://docs101.com/api/v1"
HEADERS = {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
}

# Step 2: Create a customer
customer = requests.post(
f"{BASE_URL}/customer/",
headers=HEADERS,
json={
"organization_name": "Acme Corp",
"email": "billing@acme.com",
"contact_type": "B2B",
"vat_id": "DE123456789",
"language": "English",
},
).json()
customer_id = customer["id"]
print(f"Customer created: {customer_id}")

# Step 3: Add a billing address
requests.post(
f"{BASE_URL}/customer/{customer_id}/addresses",
headers=HEADERS,
json={
"company": "Acme Corp",
"address_line_1": "Musterstraße 1",
"postal_code": "10115",
"city": "Berlin",
"country_code": "DE",
"is_default": True,
},
)
print("Address added")

# Step 4: Create an invoice
invoice = requests.post(
f"{BASE_URL}/invoice/",
headers=HEADERS,
json={
"customer_id": customer_id,
"benefit_period_start": "2026-04-01",
"benefit_period_end": "2026-04-30",
"invoice_format": "ZUGFERD",
},
).json()
invoice_id = invoice["invoice_id"]
print(f"Invoice created: {invoice_id}")

# Step 5: Add a line item
requests.post(
f"{BASE_URL}/invoice/{invoice_id}/positions",
headers=HEADERS,
json={
"title": "Pro Plan — Monthly Subscription",
"description": "April 2026",
"quantity": 1.0,
"unit_id": "HUR",
"unit_net_amount": 25.00,
"single_net_amount": 25.00,
"tax_rate": 0.19,
"tax_treatment_id": "standard",
},
)
print("Position added")

# Step 6: Validate
validation = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/validate", headers=HEADERS
).json()

if not validation["valid"]:
print(f"Validation failed: {validation['errors']}")
exit(1)
print("Invoice validated")

# Step 7: Finalize (generate PDF)
job = requests.post(
f"{BASE_URL}/invoice/{invoice_id}/job",
headers=HEADERS,
json={"task": "process_invoice_flag_invoice_send_job"},
).json()
job_id = job["job_id"]
print(f"PDF generation started: {job_id}")

for _ in range(30):
time.sleep(2)
status = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/job/{job_id}", headers=HEADERS
).json()["status"]

if status == "finished":
print("PDF generated")
break
elif status == "failed":
print("PDF generation failed")
exit(1)
else:
print("Timeout waiting for PDF generation")
exit(1)

# Step 8: Download the PDF
pdf = requests.get(
f"{BASE_URL}/invoice/{invoice_id}/pdf-link", headers=HEADERS
).json()
print(f"PDF ready: {pdf['filename']}{pdf['url']}")

# Step 9: Mark as sent
requests.put(
f"{BASE_URL}/invoice/{invoice_id}/status",
headers=HEADERS,
json={"status": "SENT"},
)
print("Invoice marked as sent")