Skip to content
Rechnungskit

Connect your own application via API

Updated

With the REST API, your own application (SaaS, your own checkout, a platform) reports transactions to Rechnungskit. This page covers keys, endpoints, the most important fields and what to watch for when you send data. The full reference is in the API reference (in German).

The flow in short: your application sends the order. The payment comes from the connected payment provider, and as soon as the two match, Rechnungskit creates the document. For goods, the invoice also waits for your shipment report.

REST API · flow
From API order to e-invoice
Your application
POST /v1/ordersline items and amounts in cents, optional paymentRef
POST /v1/fulfillmentsonly for physical goods, sets the date of supply
Rechnungskit
  1. Accepts the orderidempotent through your orderNo
  2. Matches the paymentfrom the connected payment provider, paymentRef speeds this up
  3. Creates the documentwith EN 16931 validation and PDF
Result
E-invoiceZUGFeRD, with a valid Leitweg-ID as XRechnung 3.0
Fetch via APIstatus through GET /v1/orders/{orderNo}, PDF and XML through /v1/invoices
There is no "paid" field: the payment status comes from the payment provider. The only exception is an order paid entirely with referral credit.

Basics

Item How it works
Base URL https://api.rechnungskit.de/v1
Format REST with JSON, OpenAPI 3.1
Authentication bearer token with your API key
Amounts always integer cents, never decimals
Idempotency Your order number (orderNo) is the natural key, and the Idempotency-Key header is supported as well. Duplicate submissions never create duplicate documents.

API keys

You create keys under Connections → API. There are two permissions:

Permission What for
orders:write report orders, cancellations and fulfillments, send credit movements and balances
invoices:read fetch documents, including the state of an order through GET /v1/orders/{orderNo}

There is ONE key type (prefix rk_key_). Whether documents are test data or real depends only on the project's test mode, never on the key. The same key creates test documents in test mode and real documents after go-live, so nothing has to be swapped. Older keys with the prefix rk_test_ or rk_live_ stay valid unchanged.

  • Shown once: the full key is shown exactly once, after that only its beginning.
  • Expiry: your choice of 30, 90 or 365 days or no expiry.
  • Unused: keys not used for more than 90 days are flagged.

Main endpoints

Endpoint What for Permission
POST /v1/orders submit an order (line items, amounts in cents, payment method) orders:write
GET /v1/orders/{orderNo} state of an order with all its documents invoices:read
POST /v1/orders/{orderNo}/cancel report a canceled order (see below) orders:write
POST /v1/fulfillments report shipment (sets the Leistungsdatum, the date of supply), only needed for physical goods orders:write
GET /v1/invoices list documents invoices:read
GET /v1/invoices/{reference} fetch one document invoices:read
GET /v1/invoices/{reference}/pdf and /xml download document files invoices:read

Example order

POST https://api.rechnungskit.de/v1/orders
Authorization: Bearer rk_key_…

{
  "orderNo": "CM-10482",
  "email": "kunde@example.com",
  "paymentRef": "pi_123",
  "requiresShipping": true,
  "shippingGrossCents": 490,
  "billingAddress": { "name": "Erika Muster", "country_code": "DE" },
  "items": [
    { "name": "Kaffee 1kg", "quantity": 2, "unitGrossCents": 2490, "taxRate": 7, "productId": "bohne-42" }
  ]
}

Required are orderNo, billingAddress with country_code and at least one line item with name, quantity and unitNetCents or unitGrossCents.

Shipping costs

You send shipping costs in one of two ways:

  • shippingGrossCents: the gross amount paid. Rechnungskit works out the tax itself.
  • shippingNetCents plus shippingTaxRate: when your checkout has already calculated the shipping tax.

If the cart has mixed tax rates, Rechnungskit splits the shipping automatically by the gross value of the goods (BFH, judgment of 22.01.2025, XI R 22/22: split by gross selling prices), just as in the Rechnungskit checkout. The amount paid never changes.

Fetching an order's documents

GET /v1/orders/{orderNo} returns for each document:

  • the type (invoice, correction, voucher_receipt) and the document kind,
  • the reference to the corrected invoice (referencedInvoiceId),
  • date, gross amount and PDF link.

What GET /v1/invoices/{reference} returns depends on what you ask with:

You ask with You get
UUID or document number exactly that document
order number or payment reference the order's invoice, even if a cancellation invoice (Storno) exists by now

