My Factura Public API Review
Purpose
This document provides a comprehensive review of the My Factura (Cash360) Public API that the Membership backend consumes for all payment processing, billing, consumer synchronization, and financial reporting operations. It serves two purposes:
- Reference specification -- The complete current API surface as implemented in Cash360, with all endpoints, data models, statuses, and behaviors documented for Membership developers.
- Improvement proposals -- Analysis of API design issues, naming inconsistencies, missing features, and a proposed redesign for future versions.
Membership integrates with My Factura exclusively through this public API. No internal Cash360 APIs, database access, or code-level dependencies exist.
Current API Specification
Authentication
All requests require an API_KEY header. The API key is entity-specific -- each organization (entity) in Cash360 has its own key that scopes all operations to that entity's data.
Header: API_KEY: <entity-specific-api-key>
There is no OAuth2 flow, no token expiration, and no refresh mechanism. The API key is static until manually rotated by a Cash360 administrator.
Base URL Structure
All endpoints are served under the /api/public path prefix with version identifiers:
| Resource | Base Path | Version |
|---|---|---|
| Consumer | /api/public/p2/v1/consumer |
v1 |
| Transaction | /api/public/p2/v1/transaction |
v1 |
| Payment | /api/public/p2/v1/payment |
v1 |
| Billing Reporting | /api/public/p2/v2/billing-statement-reporting |
v2 |
The p2 segment indicates the public API tier (P2). Cash360 also has P1 and P3 tiers for different access levels, but Membership exclusively uses P2.
Consumer API
The Consumer API manages the lifecycle of consumers (members) within Cash360. Each consumer in Membership must have a corresponding consumer record in Cash360 to enable billing.
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/api/public/p2/v1/consumer |
Create one or more consumers (bulk) |
PUT |
/api/public/p2/v1/consumer/{id} |
Update a consumer by internal Cash360 ID |
GET |
/api/public/p2/v1/consumer/{id} |
Get a consumer by internal Cash360 ID |
GET |
/api/public/p2/v1/consumer/external/{id} |
Get a consumer by external ID |
GET |
/api/public/p2/v1/consumer |
Filter consumers by email or external ID |
GET |
/api/public/p2/v1/consumer/custom-attribute |
Filter consumers by custom attribute |
Consumer Data Model
| Field | Type | Required | Constraints | Notes |
|---|---|---|---|---|
Id |
long | Yes (response) | Auto-generated | Internal Cash360 consumer ID |
IdExternal |
number | No | Entity-unique | External system reference (Membership member ID) |
flgDunningEnabled |
boolean | Yes | Default: true |
Whether dunning is enabled for this consumer |
firstName |
string | Conditional | Required if type = PERSON |
First name |
lastName |
string | Conditional | Required if type = PERSON |
Last name |
type |
enum | Yes | PERSON or COMPANY |
Consumer type |
companyName |
string | Conditional | Required if type = COMPANY |
Company/organization name |
email |
string | No | Entity-unique | Email address |
gender |
enum | No | MALE, FEMALE, OTHER |
Gender |
birthday |
date | No | ISO 8601 date | Date of birth |
street |
string | No | — | Street address |
addressSupplement |
string | No | — | Address line 2 |
postCode |
string | No | — | Postal code |
city |
string | No | — | City |
isoCountry |
enum | No | ISO 3166-1 alpha-2 | Country code |
countryName |
string | No | — | Country name (human-readable) |
isoLanguag |
enum | No | ISO 639-1 | Language code (note: typo in API, missing 'e') |
telephone |
string | No | — | Phone number |
cellphone |
string | No | — | Mobile phone number |
fax |
string | No | — | Fax number |
isBlacklisted |
boolean | No | Default: false |
Blacklist status |
Total fields: 23 (including Id)
Consumer Create Request
The create endpoint accepts an array of consumer objects, enabling bulk creation:
POST /api/public/p2/v1/consumer
API_KEY: <key>
[
{
"IdExternal": 12345,
"firstName": "Max",
"lastName": "Mustermann",
"type": "PERSON",
"email": "max@example.com",
"gender": "MALE",
"birthday": "1990-05-15",
"street": "Hauptstrasse 42",
"postCode": "10115",
"city": "Berlin",
"isoCountry": "DE",
"isoLanguag": "de",
"flgDunningEnabled": true,
"contract": {
"contractNumber": "MBR-2026-0001",
"contractStartDate": "2026-01-01"
},
"bankAccount": {
"iban": "DE89370400440532013000",
"accountOwner": "Max Mustermann",
"sepaMandanteId": "MBR-MNDT-12345",
"sepaMandanteDateOfSigniture": "2025-12-15"
}
}
]
Consumer Update Request
Update is a single consumer operation (not bulk):
PUT /api/public/p2/v1/consumer/42
API_KEY: <key>
{
"firstName": "Maximilian",
"street": "Neue Strasse 7",
"city": "Munich"
}
Consumer Filter Query Parameters
| Endpoint | Parameters | Description |
|---|---|---|
GET /consumer |
email |
Filter by exact email address |
GET /consumer |
externalId |
Filter by external ID |
GET /consumer/custom-attribute |
Attribute-specific | Filter by custom attribute key/value |
Contract Data Model (Embedded in Consumer)
Contract data is embedded within the consumer object during creation and cannot be managed as a separate resource.
| Field | Type | Required | Constraints | Notes |
|---|---|---|---|---|
customAttributes |
Map |
No | — | Key-value pairs for custom data |
contractNumber |
string | No | Entity-unique | Contract reference number |
contractSigningDate |
date | No | ISO 8601 | Date the contract was signed |
contractStartDate |
date | No | ISO 8601 | Contract effective start date |
contractEndDate |
date | No | ISO 8601 | Contract end date |
contractCancellationDate |
date | No | ISO 8601 | Date cancellation was requested |
contractCancellationReason |
string | No | — | Reason for cancellation |
contractCancellationActiveOnDate |
date | No | ISO 8601 | Date cancellation takes effect |
Total fields: 8 (all optional)
Bank Account API
Bank accounts are managed as sub-resources of consumers. They hold SEPA mandate information required for direct debit processing.
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/api/public/p2/v1/consumer/{consumerId}/bank-account |
Create a bank account |
GET |
/api/public/p2/v1/consumer/{consumerId}/bank-account/{id} |
Get by internal ID |
GET |
/api/public/p2/v1/consumer/{consumerId}/bank-account |
Filter bank accounts |
PUT |
/api/public/p2/v1/consumer/{consumerId}/bank-account/{id} |
Update bank account |
PUT |
/api/public/p2/v1/consumer/{consumerId}/bank-account/{id}/archive |
Archive (soft delete) |
PUT |
/api/public/p2/v1/consumer/{consumerId}/bank-account/{id}/restore |
Restore archived |
PUT |
/api/public/p2/v1/consumer/{consumerId}/bank-account/{id}/set-primary |
Set as primary account |
Bank Account Data Model
| Field | Type | Required | Constraints | Notes |
|---|---|---|---|---|
iban |
string | Yes | Valid IBAN | International Bank Account Number |
accountOwner |
string | Yes | — | Name of the account holder |
sepaMandanteId |
string | Yes | System-unique, max 35 chars | SEPA mandate reference (note: typo, should be "Mandate") |
sepaMandanteDateOfSigniture |
date | Yes | ISO 8601 | SEPA mandate signing date (note: typo, should be "Signature") |
bic |
string | No | Valid BIC/SWIFT | Bank Identifier Code |
bankName |
string | No | — | Name of the bank |
flgPrimary |
boolean | No | Default: false |
Whether this is the primary bank account |
idCsrConsumer |
long | No | — | Consumer reference (usually implicit from URL) |
Total fields: 8
Transaction API
The Transaction API manages billing transactions -- the individual charges that Cash360 collects from consumers via SEPA direct debit, manual payment, or other collection methods.
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/api/public/p2/v1/transaction |
Create one or more transactions (bulk) |
GET |
/api/public/p2/v1/transaction/{id} |
Get by internal Cash360 ID |
GET |
/api/public/p2/v1/transaction |
Get by filter |
GET |
/api/public/p2/v1/transaction/external/{id} |
Get by external ID |
PUT |
/api/public/p2/v1/transaction/collection-type |
Update collection type |
Transaction Data Model
| Field | Type | Required | Direction | Notes |
|---|---|---|---|---|
idConsumer |
long | Yes | Request | Cash360 consumer ID |
collectionType |
enum | Yes | Request | DO_NOT_COLLECT, DIRECT_DEBIT, DRAFT |
amount |
BigDecimal | Yes | Request | Gross amount (total to charge) |
dueDate |
date | Yes | Request | Due date (cannot be in the past) |
flgTermination |
boolean | Yes | Request | Default: false. Final transaction for this consumer |
idExternal |
number | No | Both | External system reference |
status |
string | No | Response | Transaction status code |
reason |
string | No | Both | Reason/description for the transaction |
amountNet |
BigDecimal | No | Both | Net amount before VAT |
vatRate |
BigDecimal | No | Both | Primary VAT rate (e.g., 19.00) |
vatAmount |
BigDecimal | No | Both | Primary VAT amount |
vatRate2 |
BigDecimal | No | Both | Secondary VAT rate (e.g., 7.00) |
vatAmount2 |
BigDecimal | No | Both | Secondary VAT amount |
amountDue |
BigDecimal | No | Response | Remaining amount due (after partial payments) |
description |
string | No | Both | Human-readable description |
dunningStatus |
string | No | Response | Current dunning level |
paused |
boolean | No | Both | Whether collection is paused |
pauseStartDate |
date | No | Both | Pause start date |
pauseEndDate |
date | No | Both | Pause end date |
pauseUnpauseReason |
string | No | Both | Reason for pausing/unpausing |
sentToInkassoDateTime |
datetime | No | Response | When sent to debt collection |
createdAt |
datetime | No | Response | Creation timestamp |
updatedAt |
datetime | No | Response | Last modification timestamp |
webhook |
string (URL) | No | Request | Webhook URL for status updates |
Total fields: 24
Transaction Create Request
POST /api/public/p2/v1/transaction
API_KEY: <key>
[
{
"idConsumer": 42,
"collectionType": "DIRECT_DEBIT",
"amount": 29.90,
"amountNet": 25.13,
"vatRate": 19.00,
"vatAmount": 4.77,
"dueDate": "2026-02-01",
"description": "Monthly membership - Adult Gold (Feb 2026)",
"idExternal": 100234,
"webhook": "https://membership.example.com/api/webhooks/cash360/transaction"
},
{
"idConsumer": 43,
"collectionType": "DIRECT_DEBIT",
"amount": 14.90,
"dueDate": "2026-02-01",
"description": "Monthly membership - Youth Basic (Feb 2026)",
"idExternal": 100235,
"webhook": "https://membership.example.com/api/webhooks/cash360/transaction"
}
]
Transaction Status Lifecycle
Status codes:
| Status | Description | Typical Trigger |
|---|---|---|
NEW |
Transaction created, awaiting processing | POST create |
ACCEPTED |
Validated, queued for SEPA export | Cash360 validation |
EXPORTED |
Included in SEPA XML file sent to bank | SEPA batch export |
PAID |
Bank confirmed payment received | Bank settlement report |
SETTLED |
Reconciled in billing statement | Billing cycle close |
FOR_DUNNING |
Overdue, entered dunning workflow | Dunning scheduler |
SHOULD_GO_TO_INKASSO |
Dunning escalated, pending debt collection | Dunning escalation |
SENDING_TO_INKASSO |
Being transmitted to debt collection agency | Operator action |
SENT_TO_INKASSO |
Confirmed received by debt collection | Agency acknowledgment |
RETURNED_FROM_INKASSO |
Case closed by debt collection (paid or written off) | Agency report |
CANCELLED |
Transaction voided | Manual cancellation or storno |
RETURNED |
Bank returned the direct debit (e.g., insufficient funds, closed account) | Bank PAIN.002 report |
REJECTED |
Validation failed (invalid consumer, missing mandate, etc.) | Cash360 validation |
INSTALLMENT |
Converted to installment payment plan | Operator action |
Collection Type Update
PUT /api/public/p2/v1/transaction/collection-type
API_KEY: <key>
{
"idTransaction": 98765,
"collectionType": "DO_NOT_COLLECT"
}
Collection types:
- DO_NOT_COLLECT -- Transaction exists but no automated collection (manual payment expected)
- DIRECT_DEBIT -- Collect via SEPA direct debit from consumer's bank account
- DRAFT -- Draft/pending transaction, not yet submitted for collection
Payment API
The Payment API handles manual payment recording and transaction cancellation (storno).
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/api/public/p2/v1/payment/pay |
Record a payment against a transaction |
PUT |
/api/public/p2/v1/payment/storno |
Cancel (storno) a transaction |
Pay Request
Supports payment by internal transaction ID or external ID. Partial payments are supported.
POST /api/public/p2/v1/payment/pay
API_KEY: <key>
{
"idTransaction": 98765,
"amount": 29.90,
"paymentMethod": "CASH"
}
Or by external ID:
{
"idExternal": 100234,
"amount": 15.00,
"paymentMethod": "CREDIT_CARD"
}
Payment methods: CASH, CREDIT, CREDIT_CARD
Partial payments: If amount is less than the transaction's amountDue, the transaction remains open with a reduced amountDue. Multiple partial payments can be applied until the full amount is settled.
Storno (Cancel) Request
PUT /api/public/p2/v1/payment/storno
API_KEY: <key>
{
"idTransaction": 98765,
"reason": "Duplicate charge"
}
Webhook System
Cash360 sends HTTP POST notifications to a per-transaction webhook URL when transaction statuses change. The webhook URL is specified during transaction creation.
Basic Webhook Payload
POST <webhook-url>
Content-Type: application/json
{
"type": "transaction",
"transactionId": 98765
}
Extended Webhook Payload
The extended payload provides richer context, reducing the need for a follow-up GET call:
POST <webhook-url>
Content-Type: application/json
{
"type": "transaction",
"transactionId": 98765,
"statusCd": "PAID",
"collectionTypeCd": "DIRECT_DEBIT",
"amountDue": 0.00,
"adjustmentDescription": null,
"beneficiaryEntityId": 1,
"paymentMethodCd": "SEPA",
"adjustmentTypeCd": null,
"adjustmentStatusCd": null,
"transactionDunningStatus": null
}
Extended Webhook Fields
| Field | Type | Description |
|---|---|---|
type |
string | Event type (currently always "transaction") |
transactionId |
number | Cash360 internal transaction ID |
statusCd |
string | Current transaction status code |
collectionTypeCd |
string | Collection type code |
amountDue |
decimal | Remaining amount due |
adjustmentDescription |
string | Description of adjustment (if applicable) |
beneficiaryEntityId |
number | ID of the entity that benefits from this transaction |
paymentMethodCd |
string | Payment method used |
adjustmentTypeCd |
string | Type of adjustment (if applicable) |
adjustmentStatusCd |
string | Status of adjustment (if applicable) |
transactionDunningStatus |
string | Current dunning status |
Retry Policy
When webhook delivery fails (non-2xx response or network timeout), Cash360 retries with exponential backoff:
| Attempt | Delay After Failure | Cumulative Wait |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 second | 1s |
| 3 | 2 seconds | 3s |
| 4 | 4 minutes | ~4min |
| 5 | 8 minutes | ~12min |
| 6 | 16 minutes | ~28min |
| 7 | 32 minutes | ~1h |
| 8 | 64 minutes | ~2h |
| 9 | 128 minutes | ~4h |
| 10 | ~22 hours | ~26h |
After 10 failed attempts, the webhook is abandoned. No notification is sent about the failure.
Webhook Resend
Manually trigger webhook resend for specific transactions:
PUT /api/public/p2/v1/transaction/resend-webhook
API_KEY: <key>
{
"transactionIds": [98765, 98766, 98767]
}
Financial Reporting API
Monthly billing statement reports providing aggregated financial data per entity.
Endpoint
| Method | Path | Description |
|---|---|---|
GET |
/api/public/p2/v2/billing-statement-reporting |
Get billing statement reports |
Report Fields
| Field | Type | Description |
|---|---|---|
payoutDate |
date | Date of the payout to the entity |
totalPayoutAmount |
decimal | Total amount paid out |
fees |
decimal | Cash360 processing fees deducted |
directDebitReturnAmount |
decimal | Amount returned due to failed direct debits |
reminderCasesAmount |
decimal | Amount in active dunning/reminder cases |
specialCases |
decimal | Amount in special handling (manual review, disputes) |
pendingAmount |
decimal | Amount still being processed |
preMonthAmount |
decimal | Carryover amount from previous month |
totalExpectedAmount |
decimal | Total expected income for the period |
paymentByPeriod |
object | Breakdown by time period |
Payment By Period Breakdown
| Field | Type | Description |
|---|---|---|
paymentByPeriod.current |
decimal | Payments received in the current billing period |
paymentByPeriod.previous |
decimal | Payments from the previous billing period |
paymentByPeriod.earlier |
decimal | Payments from earlier periods (catch-up) |
Example Response
{
"payoutDate": "2026-02-15",
"totalPayoutAmount": 12450.00,
"fees": 249.00,
"directDebitReturnAmount": 89.70,
"reminderCasesAmount": 450.00,
"specialCases": 125.00,
"pendingAmount": 890.00,
"preMonthAmount": 234.50,
"totalExpectedAmount": 14488.20,
"paymentByPeriod": {
"current": 11200.00,
"previous": 1015.00,
"earlier": 235.00
}
}
API Improvement Proposals
1. Authentication Enhancement
Current state: Static API key passed in the API_KEY header. No expiration, no scoping, no rotation mechanism.
Issues: - API key compromise requires manual rotation and coordinated update across all integrating systems - No fine-grained permission scoping (an API key grants full access to all P2 endpoints) - No rate limiting documentation (unclear if rate limits exist or how they are enforced) - API key is exposed in example documentation (should be redacted)
Proposed improvements:
| Improvement | Priority | Effort |
|---|---|---|
| Document rate limits per API key (requests/minute, requests/day) | P0 | Low |
| Add OAuth2 client credentials flow as an alternative auth method | P1 | Medium |
| Implement API key rotation with overlap period (old key valid for 24h after new key issued) | P1 | Medium |
| Add permission scopes to API keys (read-only, write, admin) | P2 | Medium |
Add X-RateLimit-Remaining and X-RateLimit-Reset response headers |
P2 | Low |
For Membership integration, the static API key is acceptable for v1.0 but should be complemented with OAuth2 client credentials for production deployments. Membership should store the API key in an encrypted secrets vault, never in application configuration files.
2. Versioning Inconsistency
Current state: Consumer, Transaction, and Payment APIs use v1. Billing Reporting uses v2. All use the p2 public tier prefix.
Issues:
- No documentation explaining when versions increment or what constitutes a breaking change
- The v2 on billing reporting suggests a v1 existed but there is no deprecation notice
- The URL structure /api/public/p2/v1/ is verbose and redundant
Proposed improvements:
- Standardize on a single version across all endpoints (bump all to v2 when a breaking change occurs)
- Document the versioning policy: what triggers a new version, how long old versions are supported, deprecation timeline
- Simplify the URL structure: /api/p2/v1/consumers instead of /api/public/p2/v1/consumer
- Add API-Version response header to all responses for programmatic version detection
3. Naming Inconsistencies
Current state: Multiple typos and naming convention inconsistencies exist across the API surface.
Typos
| Current Name | Should Be | Location |
|---|---|---|
sepaMandanteId |
sepaMandateId |
Bank Account |
sepaMandanteDateOfSigniture |
sepaMandateDateOfSignature |
Bank Account |
isoLanguag |
isoLanguage |
Consumer |
These typos are in production and changing them would be a breaking change. They should be fixed in the next major version, with the old names accepted as aliases during a migration period.
Convention Inconsistencies
| Issue | Examples | Recommended Standard |
|---|---|---|
| Boolean prefix inconsistency | flgDunningEnabled, flgPrimary, flgTermination vs. isBlacklisted, paused |
Use is/has prefix consistently: isDunningEnabled, isPrimary, isTermination, isBlacklisted, isPaused |
| Field name case inconsistency | IdExternal (PascalCase) vs. idConsumer (camelCase) |
Use camelCase consistently: idExternal |
| Status field naming | collectionType (in transaction) vs. collectionTypeCd (in webhook) |
Use consistent naming: always collectionType or always collectionTypeCd |
| Resource naming (singular) | /consumer, /transaction |
Use plural: /consumers, /transactions |
4. Missing Features
4.1 Pagination
Current state: List endpoints (GET /consumer, GET /transaction) return results without pagination parameters. There is no documented way to paginate through large result sets.
Impact: A Membership entity with 10,000+ members cannot efficiently retrieve consumer or transaction lists. Responses may time out or consume excessive memory.
Proposed solution:
GET /api/public/p2/v1/consumer?page=0&size=20&sort=lastName,asc
Response envelope:
{
"content": [...],
"page": 0,
"size": 20,
"totalElements": 1234,
"totalPages": 62,
"sort": "lastName,asc"
}
4.2 Sorting and Ordering
Current state: No sort parameters documented on any endpoint.
Proposed solution: Add sort query parameter accepting field name and direction: sort=createdAt,desc. Support multiple sort fields: sort=status,asc&sort=dueDate,desc.
4.3 Webhook Management
Current state: Webhooks are set per-transaction during creation. There is no global webhook configuration, no way to list active webhooks, update URLs, or view delivery history.
Proposed new endpoints:
GET /api/public/p2/v1/webhook -- List registered webhooks
POST /api/public/p2/v1/webhook -- Register a global webhook URL
PUT /api/public/p2/v1/webhook/{id} -- Update webhook URL
DELETE /api/public/p2/v1/webhook/{id} -- Remove webhook
GET /api/public/p2/v1/webhook/{id}/deliveries -- View delivery history
POST /api/public/p2/v1/webhook/{id}/test -- Send test event
4.4 Batch Transaction Status Query
Current state: Transaction status can only be queried one at a time (GET /transaction/{id}). The filter endpoint exists but its capabilities are undocumented.
Proposed solution:
POST /api/public/p2/v1/transaction/batch-status
{
"transactionIds": [98765, 98766, 98767, 98768]
}
Response: array of { id, status, amountDue, updatedAt } objects.
4.5 Consumer Deletion / GDPR Anonymization
Current state: No endpoint exists to delete or anonymize a consumer. The API does not support the GDPR right to erasure.
Proposed new endpoints:
DELETE /api/public/p2/v1/consumer/{id} -- Soft delete (archive)
POST /api/public/p2/v1/consumer/{id}/anonymize -- GDPR anonymization (irreversible)
GET /api/public/p2/v1/consumer/{id}/data-export -- GDPR data export (all data for consumer)
4.6 Health Check
Current state: No health check or status endpoint exists. Integrating systems cannot programmatically verify that the Cash360 API is operational.
Proposed solution:
GET /api/public/p2/v1/health
Response:
{
"status": "UP",
"version": "2.1.0",
"timestamp": "2026-02-22T10:30:00Z"
}
5. Data Model Improvements
5.1 Bulk Create vs. Single Update Asymmetry
Current state: Consumer creation accepts an array (bulk), but consumer update accepts a single object. This is inconsistent and makes batch updates impossible in a single API call.
Proposed solution: Add a batch update endpoint:
PUT /api/public/p2/v1/consumer/batch
[
{ "id": 42, "email": "new@example.com" },
{ "id": 43, "street": "Updated Street 1" }
]
5.2 Contract as Embedded Object
Current state: Contract data is embedded in the consumer object during creation. There is no way to create, update, or query contracts independently.
Issues: - A consumer can have multiple contracts over time (renewal, plan change), but the embedded model implies one contract per consumer - Contract lifecycle operations (renewal, cancellation) must be performed through consumer update - No contract history or versioning
Proposed solution: Elevate contracts to a first-class resource:
POST /api/public/p2/v1/consumer/{id}/contract -- Create contract
GET /api/public/p2/v1/consumer/{id}/contract -- List contracts
GET /api/public/p2/v1/consumer/{id}/contract/{cid} -- Get contract
PUT /api/public/p2/v1/consumer/{id}/contract/{cid} -- Update contract
DELETE /api/public/p2/v1/consumer/{id}/contract/{cid} -- Terminate contract
5.3 Limited Consumer Search
Current state: Consumers can only be filtered by email and external ID. No name-based search exists.
Proposed solution: Add full-text search:
GET /api/public/p2/v1/consumer/search?q=Mustermann&fields=firstName,lastName,email
Also add filter parameters for common queries:
GET /api/public/p2/v1/consumer?city=Berlin&isoCountry=DE&isBlacklisted=false
5.4 VAT Model Limitation
Current state: Transactions support two VAT rates (vatRate/vatAmount and vatRate2/vatAmount2). This is a rigid model that fails when more than two VAT rates apply.
Proposed solution: Replace the dual-field model with a VAT line items array:
{
"amount": 100.00,
"amountNet": 84.03,
"vatEntries": [
{ "rate": 19.00, "amount": 15.13, "description": "Standard VAT" },
{ "rate": 7.00, "amount": 0.84, "description": "Reduced VAT" }
]
}
This supports any number of VAT rates and is extensible for country-specific tax requirements.
5.5 Decimal Precision
Current state: Transaction amounts are described as BigDecimal in the documentation, but JSON has no native decimal type. There is no documentation about expected precision, rounding behavior, or how string vs. number representation should be handled.
Proposed documentation additions:
- All monetary amounts must be transmitted as JSON numbers with exactly 2 decimal places (e.g., 29.90, not 29.9 or 29.900)
- Internal precision: 4 decimal places for intermediate calculations
- Rounding: half-up (standard commercial rounding)
- Currency: assumed EUR unless otherwise specified (no multi-currency support documented)
6. Webhook Improvements
6.1 No Webhook Signature Verification
Current state: Webhook payloads are sent as plain HTTP POST requests with no authentication or integrity verification. A recipient cannot verify that the webhook originated from Cash360.
Impact: Any party that knows the webhook URL can send fake status updates, potentially corrupting the integrating system's transaction state.
Proposed solution: HMAC-SHA256 signature in a header:
X-Cash360-Signature: sha256=<HMAC(webhook_secret, request_body)>
X-Cash360-Timestamp: 1708675200
The receiving system verifies the signature using a shared secret and validates the timestamp is within a 5-minute window to prevent replay attacks.
6.2 Limited Event Types
Current state: The webhook type field is always "transaction". There are no webhooks for consumer changes, billing report availability, SEPA export completion, or system events.
Proposed event types:
| Event Type | Trigger |
|---|---|
transaction.status_changed |
Transaction status change (current functionality) |
transaction.payment_received |
Partial or full payment recorded |
consumer.created |
Consumer created in Cash360 |
consumer.updated |
Consumer data modified |
consumer.archived |
Consumer archived/deactivated |
bank_account.created |
Bank account added |
bank_account.archived |
Bank account deactivated |
billing_report.available |
Monthly billing report ready |
sepa_export.completed |
SEPA XML export file generated |
6.3 Basic vs. Extended Payload
Current state: Two webhook payload formats exist (basic and extended). The basic version contains only type and transactionId, requiring a follow-up GET call for any useful information.
Proposed solution: Deprecate the basic payload. The extended payload should be the only format. The basic payload provides insufficient information for any real-world integration -- every consumer of the basic webhook must immediately make a GET call, doubling API traffic.
6.4 No Consumer Change Webhooks
Current state: Webhooks exist only for transactions. If consumer data is modified directly in Cash360 (e.g., by an operator), the integrating system has no way to know without polling.
Proposed solution: Add consumer lifecycle webhooks (see event types above).
6.5 No Dead Letter Queue or Dashboard
Current state: After 10 failed delivery attempts, the webhook is silently abandoned. No notification, no dashboard, no retry mechanism.
Proposed improvements: - Add a webhook delivery dashboard showing recent deliveries, failures, and retry status - Persist failed webhooks in a dead letter queue accessible via API - Send an email notification to the entity admin when webhook delivery fails repeatedly - Add a bulk resend endpoint for all failed webhooks in a date range
7. Error Handling
7.1 No Standardized Error Response Format
Current state: Error response format is not documented. It is unclear whether errors return HTTP status codes, error bodies, or both. Different endpoints may return different error structures.
Proposed standard error response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Consumer creation failed: 2 of 3 items have validation errors",
"timestamp": "2026-02-22T10:30:00Z",
"requestId": "req-abc-123",
"details": [
{
"index": 0,
"field": "iban",
"code": "INVALID_IBAN",
"message": "The IBAN 'DE123' is not a valid International Bank Account Number"
},
{
"index": 2,
"field": "email",
"code": "DUPLICATE_EMAIL",
"message": "A consumer with email 'max@example.com' already exists"
}
]
}
}
7.2 No Error Code Catalog
Proposed error code catalog:
| Code | HTTP Status | Description |
|---|---|---|
AUTHENTICATION_FAILED |
401 | Invalid or missing API key |
FORBIDDEN |
403 | API key does not have permission for this operation |
NOT_FOUND |
404 | Requested resource does not exist |
VALIDATION_ERROR |
422 | Request body failed validation |
DUPLICATE_ENTRY |
409 | Unique constraint violation (email, external ID, mandate ID) |
INVALID_IBAN |
422 | IBAN checksum validation failed |
INVALID_STATUS_TRANSITION |
422 | Requested status change is not valid from current status |
TRANSACTION_ALREADY_SETTLED |
422 | Cannot modify a settled transaction |
CONSUMER_BLACKLISTED |
422 | Operation not allowed for blacklisted consumer |
PAST_DUE_DATE |
422 | Due date cannot be in the past |
RATE_LIMIT_EXCEEDED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Unexpected server error |
7.3 Bulk Operation Error Behavior
Current state: Unclear whether bulk operations (consumer create, transaction create) are all-or-nothing (atomic) or partial (best-effort). If one item in a batch of 50 fails validation, are the other 49 created?
Proposed behavior: Partial success with detailed reporting:
- Each item in the batch is validated independently
- Successfully validated items are created
- Failed items are returned in the error details array with their batch index
- Response HTTP status: 207 Multi-Status for partial success, 201 Created for full success, 422 Unprocessable Entity for full failure
8. Security Improvements
8.1 API Key Exposure
Current state: API keys appear in plain text in example documentation and Postman collections. No guidance on secure storage.
Proposed improvements:
- Redact all API keys in documentation (use <your-api-key> placeholder)
- Document secure storage recommendations (environment variables, secrets vault)
- Add API key audit log (who used the key, when, from which IP)
8.2 IP Whitelisting
Current state: No IP-based access restriction is documented.
Proposed improvement: Add IP whitelist configuration per API key:
POST /api/admin/api-key/{id}/ip-whitelist
{
"allowedIps": ["203.0.113.10/32", "198.51.100.0/24"]
}
When configured, requests from non-whitelisted IPs are rejected with 403 Forbidden.
8.3 Request/Response Encryption
Current state: TLS is assumed but not explicitly documented. No additional encryption layer for sensitive fields.
Proposed improvements: - Explicitly document TLS 1.2+ requirement - Add certificate pinning documentation for high-security integrations - Consider field-level encryption for IBAN and bank account data in transit (beyond TLS)
9. Proposed API Redesign (Future Version)
The following outlines a complete redesign for a future major version of the My Factura Public API, incorporating all improvements above and modern API design best practices.
9.1 Design Principles
- OpenAPI 3.1 specification -- Machine-readable API contract, auto-generated client SDKs
- RESTful resource naming -- Plural nouns, consistent URL structure
- Standard pagination -- Page, size, sort query parameters on all list endpoints
- Standard error format -- Consistent error envelope with codes, messages, and field-level details
- HMAC-SHA256 webhook signatures -- Cryptographic verification of webhook authenticity
- First-class sub-resources -- Contracts and bank accounts as independently manageable resources
- GDPR compliance -- Data export and anonymization endpoints built in
- OAuth2 authentication -- Client credentials flow for service-to-service communication
9.2 Proposed URL Structure
Authentication: OAuth2 Bearer token or API_KEY header (backward compatible)
# Consumers
GET /api/p2/v2/consumers -- List (paginated)
POST /api/p2/v2/consumers -- Create (single or bulk)
GET /api/p2/v2/consumers/{id} -- Get by ID
PUT /api/p2/v2/consumers/{id} -- Update
DELETE /api/p2/v2/consumers/{id} -- Archive (soft delete)
GET /api/p2/v2/consumers/search -- Full-text search
POST /api/p2/v2/consumers/{id}/anonymize -- GDPR anonymize
GET /api/p2/v2/consumers/{id}/data-export -- GDPR data export
# Contracts (first-class resource)
GET /api/p2/v2/consumers/{id}/contracts -- List contracts
POST /api/p2/v2/consumers/{id}/contracts -- Create contract
GET /api/p2/v2/consumers/{id}/contracts/{cid} -- Get contract
PUT /api/p2/v2/consumers/{id}/contracts/{cid} -- Update contract
POST /api/p2/v2/consumers/{id}/contracts/{cid}/cancel -- Cancel contract
# Bank Accounts
GET /api/p2/v2/consumers/{id}/bank-accounts -- List bank accounts
POST /api/p2/v2/consumers/{id}/bank-accounts -- Create bank account
GET /api/p2/v2/consumers/{id}/bank-accounts/{bid} -- Get bank account
PUT /api/p2/v2/consumers/{id}/bank-accounts/{bid} -- Update bank account
DELETE /api/p2/v2/consumers/{id}/bank-accounts/{bid} -- Archive bank account
POST /api/p2/v2/consumers/{id}/bank-accounts/{bid}/set-primary -- Set primary
# Transactions
GET /api/p2/v2/transactions -- List (paginated, filterable)
POST /api/p2/v2/transactions -- Create (single or bulk)
GET /api/p2/v2/transactions/{id} -- Get by ID
POST /api/p2/v2/transactions/batch-status -- Batch status query
PUT /api/p2/v2/transactions/{id}/collection-type -- Update collection type
# Payments
POST /api/p2/v2/transactions/{id}/payments -- Record payment
POST /api/p2/v2/transactions/{id}/storno -- Cancel transaction
# Webhooks
GET /api/p2/v2/webhooks -- List registered webhooks
POST /api/p2/v2/webhooks -- Register global webhook
PUT /api/p2/v2/webhooks/{id} -- Update webhook
DELETE /api/p2/v2/webhooks/{id} -- Remove webhook
GET /api/p2/v2/webhooks/{id}/deliveries -- Delivery history
POST /api/p2/v2/webhooks/{id}/test -- Send test event
# Reporting
GET /api/p2/v2/billing-reports -- Billing statement reports
# Health
GET /api/p2/v2/health -- Service health check
9.3 Naming Convention Fix Summary
| Current (v1) | Proposed (v2) | Backward Compatibility |
|---|---|---|
sepaMandanteId |
sepaMandateId |
Accept both in v2, remove old in v3 |
sepaMandanteDateOfSigniture |
sepaMandateDateOfSignature |
Accept both in v2, remove old in v3 |
isoLanguag |
isoLanguage |
Accept both in v2, remove old in v3 |
flgDunningEnabled |
isDunningEnabled |
Accept both in v2, remove old in v3 |
flgPrimary |
isPrimary |
Accept both in v2, remove old in v3 |
flgTermination |
isTermination |
Accept both in v2, remove old in v3 |
IdExternal |
idExternal |
Accept both in v2, remove old in v3 |
/consumer (singular) |
/consumers (plural) |
v1 routes remain active during migration |
/transaction (singular) |
/transactions (plural) |
v1 routes remain active during migration |
9.4 Standard Pagination Response
All list endpoints return a paginated envelope:
{
"content": [
{ "id": 1, "firstName": "Max", ... },
{ "id": 2, "firstName": "Anna", ... }
],
"pagination": {
"page": 0,
"size": 20,
"totalElements": 1234,
"totalPages": 62
},
"sort": [
{ "field": "lastName", "direction": "asc" }
]
}
9.5 Standard Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"requestId": "req-abc-123-def",
"timestamp": "2026-02-22T10:30:00Z",
"details": [
{
"field": "iban",
"code": "INVALID_IBAN",
"message": "IBAN checksum validation failed",
"rejectedValue": "DE123"
}
]
}
}
9.6 Webhook Signature
Every webhook delivery includes verification headers:
X-Cash360-Signature: sha256=5d7a9f1e3c4b2a8d6e0f7c9b1a3d5e7f...
X-Cash360-Timestamp: 1708675200
X-Cash360-Event: transaction.status_changed
X-Cash360-Delivery-Id: dlv-abc-123
Verification algorithm:
payload = timestamp + "." + request_body
expected = HMAC-SHA256(webhook_secret, payload)
verify: expected == signature
verify: abs(now - timestamp) < 300 seconds
Membership Integration Strategy
Based on the current API capabilities and the identified limitations, Membership adopts the following integration strategy:
Consumer Synchronization
- When a member is created in Membership, a corresponding consumer is created in Cash360 via
POST /consumerwith the Membership member ID asIdExternal - Member profile updates that affect billing (name, address, email) are propagated to Cash360 via
PUT /consumer/{id} - The Cash360 consumer ID is stored in the Membership
Member.externalBillingIdfield for bidirectional linking - Consumer lookup uses
GET /consumer/external/{id}with the Membership member ID
Transaction Flow
- Membership's billing engine calculates due amounts from active contracts
- Transactions are submitted in bulk via
POST /transactionwith the Membership transaction ID asidExternal - Each transaction specifies a webhook URL pointing to
POST /api/webhooks/cash360/transaction - Status updates arrive via webhook; a polling fallback runs every 15 minutes for reliability
Bank Account Management
- SEPA mandates are collected in Membership and registered in Cash360 via the bank account endpoints
- Membership generates the mandate reference:
MBR-{entityId}-{memberId}-{seq} - The primary bank account is used for direct debit collection
Financial Reporting
- Monthly billing reports are fetched via the reporting endpoint
- Data feeds into Membership's financial dashboard
- Discrepancies between Membership's expected and Cash360's reported amounts trigger alerts
Workarounds for Missing Features
| Missing Feature | Workaround |
|---|---|
| No pagination | Fetch all, cache locally, paginate in Membership |
| No consumer name search | Maintain a local consumer index synced from Cash360 |
| No webhook signatures | Validate webhook source IP + require HTTPS |
| No batch status query | Poll individual transactions, rate-limit to avoid overload |
| No GDPR anonymization | Anonymize in Membership, update Cash360 consumer with anonymized data |
| No health check | Periodic GET on a known consumer ID as a health probe |