Outcome
You have a finalized rental order with a complete rental agreement and at least one valid commercial (H17) or residential (H19) rental claim. The order uses the same submission, review, withdrawal, and acceptance lifecycle as an invoice order.Prerequisites
- A production or sandbox base URL and Bearer key.
- An entitled company UUID on every call when using a Partner key.
- A debtor UUID with sufficient address and notification data.
- The rental agreement facts: property address, monthly Warmmiete, contract conclusion date, and — per unpaid month — the H17 or H19 claim.
- One stable
Idempotency-Keyper logicalPOSTcommand.
Lifecycle context
A rental order follows the same draft →submitted → accepted lifecycle
as an invoice order. The only variant-specific pieces are the singleton
rental_agreement, which the order owns from creation, and the H17/H19
claims whose dates the agreement’s due rule derives. Documents are base64
inline uploads or later multipart uploads below the agreement or claim; they
scan asynchronously and never block finalization.
0. Create or reuse the debtor
Create the reusable debtor first. Each example parses the exact returned UUID; persist it asDEBTOR_ID and use that value in the order request. Reconcile
an ambiguous retry by the exact your_reference; never guess an ID.
import uuid
import requests
PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
DEBTOR_COMMAND_ID = "your-debtor-command-id"
DEBTOR_REFERENCE = "quickstart-debtor-80000000-0000-4000-8000-000000000001"
payload = {
"your_reference": DEBTOR_REFERENCE,
"acting_as": "consumer",
"person": {"salutation": "mx", "first_name": "Alex", "last_name": "Example"},
"addresses": [{"street": "Example Street 12", "postal_code": "10115", "city": "Berlin", "country": "DE", "primary": True}],
}
response = requests.post(
f"{PAYWISE_API_URL}/v2/debtors/",
headers={"Authorization": f"Bearer {PAYWISE_API_KEY}", "Content-Type": "application/json", "Idempotency-Key": DEBTOR_COMMAND_ID},
json=payload,
timeout=(5, 30),
)
if response.status_code != 201:
response.raise_for_status()
raise RuntimeError(f"Expected 201, received {response.status_code}")
debtor_id = str(uuid.UUID(response.json()["id"]))
const payload = {
your_reference: process.env.DEBTOR_REFERENCE,
acting_as: "consumer",
person: { salutation: "mx", first_name: "Alex", last_name: "Example" },
addresses: [{ street: "Example Street 12", postal_code: "10115", city: "Berlin", country: "DE", primary: true }],
};
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/debtors/`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": process.env.DEBTOR_COMMAND_ID },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30000),
});
if (response.status !== 201) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const debtorId = (await response.json()).id;
if (!/^[0-9a-f-]{36}$/i.test(debtorId)) throw new Error("Invalid debtor UUID");
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
class CreateDebtorRequest {
public static void main(String[] args) throws IOException, InterruptedException {
String json = "{\"your_reference\":\"quickstart-debtor-80000000-0000-4000-8000-000000000001\",\"acting_as\":\"consumer\",\"person\":{\"salutation\":\"mx\",\"first_name\":\"Alex\",\"last_name\":\"Example\"},\"addresses\":[{\"street\":\"Example Street 12\",\"postal_code\":\"10115\",\"city\":\"Berlin\",\"country\":\"DE\",\"primary\":true}]}";
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/debtors/"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", System.getenv("DEBTOR_COMMAND_ID"))
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 201) throw new IOException(response.body());
String debtorId = response.body().replaceFirst("(?s).*\\\"id\\\"\\s*:\\s*\\\"([0-9a-fA-F-]{36})\\\".*", "$1");
if (debtorId.equals(response.body())) throw new IOException("Missing debtor UUID");
}
}
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
class CreateDebtorRequest {
static async Task Main() {
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var payload = new {
your_reference = "quickstart-debtor-80000000-0000-4000-8000-000000000001",
acting_as = "consumer",
person = new { salutation = "mx", first_name = "Alex", last_name = "Example" },
addresses = new[] { new { street = "Example Street 12", postal_code = "10115", city = "Berlin", country = "DE", primary = true } }
};
using var request = new HttpRequestMessage(HttpMethod.Post, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/debtors/");
request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
request.Headers.Add("Idempotency-Key", Environment.GetEnvironmentVariable("DEBTOR_COMMAND_ID"));
request.Content = JsonContent.Create(payload);
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var response = await client.SendAsync(request, cancellation.Token);
if ((int)response.StatusCode != 201) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
using var debtor = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var debtorId = debtor.RootElement.GetProperty("id").GetGuid();
}
}
status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
--request POST "$PAYWISE_API_URL/v2/debtors/" \
--header "Authorization: Bearer $PAYWISE_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $DEBTOR_COMMAND_ID" \
--output debtor-created.json --write-out '%{http_code}' \
--data '{"your_reference":"quickstart-debtor-80000000-0000-4000-8000-000000000001","acting_as":"consumer","person":{"salutation":"mx","first_name":"Alex","last_name":"Example"},"addresses":[{"street":"Example Street 12","postal_code":"10115","city":"Berlin","country":"DE","primary":true}]}')"
[ "$status" -eq 201 ] || exit 1
DEBTOR_ID="$(jq -er '.id' debtor-created.json)" || exit 1
1. Create inline
Use one POST/v2/orders/
when the agreement, claim, and files are ready together. The nested operation
is atomic.
import requests
PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
ORDER_COMMAND_ID = "your-order-command-id"
DEBTOR_ID = debtor_id # Parsed from the immediately preceding create response.
payload = {
"type": "rental",
"debtor_id": DEBTOR_ID,
"additional_debtor_ids": [],
"starting_approach": "extrajudicial",
"creditor_obligation_fulfilled": True,
"rental_agreement": {
"property_address": {
"street": "Rental Example Street 8",
"postal_code": "10115",
"city": "Berlin",
"country": "DE"
},
"monthly_rent": {
"value": "1200.00",
"currency": "EUR"
},
"contract_date": "2026-01-10",
"documents": [
{
"type": "rental_agreement",
"filename": "rental-agreement.pdf",
"base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
]
},
"claims": [
{
"type": "receivable",
"your_reference": "RENT-2026-02",
"subject_matter": "Residential rent for February 2026",
"document_date": "2026-02-01",
"is_disputed": False,
"principal_amount": {
"value": "1200.00",
"currency": "EUR"
},
"legal_basis": {
"claim_type_code": "H19"
},
"documents": [
{
"type": "claim_statement",
"filename": "rent-February-2026.pdf",
"base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
]
}
]
}
response = requests.post(
f"{PAYWISE_API_URL}/v2/orders/",
headers={
"Authorization": f"Bearer {PAYWISE_API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": ORDER_COMMAND_ID,
},
json=payload,
timeout=(5, 30),
)
if response.status_code != 201:
response.raise_for_status()
raise RuntimeError(f"Expected 201, received {response.status_code}")
order = response.json()
order_id = order["id"]
matching_claims = [claim for claim in order.get("claims", []) if claim.get("your_reference") == "RENT-2026-02"]
if len(matching_claims) != 1:
raise RuntimeError("Expected exactly one matching claim")
claim_id = matching_claims[0]["id"]
const payload = {
"type": "rental",
"debtor_id": process.env.DEBTOR_ID,
"additional_debtor_ids": [],
"starting_approach": "extrajudicial",
"creditor_obligation_fulfilled": true,
"rental_agreement": {
"property_address": {
"street": "Rental Example Street 8",
"postal_code": "10115",
"city": "Berlin",
"country": "DE"
},
"monthly_rent": {
"value": "1200.00",
"currency": "EUR"
},
"contract_date": "2026-01-10",
"documents": [
{
"type": "rental_agreement",
"filename": "rental-agreement.pdf",
"base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
]
},
"claims": [
{
"type": "receivable",
"your_reference": "RENT-2026-02",
"subject_matter": "Residential rent for February 2026",
"document_date": "2026-02-01",
"is_disputed": false,
"principal_amount": {
"value": "1200.00",
"currency": "EUR"
},
"legal_basis": {
"claim_type_code": "H19"
},
"documents": [
{
"type": "claim_statement",
"filename": "rent-February-2026.pdf",
"base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
]
}
]
};
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/orders/`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYWISE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": process.env.ORDER_COMMAND_ID,
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30000),
});
if (response.status !== 201) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const order = await response.json();
const orderId = order.id;
const matchingClaims = order.claims.filter((claim) => claim.your_reference === payload.claims[0].your_reference);
if (matchingClaims.length !== 1) throw new Error("Expected exactly one matching claim");
const claimId = matchingClaims[0].id;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
class RentalOrderRequest {
static int closingDelimiter(String json, int opening, char open, char close) {
int depth = 0;
boolean inString = false;
boolean escaped = false;
for (int index = opening; index < json.length(); index++) {
char character = json.charAt(index);
if (inString) {
if (escaped) escaped = false;
else if (character == '\\') escaped = true;
else if (character == '\"') inString = false;
} else if (character == '\"') inString = true;
else if (character == open) depth++;
else if (character == close && --depth == 0) return index;
}
throw new IllegalArgumentException("Malformed JSON response");
}
static int closingQuote(String json, int opening) {
boolean escaped = false;
for (int index = opening + 1; index < json.length(); index++) {
char character = json.charAt(index);
if (escaped) escaped = false;
else if (character == '\\') escaped = true;
else if (character == '\"') return index;
}
throw new IllegalArgumentException("Malformed JSON string");
}
static int hexDigit(char character) {
if (character >= '0' && character <= '9') return character - '0';
if (character >= 'a' && character <= 'f') return character - 'a' + 10;
if (character >= 'A' && character <= 'F') return character - 'A' + 10;
throw new IllegalArgumentException("Malformed Unicode escape");
}
static int unicodeEscape(String json, int firstDigit, int closing) {
if (firstDigit + 4 > closing) throw new IllegalArgumentException("Malformed Unicode escape");
int value = 0;
for (int index = firstDigit; index < firstDigit + 4; index++)
value = value * 16 + hexDigit(json.charAt(index));
return value;
}
static String decodeJsonString(String json, int opening, int closing) {
StringBuilder decoded = new StringBuilder();
for (int index = opening + 1; index < closing; index++) {
char character = json.charAt(index);
if (character == '\\') {
if (++index >= closing) throw new IllegalArgumentException("Malformed JSON escape");
char escape = json.charAt(index);
switch (escape) {
case '\"': decoded.append('\"'); break;
case '\\': decoded.append('\\'); break;
case '/': decoded.append('/'); break;
case 'b': decoded.append('\b'); break;
case 'f': decoded.append('\f'); break;
case 'n': decoded.append('\n'); break;
case 'r': decoded.append('\r'); break;
case 't': decoded.append('\t'); break;
case 'u':
char unit = (char) unicodeEscape(json, index + 1, closing);
index += 4;
if (Character.isHighSurrogate(unit)) {
if (index + 6 >= closing || json.charAt(index + 1) != '\\' || json.charAt(index + 2) != 'u')
throw new IllegalArgumentException("Missing low surrogate");
char low = (char) unicodeEscape(json, index + 3, closing);
if (!Character.isLowSurrogate(low)) throw new IllegalArgumentException("Invalid low surrogate");
decoded.appendCodePoint(Character.toCodePoint(unit, low));
index += 6;
} else if (Character.isLowSurrogate(unit)) throw new IllegalArgumentException("Unexpected low surrogate");
else decoded.append(unit);
break;
default: throw new IllegalArgumentException("Unsupported JSON escape");
}
} else {
if (character < 0x20) throw new IllegalArgumentException("Unescaped control character");
if (Character.isHighSurrogate(character)) {
if (index + 1 >= closing || !Character.isLowSurrogate(json.charAt(index + 1)))
throw new IllegalArgumentException("Missing raw low surrogate");
decoded.append(character).append(json.charAt(++index));
} else if (Character.isLowSurrogate(character)) throw new IllegalArgumentException("Unexpected raw low surrogate");
else decoded.append(character);
}
}
return decoded.toString();
}
static String directStringField(String object, String wanted) {
int opening = 0;
while (opening < object.length() && Character.isWhitespace(object.charAt(opening))) opening++;
if (opening >= object.length() || object.charAt(opening) != '{')
throw new IllegalArgumentException("Expected JSON object");
int closing = closingDelimiter(object, opening, '{', '}');
for (int cursor = opening + 1; cursor < closing;) {
while (cursor < closing && (Character.isWhitespace(object.charAt(cursor)) || object.charAt(cursor) == ',')) cursor++;
if (cursor >= closing) break;
if (object.charAt(cursor) != '\"') throw new IllegalArgumentException("Malformed JSON field");
int keyClosing = closingQuote(object, cursor);
String key = decodeJsonString(object, cursor, keyClosing);
cursor = keyClosing + 1;
while (cursor < closing && Character.isWhitespace(object.charAt(cursor))) cursor++;
if (cursor >= closing || object.charAt(cursor++) != ':') throw new IllegalArgumentException("Malformed JSON field");
while (cursor < closing && Character.isWhitespace(object.charAt(cursor))) cursor++;
if (cursor >= closing) throw new IllegalArgumentException("Missing JSON value");
char valueStart = object.charAt(cursor);
if (valueStart == '\"') {
int valueClosing = closingQuote(object, cursor);
if (key.equals(wanted)) return decodeJsonString(object, cursor, valueClosing);
cursor = valueClosing + 1;
} else if (valueStart == '{' || valueStart == '[') {
char valueClose = valueStart == '{' ? '}' : ']';
if (key.equals(wanted)) throw new IllegalArgumentException("Expected string field " + wanted);
cursor = closingDelimiter(object, cursor, valueStart, valueClose) + 1;
} else {
if (key.equals(wanted)) throw new IllegalArgumentException("Expected string field " + wanted);
while (cursor < closing && object.charAt(cursor) != ',') cursor++;
}
}
return null;
}
static String topLevelId(String object) {
String id = object.replaceFirst(
"(?s)^\\s*\\{(?:(?!\\{).)*?\\\"id\\\"\\s*:\\s*\\\"([^\\\"]+)\\\".*$", "$1");
if (id.equals(object)) throw new IllegalArgumentException("Missing top-level id");
return id;
}
static String[] responseIds(String body, String reference) {
if (reference == null) throw new IllegalArgumentException("Missing business reference");
String orderId = topLevelId(body);
java.util.regex.Matcher claims = java.util.regex.Pattern
.compile("\\\"claims\\\"\\s*:\\s*\\[").matcher(body);
if (!claims.find()) throw new IllegalArgumentException("Missing claims collection");
int opening = body.indexOf('[', claims.start());
int closing = closingDelimiter(body, opening, '[', ']');
int matches = 0;
String claimId = null;
for (int cursor = opening + 1; cursor < closing;) {
while (cursor < closing && (Character.isWhitespace(body.charAt(cursor)) || body.charAt(cursor) == ',')) cursor++;
if (cursor >= closing) break;
if (body.charAt(cursor) != '{') throw new IllegalArgumentException("Malformed claim object");
int claimClosing = closingDelimiter(body, cursor, '{', '}');
String claim = body.substring(cursor, claimClosing + 1);
if (reference.equals(directStringField(claim, "your_reference"))) {
matches++;
claimId = directStringField(claim, "id");
if (claimId == null) throw new IllegalArgumentException("Missing direct claim id");
}
cursor = claimClosing + 1;
}
if (matches != 1) throw new IllegalArgumentException("Expected exactly one matching claim");
return new String[] {orderId, claimId};
}
public static void main(String[] args) throws IOException, InterruptedException {
String json = "{\"type\":\"rental\",\"debtor_id\":\"" + System.getenv("DEBTOR_ID") + "\",\"additional_debtor_ids\":[],\"starti"
+ "ng_approach\":\"extrajudicial\",\"creditor_obligation_fulfilled\":true,\"renta"
+ "l_agreement\":{\"property_address\":{\"street\":\"Rental Example Street 8\",\"po"
+ "stal_code\":\"10115\",\"city\":\"Berlin\",\"country\":\"DE\"},\"monthly_rent\":{\"valu"
+ "e\":\"1200.00\",\"currency\":\"EUR\"},\"contract_date\":\"2026-01-10\",\"documents\":"
+ "[{\"type\":\"rental_agreement\",\"filename\":\"rental-agreement.pdf\",\"base64\":\""
+ "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5k"
+ "b2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4K"
+ "ZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3gg"
+ "WzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAow"
+ "MDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAw"
+ "MCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4Ngol"
+ "JUVPRgo=\"}]},\"claims\":[{\"type\":\"receivable\",\"your_reference\":\"RENT-2026-"
+ "02\",\"subject_matter\":\"Residential rent for February 2026\",\"document_date"
+ "\":\"2026-02-01\",\"is_disputed\":false,\"principal_amount\":{\"value\":\"1200.00\""
+ ",\"currency\":\"EUR\"},\"legal_basis\":{\"claim_type_code\":\"H19\"},\"documents\":["
+ "{\"type\":\"claim_statement\",\"filename\":\"rent-February-2026.pdf\",\"base64\":\""
+ "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5k"
+ "b2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4K"
+ "ZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3gg"
+ "WzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAow"
+ "MDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAw"
+ "MCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4Ngol"
+ "JUVPRgo=\"}]}]}";
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/orders/"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", System.getenv("ORDER_COMMAND_ID"))
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 201) throw new IOException(response.body());
String[] ids = responseIds(response.body(), "RENT-2026-02");
String orderId = ids[0];
String claimId = ids[1];
}
}
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
class RentalOrderRequest
{
static async Task Main()
{
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var payload = new {
type = "rental",
debtor_id = Environment.GetEnvironmentVariable("DEBTOR_ID"), additional_debtor_ids = Array.Empty<string>(),
starting_approach = "extrajudicial",
creditor_obligation_fulfilled = true,
rental_agreement = new {
property_address = new {
street = "Rental Example Street 8",
postal_code = "10115",
city = "Berlin",
country = "DE"
},
monthly_rent = new {
value = "1200.00",
currency = "EUR"
},
contract_date = "2026-01-10",
documents = new[] {
new {
type = "rental_agreement",
filename = "rental-agreement.pdf",
base64 = "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
}
},
claims = new[] {
new {
type = "receivable",
your_reference = "RENT-2026-02",
subject_matter = "Residential rent for February 2026",
document_date = "2026-02-01",
is_disputed = false,
principal_amount = new {
value = "1200.00",
currency = "EUR"
},
legal_basis = new {
claim_type_code = "H19"
},
documents = new[] {
new {
type = "claim_statement",
filename = "rent-February-2026.pdf",
base64 = "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
}
}
}
};
using var request = new HttpRequestMessage(HttpMethod.Post,
$"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/orders/");
request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
request.Headers.Add("Idempotency-Key", Environment.GetEnvironmentVariable("ORDER_COMMAND_ID"));
request.Content = JsonContent.Create(payload);
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var response = await client.SendAsync(request, cancellation.Token);
if ((int)response.StatusCode != 201) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
using var order = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var orderId = order.RootElement.GetProperty("id").GetGuid();
var matches = order.RootElement.GetProperty("claims").EnumerateArray()
.Where(claim => claim.GetProperty("your_reference").GetString() == payload.claims[0].your_reference).ToArray();
if (matches.Length != 1) throw new InvalidOperationException("Expected exactly one matching claim");
var claimId = matches[0].GetProperty("id").GetGuid();
}
}
http_status="$(curl --fail-with-body --output order-response.json --write-out '%{http_code}' --silent --show-error --connect-timeout 5 --max-time 30 \
--request POST "$PAYWISE_API_URL/v2/orders/" \
--header "Authorization: Bearer $PAYWISE_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $ORDER_COMMAND_ID" \
--data @- <<JSON
{
"type": "rental",
"debtor_id": "$DEBTOR_ID",
"additional_debtor_ids": [],
"starting_approach": "extrajudicial",
"creditor_obligation_fulfilled": true,
"rental_agreement": {
"property_address": {
"street": "Rental Example Street 8",
"postal_code": "10115",
"city": "Berlin",
"country": "DE"
},
"monthly_rent": {
"value": "1200.00",
"currency": "EUR"
},
"contract_date": "2026-01-10",
"documents": [
{
"type": "rental_agreement",
"filename": "rental-agreement.pdf",
"base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
]
},
"claims": [
{
"type": "receivable",
"your_reference": "RENT-2026-02",
"subject_matter": "Residential rent for February 2026",
"document_date": "2026-02-01",
"is_disputed": false,
"principal_amount": {
"value": "1200.00",
"currency": "EUR"
},
"legal_basis": {
"claim_type_code": "H19"
},
"documents": [
{
"type": "claim_statement",
"filename": "rent-February-2026.pdf",
"base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="
}
]
}
]
}
JSON
)"
[ "$http_status" -eq 201 ] || exit 1
ORDER_ID="$(jq -er '.id' order-response.json)" || exit 1
CLAIM_ID="$(jq -er --arg ref "RENT-2026-02" '[.claims[] | select(.your_reference == $ref)] | if length == 1 then .[0].id else error("expected exactly one matching claim") end' order-response.json)" || exit 1
contract_date is the date the rental contract was concluded. It is not the
move-in date or the beginning of the tenancy. monthly_rent is the recurring
Warmmiete including ancillary-cost prepayments; it provides agreement context
and does not cap or determine an individual claim amount.
2. Let dates derive—or override them deliberately
Whenrental_agreement.due_rule is omitted or null, both H17 and H19 claims
default to the third German business day of the rental month. German business
days are Monday through Friday excluding German federal holidays.
For each H17/H19 claim:
document_datemust be the first day of the claimed month.- Omitted
legal_basis.contract_dateis copied from the agreement. A different supplied value returns400. - Omitted
due_dateis derived from the agreement rule. - Omitted
delay_datebecomes the day after the resolved due date. - An explicit
delay_datemust be later than the resolveddue_date. - Explicit due and delay dates remain claim-level overrides. Patching either
field to
nullrestores server derivation. - A later agreement due-rule change recomputes derived dates and preserves
explicit overrides — with one exception: an explicit
delay_datethat falls on or before the recomputeddue_dateis reset todue_date + 1and stops being an override. Reminders are informational and do not change this rule. - A rental-agreement
contract_date(set inline or through a later agreementPATCH) is written to every rental claim’slegal_basis.contract_date, so an order whose contract date arrives through the agreement satisfies readiness. - Every date is
YYYY-MM-DDin ASCII digits;9999-12-30is the latest accepteddue_date(delay_dateis its successor) and the latest accepted reminderdate/due_date; a later value is400on that field with codeinvalid.
400; a duplicate month returns 409, so patch
the existing claim instead. Other ReceivableClaim entries may coexist with the
rental claims.
3. Build the draft in stages
Create the empty draft first, then use a distinct stable idempotency key for this logical claim command. Preserve the exact key and body for replay, and store the returned claim UUID after verifying its exact business reference.import requests
PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
CREATE_CLAIM_KEY = "your-distinct-create-claim-key"
claim_payload = {
"order_id": "20000000-0000-4000-8000-000000000001",
"type": "receivable",
"your_reference": "RENT-2026-02",
"subject_matter": "Residential rent for February 2026",
"document_date": "2026-02-01",
"is_disputed": False,
"principal_amount": {"value": "1200.00", "currency": "EUR"},
"legal_basis": {"claim_type_code": "H19"},
"documents": [{"type": "claim_statement", "filename": "rent-February-2026.pdf", "base64": "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="}],
}
response = requests.post(
f"{PAYWISE_API_URL}/v2/claims/",
headers={"Authorization": f"Bearer {PAYWISE_API_KEY}", "Content-Type": "application/json", "Idempotency-Key": CREATE_CLAIM_KEY},
json=claim_payload,
timeout=(5, 30),
)
if response.status_code != 201:
response.raise_for_status()
raise RuntimeError(f"Expected 201, received {response.status_code}")
claim = response.json()
if claim.get("your_reference") != claim_payload["your_reference"]:
raise RuntimeError("Claim business reference mismatch")
claim_id = claim["id"]
const claimPayload = {
order_id: "20000000-0000-4000-8000-000000000001",
type: "receivable",
your_reference: "RENT-2026-02",
subject_matter: "Residential rent for February 2026",
document_date: "2026-02-01",
is_disputed: false,
principal_amount: { value: "1200.00", currency: "EUR" },
legal_basis: { claim_type_code: "H19" },
documents: [{ type: "claim_statement", filename: "rent-February-2026.pdf", base64: "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo=" }],
};
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/claims/`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": process.env.CREATE_CLAIM_KEY },
body: JSON.stringify(claimPayload),
signal: AbortSignal.timeout(30000),
});
if (response.status !== 201) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const claim = await response.json();
if (claim.your_reference !== claimPayload.your_reference) throw new Error("Claim business reference mismatch");
const claimId = claim.id;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
class CreateClaimRequest {
static String quoteJsonString(String value) {
if (value == null) throw new IllegalArgumentException("Missing business reference");
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
static String topLevelId(String object) {
String id = object.replaceFirst(
"(?s)^\\s*\\{(?:(?!\\{).)*?\\\"id\\\"\\s*:\\s*\\\"([^\\\"]+)\\\".*$", "$1");
if (id.equals(object)) throw new IllegalArgumentException("Missing top-level claim id");
return id;
}
static String createdClaimId(String body, String reference) {
String referencePattern = "(?s)^\\s*\\{(?:(?!\\{).)*?\\\"your_reference\\\"\\s*:\\s*"
+ java.util.regex.Pattern.quote(quoteJsonString(reference));
if (!java.util.regex.Pattern.compile(referencePattern).matcher(body).find())
throw new IllegalArgumentException("Claim business reference mismatch");
return topLevelId(body);
}
public static void main(String[] args) throws IOException, InterruptedException {
String json = "{\"order_id\":\"20000000-0000-4000-8000-000000000001\",\"type\":\"receivable\",\"your_reference\":\"RENT-2026-02\",\"subject_matter\":\"Residential rent for February 2026\",\"document_date\":\"2026-02-01\",\"is_disputed\":false,\"principal_amount\":{\"value\":\"1200.00\",\"currency\":\"EUR\"},\"legal_basis\":{\"claim_type_code\":\"H19\"},\"documents\":[{\"type\":\"claim_statement\",\"filename\":\"rent-February-2026.pdf\",\"base64\":\"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo=\"}]}";
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/claims/"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", System.getenv("CREATE_CLAIM_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 201) throw new IOException(response.body());
String claimId = createdClaimId(response.body(), "RENT-2026-02");
}
}
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
class CreateClaimRequest {
static async Task Main() {
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var payload = new {
order_id = "20000000-0000-4000-8000-000000000001", type = "receivable",
your_reference = "RENT-2026-02",
subject_matter = "Residential rent for February 2026",
document_date = "2026-02-01",
is_disputed = false,
principal_amount = new { value = "1200.00", currency = "EUR" },
legal_basis = new { claim_type_code = "H19" },
documents = new[] { new { type = "claim_statement", filename = "rent-February-2026.pdf", base64 = "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo=" } }
};
using var request = new HttpRequestMessage(HttpMethod.Post, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/claims/");
request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
request.Headers.Add("Idempotency-Key", Environment.GetEnvironmentVariable("CREATE_CLAIM_KEY"));
request.Content = JsonContent.Create(payload);
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var response = await client.SendAsync(request, cancellation.Token);
if ((int)response.StatusCode != 201) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
using var claim = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
if (claim.RootElement.GetProperty("your_reference").GetString() != payload.your_reference) throw new InvalidOperationException("Claim business reference mismatch");
var claimId = claim.RootElement.GetProperty("id").GetGuid();
}
}
status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
--request POST "$PAYWISE_API_URL/v2/claims/" \
--header "Authorization: Bearer $PAYWISE_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $CREATE_CLAIM_KEY" \
--output claim-created.json --write-out '%{http_code}' \
--data '{"order_id":"20000000-0000-4000-8000-000000000001","type":"receivable","your_reference":"RENT-2026-02","subject_matter":"Residential rent for February 2026","document_date":"2026-02-01","is_disputed":false,"principal_amount":{"value":"1200.00","currency":"EUR"},"legal_basis":{"claim_type_code":"H19"},"documents":[{"type":"claim_statement","filename":"rent-February-2026.pdf","base64":"JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCAyMDAgMTAwXSA+PgplbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDQgL1Jvb3QgMSAwIFIgPj4Kc3RhcnR4cmVmCjE4NgolJUVPRgo="}]}')"
[ "$status" -eq 201 ] || exit 1
CLAIM_ID="$(jq -er --arg ref "RENT-2026-02" 'if .your_reference == $ref then .id else error("claim business reference mismatch") end' claim-created.json)" || exit 1
{
"order_id": "20000000-0000-4000-8000-000000000001",
"type": "receivable",
"your_reference": "COMMERCIAL-RENT-2026-03",
"subject_matter": "Commercial rent for March 2026",
"document_date": "2026-03-01",
"due_date": "2026-03-10",
"delay_date": "2026-03-11",
"is_disputed": false,
"principal_amount": {"value": "2400.00", "currency": "EUR"},
"legal_basis": {"claim_type_code": "H17"}
}
rental_agreement document on the agreement and one relevant
claim_statement on each rental claim. A second primary of the same type
conflicts with 409; delete the old draft document before replacing it.
Agreement documents are optional at finalization, but every claim still needs
a non-empty subject_matter or a supporting document.
4. Finalize or remove the draft
Finalize with an empty-body POST/v2/orders/{id}/finalize/
and a fresh Idempotency-Key. A rental draft is incomplete until it has a
complete property address, positive monthly rent, contract date, and at least
one H17/H19 claim. Missing readiness data returns 422 with public paths such
as rental_agreement.contract_date; invalid commands return 400, and
conflicts return 409.
Read the order after any ambiguous result. Once submitted, it may be withdrawn
while still eligible. If the draft should never be submitted, use
DELETE /v2/orders/{id}/
instead; draft deletion also removes its rental agreement.
Runnable Python workflow
This orchestration proves the sandbox before any write, builds the rental order in stages (empty draft → agreement → claim), uses one generated key per logical POST, and never automatically repeats an ambiguous finalize call.Python workflow
import uuid
import requests
PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
DEBTOR_ID = "40000000-0000-4000-8000-000000000001"
base_url = PAYWISE_API_URL
if not base_url.startswith("https://"):
raise ValueError("PAYWISE_API_URL must be an HTTPS sandbox URL")
headers = {"Authorization": f"Bearer {PAYWISE_API_KEY}"}
probe = requests.get(
f"{base_url}/v2/orders/",
headers=headers,
params={"limit": 1},
timeout=(5, 30),
)
if probe.status_code != 200:
probe.raise_for_status()
raise RuntimeError(f"Expected 200, received {probe.status_code}")
if probe.headers.get("X-Paywise-Environment", "").lower() != "sandbox":
raise RuntimeError("Refusing writes without authenticated sandbox proof")
# Stage 1: an empty rental draft. The order owns its rental agreement from
# creation, so the singleton can be completed in a separate step.
create_key = str(uuid.uuid4())
created = requests.post(
f"{base_url}/v2/orders/",
headers={**headers, "Idempotency-Key": create_key},
json={
"type": "rental",
"debtor_id": DEBTOR_ID,
"starting_approach": "extrajudicial",
"creditor_obligation_fulfilled": True,
"claims": [],
},
timeout=(5, 30),
)
if created.status_code != 201:
created.raise_for_status()
raise RuntimeError(f"Expected 201, received {created.status_code}")
order = created.json()
if order.get("type") != "rental" or order.get("status") != "draft":
raise RuntimeError("Created order is not an editable rental draft")
order_id = order["id"]
# Stage 2: complete the agreement; omitted due_rule keeps the default
# third-business-day derivation for every rental claim.
agreement = requests.patch(
f"{base_url}/v2/orders/{order_id}/rental-agreement/",
headers=headers,
json={
"property_address": {
"street": "Rental Example Street 8",
"postal_code": "10115",
"city": "Berlin",
"country": "DE",
},
"monthly_rent": {"value": "1200.00", "currency": "EUR"},
"contract_date": "2026-01-10",
},
timeout=(5, 30),
)
if agreement.status_code != 200:
agreement.raise_for_status()
raise RuntimeError(f"Expected 200, received {agreement.status_code}")
# Stage 3: one residential rental month; due and delay dates derive from
# the agreement, contract_date is copied from it.
claim_key = str(uuid.uuid4())
claim_created = requests.post(
f"{base_url}/v2/claims/",
headers={**headers, "Idempotency-Key": claim_key},
json={
"order_id": order_id,
"type": "receivable",
"your_reference": f"RENT-2026-02-{order_id}",
"subject_matter": "Residential rent for February 2026",
"document_date": "2026-02-01",
"is_disputed": False,
"principal_amount": {"value": "1200.00", "currency": "EUR"},
"legal_basis": {"claim_type_code": "H19"},
},
timeout=(5, 30),
)
if claim_created.status_code != 201:
claim_created.raise_for_status()
raise RuntimeError(f"Expected 201, received {claim_created.status_code}")
claim = claim_created.json()
expected_reference = f"RENT-2026-02-{order_id}"
if claim.get("your_reference") != expected_reference:
raise RuntimeError("Claim business reference mismatch")
claim_id = claim["id"]
finalize_key = str(uuid.uuid4())
try:
finalized = requests.post(
f"{base_url}/v2/orders/{order_id}/finalize/",
headers={**headers, "Idempotency-Key": finalize_key},
timeout=(5, 30),
)
except requests.Timeout:
recovered = requests.get(
f"{base_url}/v2/orders/{order_id}/",
headers=headers,
timeout=(5, 30),
)
if recovered.status_code != 200:
recovered.raise_for_status()
raise RuntimeError(f"Expected 200, received {recovered.status_code}")
order = recovered.json()
if order.get("status") not in {
"submitted",
"awaiting_client_response",
"accepted",
"rejected",
"withdrawn",
"expired",
}:
raise RuntimeError(
"Finalize outcome remains ambiguous; inspect the draft before deciding whether to retry with the same key"
)
else:
if finalized.status_code != 200:
finalized.raise_for_status()
raise RuntimeError(f"Expected 200, received {finalized.status_code}")
order = finalized.json()
WORKFLOW_RESULT = {"order_id": order_id, "claim_id": claim_id, "status": order["status"]}
Failure and recovery
400 validation_erroron create or claim commands: a mixed H17/H19 family, a claimdocument_datethat is not the first of the month, alegal_basis.contract_datethat differs from the agreement, or adelay_datenot later than the resolveddue_date. Correct the field paths named inerrors[]and issue a new logical command.409on a claim create: a rental claim for that month already exists — patch the existing claim instead of retrying.409on a document create: a primaryrental_agreementorclaim_statementalready exists; delete the old draft document first.422on finalize: the readiness paths (for examplerental_agreement.contract_date,rental_agreement.property_address, orclaims.<id>.subject_matter) name what the draft still lacks. A claim whose storeddelay_dateis not after itsdue_dateis reported asclaims.<id>.delay_datewith codedelay_before_due(delay_date must be after due_date.). The draft stays editable; complete it and finalize with a fresh key.- Finalize timeout,
500, or503: read the order by ID before deciding on a retry. Only an observeddraftjustifies repeating finalize with the same key. 429: wait at least the integer seconds inRetry-After, then retry the same command and key.
Verify
import requests
PAYWISE_API_URL = "https://api-sandbox.paywise.de"
PAYWISE_API_KEY = "pw_sbx_your_api_key"
ORDER_ID = "20000000-0000-4000-8000-000000000001"
response = requests.get(
f"{PAYWISE_API_URL}/v2/orders/{ORDER_ID}/",
headers={"Authorization": f"Bearer {PAYWISE_API_KEY}"},
timeout=(5, 30),
)
if response.status_code != 200:
response.raise_for_status()
raise RuntimeError(f"Expected 200, received {response.status_code}")
const response = await fetch(`${process.env.PAYWISE_API_URL.replace(/\/$/, "")}/v2/orders/${process.env.ORDER_ID}/`, {
method: "GET", headers: { Authorization: `Bearer ${process.env.PAYWISE_API_KEY}` }, signal: AbortSignal.timeout(30000),
});
if (response.status !== 200) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
class ReadRentalOrderRequest {
public static void main(String[] args) throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(System.getenv("PAYWISE_API_URL") + "/v2/orders/" + System.getenv("ORDER_ID") + "/"))
.timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + System.getenv("PAYWISE_API_KEY")).GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException(response.body());
}
}
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class ReadRentalOrderRequest
{
static async Task Main()
{
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(HttpMethod.Get, $"{Environment.GetEnvironmentVariable("PAYWISE_API_URL")}/v2/orders/{Environment.GetEnvironmentVariable("ORDER_ID")}/");
request.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("PAYWISE_API_KEY"));
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var response = await client.SendAsync(request, cancellation.Token);
if ((int)response.StatusCode != 200) throw new HttpRequestException(await response.Content.ReadAsStringAsync());
}
}
http_status="$(curl --fail-with-body --silent --show-error --connect-timeout 5 --max-time 30 \
--request GET "$PAYWISE_API_URL/v2/orders/$ORDER_ID/" \
--header "Authorization: Bearer $PAYWISE_API_KEY" \
--output rental-order.json --write-out '%{http_code}')"
[ "$http_status" -eq 200 ] || exit 1
.type to be rental, .status to be submitted or accepted,
and .rental_agreement.property_address plus at least one claim with
legal_basis.claim_type_code H17 or H19 before recording the workflow
as successful.
Reconcile acceptance by claim UUID
Webhook deliveries are at least once. Onorder.accepted, intersect the event’s
claim_ids with the claim UUIDs stored by exact business reference. For every
match, refetch /v2/claims/{claim_id}/ and trust the claim’s current
order_id and mandate_id, even when the event order differs from the
originally submitted order. The claim UUID remains stable; do not scan orders
or reconstruct merge chains.