You fetch the cancellation invoice by its UUID from GET /v1/orders/{orderNo}.

Item ID per line: productId

Each line item may carry a productId, the stable item ID from your system. It is optional but recommended: it ties the line to exactly one product in the Rechnungskit catalog, even if the name or SKU changes later.

Without a productId, Rechnungskit looks up the product by SKU first and, if that is missing too, by the exact name. If you then change your SKU scheme or add a SKU later, a new catalog product appears for each variant. You notice this first where products are maintained: the same item shows up several times in the list, and each version needs its own VAT category and revenue recognition.

The ID can be any text (the primary key of your product table, for example). It only has to stay the same for each item. Different productId values mean different items, even with identical names. That keeps real variants cleanly apart.

SaaS and digital services: one endpoint

For SaaS or purely digital services, POST /v1/orders is all you need. Simply leave out the field requiresShipping (default false). Then there is no fulfillment waiting step, and /v1/fulfillments is never called.

The invoice is created as soon as the payment from the connected payment provider matches the order. Sending paymentRef speeds up the matching. There is deliberately no "paid" field: the payment status comes from the payment provider, not from the sender.

Service period (SaaS/subscriptions): the field service_period

For time-based services (SaaS, subscriptions), you send the service period in the optional field service_period. Note that this one is snake_case, not camelCase.

Rule Details
Shape object with start_date and end_date, each an ISO date YYYY-MM-DD
Complete either both keys or neither, if one is missing you get 422
Order end_date must not be before start_date

Example in the order body:

"service_period": { "start_date": "2026-06-01", "end_date": "2026-06-30" }
service_period · cases
What happens with the service period
Sent or stored on the product
Drives the deferred income (balance sheet accounting only) and appears as the service period on the ZUGFeRD invoice. A period you send takes precedence over the billing cycle on the SaaS product
Invoiceright away
Left out, no default
With EÜR nothing else happens. With balance sheet accounting, Rechnungskit holds the invoice as a task "Product without revenue recognition"
Balance sheetonly once added
Starts in an earlier financial year
With balance sheet accounting, a task "Service period in a prior year" appears, as a notice, not a block
Invoicestill right away
With EÜR, revenue counts at once, and there is no deferral.

What the period is for

The service period controls the deferred income (pRAP, passive Rechnungsabgrenzung; only for businesses that prepare a balance sheet; with EÜR, the German cash-basis accounting, revenue counts at once) and appears as the service period on the ZUGFeRD invoice. A service_period sent here takes precedence over the SaaS default on the product.

If you leave the field out, the product default applies (the billing cycle stored on the SaaS product, for example). The checkout link (/c/…) has no field for this; there the period comes from the product configuration.

Backdated service period

A start_date BEFORE the invoice date is allowed. If the start is in the same financial year, that is no problem, the annual result is correct.

If the period starts in an EARLIER financial year, usually one already closed, Rechnungskit (for balance sheet accounting) creates a task "Service period in a prior year". The months already past belong to the prior year on an accrual basis, and a correction of the balance sheet may be needed. The invoice is created anyway; the task is a notice, not a block.

Missing service period for time-based products

If you send no service_period and the product has no SaaS default (billing period) either, it depends on how you determine profit:

  • EÜR: revenue counts at once, and nothing else happens.
  • Balance sheet accounting: Rechnungskit needs the period for the deferral. The invoice is then NOT created right away but held as a task "Product without revenue recognition" under To review. No invoice number is assigned yet.

You then set the revenue recognition on the product (Products → All products) or send the service_period for the order afterwards. The invoice is then created automatically.

Payment methods via the API

Payments are matched through the payment provider connections (Stripe, Mollie, PayPal, Unzer); your application supplies the order data. For pay by invoice, the field paymentGateway must match a pay-by-invoice gateway stored under Connections → Connect exactly. Spelling counts.

Bank transfer via Stripe (paymentGateway stripe_transfer)

If your customer is to pay by bank transfer and the money is to arrive through your Stripe account, you send the order with paymentGateway "stripe_transfer" and an email address.

stripe_transfer · flow
Bank transfer through your Stripe account
  1. Your applicationOrder with stripe_transferplus an email address
  2. RechnungskitInvoice and payment requestservices right away, goods with the shipment report
  3. Your customerTransfersto the virtual Stripe IBAN, with Stripe's reference as the payment reference
  4. StripeMatchesRechnungskit closes the open item
No Stripe invoice is created, so there is no Invoicing fee, only the fee for the transfer.

This is how it works:

  1. For a service, the invoice is created right away; for goods, only when you report shipment through /v1/fulfillments.
  2. At the same time, Rechnungskit creates a payment request for bank transfer in your Stripe account.
  3. The invoice shows your customer's virtual Stripe IBAN and, as the payment reference, Stripe's reference for exactly this payment.
  4. When the transfer arrives with this reference, Stripe matches the money and Rechnungskit closes the open item under /receivables.

Since no Stripe invoice is created, there is no Invoicing fee, only the fee for the transfer. The transfer amount is the order's gross total, and the invoice matches it to the cent.

For this, the Stripe key needs the permissions Customers (write) and Payment Intents (write). If one is missing, no invoice is created. Instead you get an urgent task plus an email, and once you add the permission, Rechnungskit creates the invoice automatically. If the payment request is canceled in Stripe before it is paid, a cancellation invoice follows.

Referral credit on the order: creditRedemptions

If your customer used referral credit for an order, you send the amount in the optional field creditRedemptions. It is a list of objects:

Field Content
type "referral_credit"
amountCents whole cents, greater than 0
externalRef optional, the ID of the redemption in your system
"creditRedemptions": [{ "type": "referral_credit", "amountCents": 1000, "externalRef": "tx_81" }]

With this field, grossTotalCents is required, and the sum of the redemptions must not exceed the gross amount. Otherwise the API responds with 422.

Credit is a payment, not a discount

The invoice shows the VAT on the full amount, and the credit part counts as already paid. The payment method names both parts, such as "PayPal (37,00 €) und Empfehlungsguthaben (12,00 €)" on a German invoice. Your payment provider only collects the rest. Discounts still belong in discountNetCents.

If the order is paid entirely with credit, there is no payment at the provider. You then send financialStatus "paid", and the sum of the redemptions equals grossTotalCents. Rechnungskit then issues the invoice without an incoming payment; for goods, only when shipment is reported.

Your shop keeps the credit itself. Rechnungskit mirrors it in the credit ledger under Vouchers for the liability and the DATEV bookings, and deducts the redemption there from the customer's oldest balance. If there is no matching balance, the invoice is still correct, and a task "Referral credit without a balance" appears under To review.

Credit via push: credit-transactions

If your application manages credit itself, referral rewards for example, you report every movement to POST /v1/credit-transactions, up to 500 per call: { "transactions": [ ... ] }.

Field Required Content
id yes your unique identifier
type yes earn, redeem, reverse, payout or expire
email yes the customer's email address
amount_cents yes amount in cents
occurred_at yes time of the movement
delta_cents for reverse change in cents
order_ref, reverses_id, payout_ref no references to the order, the reversed movement and the payout
hc_booking_date no the date on which your accounting has booked the transaction so far

Repeating a call creates nothing twice. The response lists new (accepted) and already known (duplicates) transactions.

Once a day you report the balances to PUT /v1/credit-balances:

{ "balances": [{ "email": "...", "balance_cents": 1250 }] }

Rechnungskit compares them with the history. If a customer differs, a task is created, so a lost report gets noticed. Both endpoints need the scope orders:write. Bookings only start from the cut-over date you set under Vouchers. The guide is in Guthaben aus dem eigenen Shop übernehmen.

Letting Rechnungskit read the credit history (pull)

Instead of pushing, your shop can provide the history, and Rechnungskit reads it itself. For this, your shop provides two read-only endpoints, protected with a token:

Endpoint in your shop Returns
GET <adresse>/referral-transactions?since=<cursor>&limit=<n> { "items": [...], "next_cursor": "...", "has_more": true }
GET <adresse>/referral-balances?as_of=<YYYY-MM-DD> the balance per customer as [{ "email": "...", "balance_cents": 1250 }]

Each history entry has id, type (earn, redeem, reverse, payout or expire), email, amount_cents, occurred_at and optionally delta_cents (signed), order_ref, reverses_id, payout_ref and hc_booking_date. Entries never change; a correction is a new entry.

Setting up and poking

You enter the address, token and cut-over date under Vouchers in the section "Credit from your own shop". The step-by-step guide with dry run and cut-over date is in Guthaben aus dem eigenen Shop übernehmen.

Rechnungskit reads hourly from where it last stopped and then compares per customer: the balance in the shop against the sum of the history. If a customer differs, you see them there in a list.

A poke makes it faster: POST /api/v1/referral-credits/poke with your API key and no body. Rechnungskit then reads right away. If a poke gets lost, the next hourly run still reads everything.

Dry run and cut-over date

Without a cut-over date, this is a dry run: Rechnungskit reads and compares but books nothing. From the cut-over date, Rechnungskit takes the history into the credit ledger. On the cut-over date it creates an opening balance per customer from the history so far, without a booking, because until then your shop did its own bookings.

After that, Rechnungskit books through the account roles under Settings → DATEV settings:

Movement Booking
Earned credit expense to liability
Reversal the other way round
Payout to the bank
Expiry as income
Refund to credit debtor account (Debitor) to liability

Rechnungskit books a redemption through the invoice (field creditRedemptions), not through the history. Credit that was created before the cut-over date and only became available after it is not booked a second time if hc_booking_date is before the cut-over date.

From the cut-over date, the comparison checks the balance in the shop against the credit ledger. Redemptions whose invoice is still missing are left out of the calculation. If something differs, a task is created. If an account role is missing, it shows up in the DATEV export as a missing mapping.

Public sector buyers: leitweg_id

Besides vat_id, the billing address (billingAddress) also accepts leitweg_id (the Leitweg-ID, the routing ID German public authorities use for e-invoices). If a valid Leitweg-ID is set, the invoice for this order is created as XRechnung 3.0, otherwise as ZUGFeRD. The API answers a formally invalid Leitweg-ID with 422.

You need a phone number in the invoice contact under Settings. Without it, Rechnungskit holds the XRechnung as a task.

Testing

Use your normal API key in test mode. It is the same pipeline, EN 16931 validation and PDF creation included, but with:

  • a test number range (TEST-...),
  • no billing and no sending,
  • no effect on DATEV, OSS and ZM (EC sales list).

Fair use applies, so please no load tests. More in Test mode (sandbox).

Reporting canceled orders

If your shop cancels an order, you report it through POST /v1/orders/{orderNo}/cancel, optionally with reason and cancelledAt. What happens then depends on the state of the order.

Cancellation · responses
What a cancellation does, depending on the state
No document yet
Fully refunded: payment and refund are filed as "canceled + refunded", without a task. If you still have the money, a task is created until the refund arrives
Resultnever a document
Document already exists
The correction comes from the refund at the payment provider as a cancellation invoice
Response409 order_invoiced
Order unknown
Never submitted? Then POST /v1/orders with status "cancelled" and paymentRef
Response404 order_not_found
You may repeat both calls as often as you like.
State of the order What happens
no document yet The order is canceled and never gets a document.
no document, payment fully refunded Rechnungskit files payment and refund as "canceled + refunded". Since there was never any revenue, there is neither a document nor a task.
no document, you still have the money A task is created, and a later refund files the case by itself.
document already exists The endpoint answers with 409 order_invoiced and changes nothing. The correction is created from the refund at the payment provider as a cancellation invoice.
order unknown The cancel call answers with 404 order_not_found.

For an order that was canceled before the payment was completed and therefore never submitted, you send POST /v1/orders with "status": "cancelled" and the paymentRef. Through it, Rechnungskit finds the payment along with the automatic refund and files both. You may repeat both calls as often as you like.

Delivery contract (required for integrators)

Your system must retry failed pushes. In practice:

  1. Every 5xx or network error is transient. Deployments also cause short 502 windows.
  2. Retry with exponential backoff over at least 24 hours.
  3. Also recommended: a daily reconciliation job that resends everything your system has marked as failed.

Retries are always safe: orders are idempotent through orderNo, and the Idempotency-Key header deduplicates parallel deliveries.

A typical failure without retries: the payment arrives (the payment provider retries automatically), but the order push fails on a single 502. The payment then sits as "No order" in review until the order is sent later.

Limits, stated plainly

  • Error responses use the usual codes (400/401/403/404/409/422/429/503), with Retry-After for 429/503. Build in backoff.
  • Requests can be traced by request ID for 30 days in the API activity.

Where to find it in the app

Rechnungskit is not a tax advisory or law firm. This article explains general principles and does not replace advice from a tax advisor (Steuerberater, § 5 StBerG) or a lawyer (§ 3 RDG). Rechnungskit is built for businesses based in Germany and prepares documents, tax rates and bookings automatically. How your specific case is treated remains your decision, ideally together with your tax advisor or a lawyer.

de en