<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://docs.demo.myhandl.de/changelog</id>
    <title>Handl Developer Portal Blog</title>
    <updated>2026-11-12T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://docs.demo.myhandl.de/changelog"/>
    <subtitle>Handl Developer Portal Blog</subtitle>
    <icon>https://docs.demo.myhandl.de/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Which categories have something here]]></title>
        <id>https://docs.demo.myhandl.de/changelog/categories-with-offers</id>
        <link href="https://docs.demo.myhandl.de/changelog/categories-with-offers"/>
        <updated>2026-11-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[New: GET /v1/providers/categories answers which categories and groups have an offer at a place.]]></summary>
        <content type="html"><![CDATA[<p>New: <code>GET /v1/providers/categories</code> answers which categories and groups have an offer at a place.</p>
<!-- -->
<ul>
<li class="">It takes the location filters of <code>GET /v1/providers</code> (<code>country</code>, <code>postalCode</code>, <code>city</code>,
<code>street</code>, <code>lat</code>/<code>lng</code>, <code>radiusKm</code>) and <code>serviceMethod</code>, and answers
<code>{ "categories": ["backwaren", "essen-trinken", …] }</code>: every category with at least one
orderable offer there, and the group each sits in.</li>
<li class="">It matches exactly as <code>GET /v1/providers?category=</code> does, so a home screen can leave out a group
card that would open on an empty list. Works with a publishable key.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Place names in the customer's language, and nearby places first]]></title>
        <id>https://docs.demo.myhandl.de/changelog/place-names-in-your-language</id>
        <link href="https://docs.demo.myhandl.de/changelog/place-names-in-your-language"/>
        <updated>2026-11-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[GET /v1/places/search and GET /v1/places/reverse take a language, and search takes near.]]></summary>
        <content type="html"><![CDATA[<p><code>GET /v1/places/search</code> and <code>GET /v1/places/reverse</code> take a <code>language</code>, and search takes <code>near</code>.</p>
<!-- -->
<ul>
<li class=""><code>language</code> is a two-letter ISO 639-1 code (<code>en</code>, <code>de</code>, …). Place names come back in that
language where the geocoder has one, falling back to English. Left off, names come back as
before (German first, then English).</li>
<li class=""><code>near</code> is an ISO alpha-2 country. Results in that country come first; the others stay in the
list. A bare postcode like <code>24105</code> exists in several countries, and <code>near</code> puts the one the
customer most likely means at the top. Unlike <code>country</code>, it rules nothing out.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Customers can clear finished orders from their list]]></title>
        <id>https://docs.demo.myhandl.de/changelog/clear-finished-orders</id>
        <link href="https://docs.demo.myhandl.de/changelog/clear-finished-orders"/>
        <updated>2026-11-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[POST /v1/customers/me/orders/:id/hide takes a finished order out of GET /v1/customers/me/orders.]]></summary>
        <content type="html"><![CDATA[<p><code>POST /v1/customers/me/orders/:id/hide</code> takes a finished order out of <code>GET /v1/customers/me/orders</code>.</p>
<!-- -->
<ul>
<li class="">Only <code>COMPLETED</code>, <code>CANCELLED</code>, <code>REFUNDED</code> or <code>FAILED</code> orders; an open one answers
<code>409 order_status_conflict</code>. It answers <code>204</code>, and repeating it is harmless.</li>
<li class="">Nothing is deleted. The order is still readable by id, its invoice stays valid, and the
provider and you still see it.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Businesses listed by what they sell]]></title>
        <id>https://docs.demo.myhandl.de/changelog/providers-by-what-they-sell</id>
        <link href="https://docs.demo.myhandl.de/changelog/providers-by-what-they-sell"/>
        <updated>2026-11-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[GET /v1/providers?category= now matches a business by the categories of its active listings.]]></summary>
        <content type="html"><![CDATA[<p><code>GET /v1/providers?category=</code> now matches a business by the categories of its active listings.</p>
<!-- -->
<ul>
<li class="">Before, <code>category</code> was matched against the categories a business stated in its settings,
so a bakery that also sells coffee was found under bakery only. Now it is found under both.</li>
<li class="">Under a <code>category</code> filter, <code>listingCount</code> and <code>priceFromCents</code> count only the offers in that
category, the same way they already did for <code>serviceMethod</code> and location.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Delivery or pickup in one filter]]></title>
        <id>https://docs.demo.myhandl.de/changelog/several-service-methods</id>
        <link href="https://docs.demo.myhandl.de/changelog/several-service-methods"/>
        <updated>2026-11-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[serviceMethod on GET /v1/listings and GET /v1/providers now takes several values.]]></summary>
        <content type="html"><![CDATA[<p><code>serviceMethod</code> on <code>GET /v1/listings</code> and <code>GET /v1/providers</code> now takes several values.</p>
<!-- -->
<ul>
<li class=""><strong><code>?serviceMethod=DELIVERY,AT_CUSTOMER</code></strong> (or the parameter repeated) matches a listing that
offers <strong>any</strong> of them — everything that comes to the customer, goods and services alike.
<code>PICKUP,AT_PROVIDER</code> is everything the customer goes to. One value works as before.</li>
<li class="">An unknown value is still a <code>400</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Right away or at a time]]></title>
        <id>https://docs.demo.myhandl.de/changelog/instant-or-scheduled</id>
        <link href="https://docs.demo.myhandl.de/changelog/instant-or-scheduled"/>
        <updated>2026-11-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A scheduled listing can now also be ordered without a time.]]></summary>
        <content type="html"><![CDATA[<p>A scheduled listing can now also be ordered without a time.</p>
<!-- -->
<ul>
<li class=""><strong><code>instantAllowed</code></strong> on every listing. <code>true</code> only on a <code>SCHEDULED</code> listing, and it means the
customer chooses: leave <code>scheduledFor</code> out of <code>POST /v1/orders</code> (or
<code>POST /v1/customers/me/orders</code>) and the order is <code>INSTANT</code>, with no appointment booked; send
it and the order is <code>SCHEDULED</code> as before. A basket needs a time as soon as one scheduled line
doesn't allow this. See <a class="" href="https://docs.demo.myhandl.de/docs/getting-started#4-place-an-order">Place an order</a>.</li>
<li class=""><strong><code>GET /v1/listings?fulfillment=INSTANT</code></strong> includes these listings.</li>
<li class=""><strong>A quote offer</strong> on such a listing may leave out <code>scheduledFor</code> and <code>durationMinutes</code>.</li>
<li class=""><strong>Moving an <code>INSTANT</code> order</strong> with <code>scheduledFor</code> is refused:
<code>scheduledFor is only valid on an order booked for a time</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Ready for go-live]]></title>
        <id>https://docs.demo.myhandl.de/changelog/go-live</id>
        <link href="https://docs.demo.myhandl.de/changelog/go-live"/>
        <updated>2026-10-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[What changed on the way to go-live.]]></summary>
        <content type="html"><![CDATA[<p>What changed on the way to go-live.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_s7X_" id="api-operations">API operations<a href="https://docs.demo.myhandl.de/changelog/go-live#api-operations" class="hash-link" aria-label="Direct link to API operations" title="Direct link to API operations" translate="no">​</a></h2>
<ul>
<li class=""><strong><code>GET /health</code></strong> answers <code>200 {"status":"ok"}</code> when the API and its database are up and
<code>503</code> otherwise. No <code>/v1</code>, no key, and it never counts against a rate limit or shows in your
usage.</li>
<li class=""><strong>Rate limits are counted once across all servers.</strong> The limits, the <code>X-RateLimit-*</code>
headers and the <code>429</code> body are unchanged — a budget simply no longer resets or multiplies
depending on which server answers. See <a class="" href="https://docs.demo.myhandl.de/docs/error-codes#rate-limits">Rate limits</a>.</li>
<li class=""><strong>Webhook deliveries are sent once.</strong> A retry is claimed before it is sent, so a delivery
never goes out twice when the API runs on several servers.</li>
<li class=""><strong>Request logs and webhook delivery history are kept for 400 days</strong>, longer than the usage
charts' longest range (a year), and deleted after that.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_s7X_" id="api-changes">API changes<a href="https://docs.demo.myhandl.de/changelog/go-live#api-changes" class="hash-link" aria-label="Direct link to API changes" title="Direct link to API changes" translate="no">​</a></h2>
<ul>
<li class=""><strong>Customers can delete their own account</strong> — <code>DELETE /v1/customers/me</code> erases the profile,
addresses, quote requests and push tokens and signs out every device. While an order is
still open it answers <code>409 customer_has_open_orders</code> with the orders in
<code>details.openOrders</code>. See <a class="" href="https://docs.demo.myhandl.de/docs/customer-ordering#deleting-the-account">Deleting the account</a>.</li>
<li class=""><strong>Push notifications to the customer's phone.</strong> Register an Expo push token with
<code>PUT /v1/customers/me/push-token</code> and Handl pushes when the provider accepts, declines,
cancels, changes or completes the order, or sends a quote — in German or English by the
customer's <code>locale</code>. Prefer to push from your own server? Skip the token and use the
<code>order.*</code> webhooks. See <a class="" href="https://docs.demo.myhandl.de/docs/building-a-consumer-app#push-notifications">Push notifications</a>.</li>
<li class=""><strong><code>startWithinWithdrawalPeriod</code></strong> on <code>POST /v1/orders</code>, <code>POST /v1/customers/me/orders</code> and
quote accept records that the customer asked for a service to start inside the withdrawal
period; the order answers it as <code>withdrawalWaiverAt</code>. Services only — on goods it is a <code>400</code>.
See <a class="" href="https://docs.demo.myhandl.de/docs/getting-started#starting-a-service-inside-the-withdrawal-period">Starting a service inside the withdrawal period</a>.</li>
<li class=""><strong><code>VARIABLE</code> pricing is refused</strong> on listing create and update (<code>400</code>); use <code>FIXED</code>, or
<code>QUOTE</code> to price per request.</li>
<li class=""><strong>An order belonging to another provider is a <code>404</code></strong>, like any other order you can't see.
The <code>order_not_yours</code> code is gone.</li>
<li class=""><strong>Applications need <code>privacyAccepted: true</code></strong> — <code>POST /v1/partner-applications</code> and
<code>POST /v1/provider-applications</code> answer <code>400</code> without it. Rejected applications are
deleted six months after the decision.</li>
<li class=""><strong>Opening hours don't gate orders.</strong> An <code>INSTANT</code> order is taken at any hour; show
<code>provider.openingHours</code> if it matters to your customer.</li>
<li class=""><strong>Repeating diary exceptions.</strong> A dated availability window may carry an <code>rrule</code>
(<code>FREQ=WEEKLY;BYDAY=…</code>, <code>FREQ=MONTHLY</code>, <code>FREQ=YEARLY</code>, with <code>UNTIL</code> or <code>COUNT</code>) and then
applies on every occurrence — in the portal and on
<code>PUT /v1/sandbox/providers/:id/listings/:listingId/availability</code>. See <a class="" href="https://docs.demo.myhandl.de/docs/sandbox">Sandbox</a>.</li>
<li class=""><strong>Providers can connect an external calendar</strong> by its iCal address; its events are
synced every 15 minutes and those hours stop being offered in <code>availability</code>. Nothing
changes in the API you call.</li>
<li class=""><strong>Webhook URLs must be public https</strong> — in the sandbox too, since one URL serves both
environments. <code>POST /v1/partners/me/webhook</code> answers <code>400</code> for <code>http://</code>, a port other than
443, credentials in the URL, <code>localhost</code> or a private address. Deliveries check the resolved
address on every attempt and no longer follow redirects. For local testing, use an https
tunnel. See <a class="" href="https://docs.demo.myhandl.de/docs/webhooks#what-the-url-must-be">What the URL must be</a>.</li>
<li class=""><strong>Ready for pickup.</strong> A <code>PICKUP</code> order the provider has accepted can be marked ready to
collect: the order gains <code>readyAt</code>, the new webhook
<a class="" href="https://docs.demo.myhandl.de/docs/webhooks#orderready_for_pickup"><code>order.ready_for_pickup</code></a> is sent once, and a
customer with a registered push token is told. The status stays <code>IN_PROGRESS</code>.
<code>GET /v1/customers/me/orders</code> carries <code>readyAt</code> and <code>paidAt</code> too.</li>
<li class=""><strong><code>paidAt</code></strong> on every order: when the payment succeeded and the order became <code>CONFIRMED</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[An appointment lasts as long as what was bought]]></title>
        <id>https://docs.demo.myhandl.de/changelog/appointment-length</id>
        <link href="https://docs.demo.myhandl.de/changelog/appointment-length"/>
        <updated>2026-10-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How long an appointment runs now comes from what the customer buys: each price of a service]]></summary>
        <content type="html"><![CDATA[<p>How long an appointment runs now comes from what the customer buys: each price of a service
states its working time, the basket's lines are summed, and the availability endpoints
answer with the starts where that whole job fits. A job longer than the provider's day runs
on over their next open hours. See <a class="" href="https://docs.demo.myhandl.de/docs/customer-ordering#picking-a-time">Picking a time</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_s7X_" id="breaking">Breaking<a href="https://docs.demo.myhandl.de/changelog/appointment-length#breaking" class="hash-link" aria-label="Direct link to Breaking" title="Direct link to Breaking" translate="no">​</a></h2>
<ul>
<li class=""><code>ListingDto.durationMinutes</code> is removed.</li>
<li class=""><code>ListingPriceDto</code> gains <code>minutesPerUnit</code> (1–2400: the time for one unit — up to a 40-hour
week — or the whole job when the price has no unit) and <code>setupMinutes</code> (counted once per
appointment). Every price of a fixed-price service listing needs <code>minutesPerUnit</code>; goods
carry neither.</li>
<li class=""><code>AvailabilitySlotDto</code> gains <code>segments</code>, and its <code>endsAt</code> is now when the <strong>job</strong> finishes,
not the end of a grid cell. A window's <code>slotDurationMinutes</code> is only the grid starts sit on.</li>
<li class="">Order DTOs gain <code>appointmentSegments: { startsAt, endsAt }[]</code>, empty when there is no
appointment.</li>
<li class="">A quote offer on a <code>SCHEDULED</code> listing needs <code>durationMinutes</code> (5–1440) beside
<code>scheduledFor</code>.</li>
</ul>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_s7X_" id="new">New<a href="https://docs.demo.myhandl.de/changelog/appointment-length#new" class="hash-link" aria-label="Direct link to New" title="Direct link to New" translate="no">​</a></h2>
<ul>
<li class=""><code>POST /v1/availability</code> (publishable key allowed): the basket's <code>items</code>, as
<code>POST /v1/orders</code> takes them, plus optional <code>from</code>/<code>to</code>/<code>serviceMethod</code>; answers the starts
where the whole basket fits on every scheduled listing in it. Use it instead of
intersecting diaries in your app.</li>
<li class=""><code>?items=&lt;listingPriceId&gt;:&lt;quantity&gt;,…</code> and <code>?serviceMethod=</code> on
<code>GET /v1/listings/:id/availability</code>.</li>
<li class=""><code>durationMinutes</code> on quote offers, and the order that accepting one creates runs that long.</li>
<li class=""><code>GET /v1/sandbox/quote-requests/:id/availability?durationMinutes=</code> (sandbox key): the free
starts for an offer of that length, so you can answer a scheduled request as its provider.</li>
<li class="">Travel time: a provider who comes to the customer can keep a fixed time free before each
appointment. It is never part of the price or of <code>segments</code>, but it can make a start
unavailable.</li>
<li class="">Moving an order keeps its length and replaces its <code>appointmentSegments</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Price on request: quote requests ending in an order]]></title>
        <id>https://docs.demo.myhandl.de/changelog/quotes</id>
        <link href="https://docs.demo.myhandl.de/changelog/quotes"/>
        <updated>2026-10-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Listings can now be priced on request (priceType: "QUOTE", no prices). The customer asks,]]></summary>
        <content type="html"><![CDATA[<p>Listings can now be priced on request (<code>priceType: "QUOTE"</code>, no <code>prices</code>). The customer asks,
the provider answers with a fixed-price offer, and accepting it creates an ordinary order
waiting for payment. See <a class="" href="https://docs.demo.myhandl.de/docs/quotes">Price on request</a>.</p>
<ul>
<li class="">New: <code>POST/GET /v1/customers/me/quote-requests[/:id]</code>, <code>…/:id/accept</code> (<code>{ offerId }</code>,
returns the order and its <code>clientSecret</code>) and <code>…/:id/decline</code>.</li>
<li class="">New: <code>GET /v1/quote-requests[/:id]</code> with your secret key (<code>orders:read</code>).</li>
<li class="">New webhooks: <code>quote.offered</code>, <code>quote.withdrawn</code>, <code>quote.rejected</code>, <code>quote.expired</code>.</li>
<li class="">New sandbox routes: <code>POST /v1/sandbox/quote-requests/:id/offer</code> and <code>…/reject</code>.</li>
<li class="">Ordering a QUOTE listing directly is now <code>409 listing_requires_quote</code>; a request or offer
that moved on is <code>409 quote_status_conflict</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Customers can cancel until the provider accepts]]></title>
        <id>https://docs.demo.myhandl.de/changelog/refunds</id>
        <link href="https://docs.demo.myhandl.de/changelog/refunds"/>
        <updated>2026-10-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A customer can now cancel an order they have already paid for, as long as the provider]]></summary>
        <content type="html"><![CDATA[<p>A customer can now cancel an order they have already paid for, as long as the provider
hasn't accepted it yet. Refunds made outside Handl now reach your webhook too. See
<a class="" href="https://docs.demo.myhandl.de/docs/customer-ordering#reading-orders-back">Reading orders back</a> and
<a class="" href="https://docs.demo.myhandl.de/docs/webhooks#ordercancelled"><code>order.cancelled</code></a>.</p>
<ul>
<li class=""><code>POST /v1/customers/me/orders/:id/cancel</code> now also takes a <code>CONFIRMED</code> order. The
customer gets back everything not already refunded, and the order ends at <code>REFUNDED</code>.
From <code>IN_PROGRESS</code> on it is still a <code>409</code>.</li>
<li class="">A paid order cancelled this way fires <code>order.cancelled</code>, with <code>amountCents</code> and
<code>refundedAmountCents</code> like <code>order.refunded</code>. No separate <code>order.refunded</code> follows.</li>
<li class="">A refund made by hand in the payment dashboard is now recorded on the order and fires
<code>order.refunded</code> like any other refund, with its share of VAT, commission and your
partner share accounted for.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Invite and remove your team yourself]]></title>
        <id>https://docs.demo.myhandl.de/changelog/team</id>
        <link href="https://docs.demo.myhandl.de/changelog/team"/>
        <updated>2026-10-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The owner of a partner account can now invite and remove the people who sign in to the]]></summary>
        <content type="html"><![CDATA[<p>The owner of a partner account can now invite and remove the people who sign in to the
partner console, from <strong>Settings</strong>. See
<a class="" href="https://docs.demo.myhandl.de/docs/partner-console#settings-your-team-and-closing-the-account">Settings: your team</a>.</p>
<ul>
<li class="">An invitation mails the person a link to set a password. Everyone on the team keeps the
same rights; only the owner invites, removes and hands ownership on.</li>
<li class=""><code>POST /v1/partners/me/users { email }</code> and <code>DELETE /v1/partners/me/users/:uid</code> are
console-session only: an API key gets <code>403 partner_session_required</code>.</li>
<li class="">New error: <code>409 cannot_remove_self</code> — the owner can't remove themselves; hand ownership on
first.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Closing your account]]></title>
        <id>https://docs.demo.myhandl.de/changelog/account-closing</id>
        <link href="https://docs.demo.myhandl.de/changelog/account-closing"/>
        <updated>2026-10-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Your account's owner can now close it from Settings in the partner console. Closing stops every]]></summary>
        <content type="html"><![CDATA[<p>Your account's owner can now close it from Settings in the partner console. Closing stops every
key, customer token and webhook at once; Handl can restore the account for 30 days, after which
its data is erased. See <a class="" href="https://docs.demo.myhandl.de/docs/closing-your-account">Closing your account</a>.</p>
<ul>
<li class="">A closed account answers every request with <code>403 account_closed</code>, carrying <code>closedAt</code> and
<code>purgeAfter</code>.</li>
<li class="">The <code>REVOKED</code> partner status is gone; <code>api_key_suspended</code> now means <code>SUSPENDED</code> only.</li>
<li class="">New error codes: <code>account_closed</code>, <code>account_has_open_business</code>, <code>not_account_owner</code>,
<code>confirmation_mismatch</code>, <code>recent_sign_in_required</code>, <code>account_not_closed</code>, <code>account_purged</code>,
<code>owner_replacement_required</code>, <code>owner_is_only_user</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Every provider has a category]]></title>
        <id>https://docs.demo.myhandl.de/changelog/provider-categories</id>
        <link href="https://docs.demo.myhandl.de/changelog/provider-categories"/>
        <updated>2026-10-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A provider now has to be filed under at least one category before it is visible to partners,]]></summary>
        <content type="html"><![CDATA[<p>A provider now has to be filed under at least one category before it is visible to partners,
alongside its address, service types and payout account. See <a class="" href="https://docs.demo.myhandl.de/docs/getting-started#the-business-behind-a-listing">What a provider always
has</a>.</p>
<ul>
<li class=""><code>categories</code> on a provider is never empty.</li>
<li class=""><code>provider_setup_incomplete</code> now also covers a provider with no category.</li>
<li class="">A sandbox provider's categories are the ones its listings are filed under. Created with no
listings, it answers <code>404</code> until you add its first offer. See <a class="" href="https://docs.demo.myhandl.de/docs/sandbox#your-own-test-businesses">Your own test
businesses</a>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Partner console]]></title>
        <id>https://docs.demo.myhandl.de/changelog/partner-console</id>
        <link href="https://docs.demo.myhandl.de/changelog/partner-console"/>
        <updated>2026-10-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Your team can now sign in to a partner console: earnings and payouts per order with CSV export,]]></summary>
        <content type="html"><![CDATA[<p>Your team can now sign in to a partner console: earnings and payouts per order with CSV export,
orders, API keys (rotate and stop the previous key), webhook deliveries with redeliver and a test
event, and usage — for live and sandbox.</p>
<ul>
<li class="">New webhook event type <code>webhook.test</code>.</li>
<li class=""><code>GET /v1/partners/me/usage</code> behaves as before with a key; requests made by the console are not
counted.</li>
<li class=""><code>GET /v1/orders</code> accepts <code>sort=desc</code> (default stays <code>asc</code>).</li>
<li class="">New error codes: <code>partner_session_required</code>, <code>live_key_not_active</code>, <code>no_previous_key</code>,
<code>webhook_not_configured</code>.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Every order now pays three parties]]></title>
        <id>https://docs.demo.myhandl.de/changelog/partner-revenue-share</id>
        <link href="https://docs.demo.myhandl.de/changelog/partner-revenue-share"/>
        <updated>2026-10-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Until now an order paid the provider and Handl — the partner app that brought the]]></summary>
        <content type="html"><![CDATA[<p>Until now an order paid the provider and Handl — the partner app that brought the
customer in got nothing for it. Every order now splits three ways: provider, partner,
Handl, and a partner can set up payouts the same way a provider already does.</p>
<!-- -->
<p><strong>What changed for you</strong></p>
<ul>
<li class="">Your app's revenue share (<code>revenueShareBps</code> on <code>GET /v1/partners/me</code>, 20% by default)
is now paid out automatically from every live order, as a Stripe transfer out of the
same charge the customer was billed on.</li>
<li class="">Three new endpoints set it up: <code>POST /v1/partners/me/payouts/onboarding</code>,
<code>GET /v1/partners/me/payouts</code>, <code>GET /v1/partners/me/payouts/dashboard-link</code>. See
<a class="" href="https://docs.demo.myhandl.de/docs/getting-paid">Getting paid</a>.</li>
<li class=""><strong>A live order is refused with 409 <code>partner_payouts_not_set_up</code> until you finish
onboarding</strong>, unless your share is 0%. Sandbox never checks this.</li>
<li class="">A new webhook, <code>partner_share.transferred</code>, fires when your share lands — simulated
in sandbox, a real transfer in live.</li>
<li class="">Refunds and disputes reduce your share proportionally, the same way they already
reduce the provider's.</li>
<li class="">None of this is visible to the customer who paid — <code>partnerShareBps</code>,
<code>partnerShareCents</code> and <code>partnerTransferStatus</code> are partner- and provider-facing only.</li>
</ul>
<p>See <code>adr/0020-three-way-revenue-split.md</code> for the full design.</p>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cancelling an invoiced order, and appointments in the past]]></title>
        <id>https://docs.demo.myhandl.de/changelog/cancel-and-past-appointments</id>
        <link href="https://docs.demo.myhandl.de/changelog/cancel-and-past-appointments"/>
        <updated>2026-10-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Two fixes to how orders are placed and cancelled.]]></summary>
        <content type="html"><![CDATA[<p>Two fixes to how orders are placed and cancelled.</p>
<ul>
<li class=""><strong><code>POST …/orders/:id/cancel</code> works on an order paid through an invoice.</strong> It used to try to
cancel the invoice's PaymentIntent, which Stripe refuses; it now voids the invoice, which
cancels the PaymentIntent with it. An invoice that is already void is accepted as is, and one
Stripe reports paid answers <code>409 order_status_conflict</code> — refund it instead.</li>
<li class=""><strong><code>scheduledFor</code> in the past is a <code>400</code></strong>, when an order is placed and when it is changed,
even if that hour was free. The availability routes never offered one.</li>
</ul>
<!-- -->
<p>A refused <code>scheduledFor</code> now names the picker to read: the listing's
<code>GET /v1/listings/:id/availability</code> when an order is placed, the order's own
<code>…/orders/:id/availability</code> when one is moved.</p>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Service areas can be a place's outline]]></title>
        <id>https://docs.demo.myhandl.de/changelog/outlined-service-areas</id>
        <link href="https://docs.demo.myhandl.de/changelog/outlined-service-areas"/>
        <updated>2026-10-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A service area can now name a place by placeId (from GET /v1/places/search): we store]]></summary>
        <content type="html"><![CDATA[<p>A service area can now name a place by <code>placeId</code> (from <code>GET /v1/places/search</code>): we store
that place's outline, and only addresses inside it can order. "Ibiza" means the island,
not Spain.</p>
<!-- -->
<ul>
<li class=""><code>UpsertServiceAreaDto.placeId</code> — new. We fill in <code>city</code> and <code>region</code> from the place.
Cannot be combined with <code>postalCodePrefixes</code> or a radius (400). If the place can't be
looked up right now the save is refused with <strong>503</strong> — try again.</li>
<li class=""><code>ServiceAreaDto</code> gains <code>placeId</code> and <code>hasOutline</code>. The polygon itself is at
<code>GET /v1/places/:id/boundary</code>.</li>
<li class=""><code>GET /v1/listings</code>, <code>GET /v1/providers</code> and <code>GET /v1/listings/:id/service-area-check</code>
take an optional <code>street</code>, so the check asks exactly the question an order will.</li>
<li class="">Orders place a delivery address by its street (<code>deliveryAddress.line1</code>), falling back to
the postcode. If the address can't be placed at all, an outlined area falls back to its
city, or to its whole country.</li>
</ul>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Fewer categories, scoped to renters]]></title>
        <id>https://docs.demo.myhandl.de/changelog/tenant-taxonomy</id>
        <link href="https://docs.demo.myhandl.de/changelog/tenant-taxonomy"/>
        <updated>2026-10-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[GET /v1/categories now returns five groups and thirty-one categories, down from nine and]]></summary>
        <content type="html"><![CDATA[<p><code>GET /v1/categories</code> now returns five groups and thirty-one categories, down from nine and
sixty-one. For launch the catalogue covers what someone in a rented flat orders and pays for
themselves; work the landlord commissions — building, heating, roofing, garden, energy — is
gone.</p>
<!-- -->
<p><strong>What changed for you</strong></p>
<ul>
<li class=""><strong>Groups:</strong> Essen, Reinigung, Reparatur &amp; Montage (Handwerk and Haushalt merged),
Umzug &amp; Entsorgung (formerly Transport), Technik im Haus. The groups <code>bau</code>, <code>garden</code>,
<code>energy</code> and <code>household</code> no longer exist.</li>
<li class=""><strong>Renamed slugs:</strong> <code>cleaning-construction</code> → <code>cleaning-moveout</code> (Umzugs- &amp; Endreinigung),
<code>household-handyman</code> → <code>handwerk-handyman</code>, <code>household-furniture</code> → <code>handwerk-furniture</code>,
<code>household-pest</code> → <code>handwerk-pest</code>, <code>household-clearance</code> → <code>transport-clearance</code>.
Category ids did not change.</li>
<li class=""><strong>Deleted categories:</strong> everything under the four removed groups, plus
<code>cleaning-office</code>, <code>handwerk-heating</code>, <code>-carpentry</code>, <code>-flooring</code>, <code>-roofing</code>, <code>-metal</code>,
<code>-glazing</code>, <code>household-other</code>, <code>tech-smarthome</code> and <code>tech-security</code>. A <code>?category=</code>
filter on one of them now simply matches nothing, and filing a listing under one of
their ids is refused.</li>
<li class=""><strong>Existing rows were re-filed, not lost.</strong> A listing, order or provider tag on a deleted
category now names the nearest kept one: <code>handwerk-heating</code> → <code>handwerk-plumbing</code>,
<code>tech-smarthome</code> → <code>tech-tv</code>, <code>tech-security</code> → <code>tech-other</code>, <code>cleaning-office</code> →
<code>cleaning-other</code>, everything else → <code>handwerk-other</code>.</li>
</ul>
<p>If you cached category ids or slugs, refresh them from <code>GET /v1/categories</code>.</p>]]></content>
        <category label="v1" term="v1"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Orders can be corrected after they are placed]]></title>
        <id>https://docs.demo.myhandl.de/changelog/2026/09/25/order-changes</id>
        <link href="https://docs.demo.myhandl.de/changelog/2026/09/25/order-changes"/>
        <updated>2026-09-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A customer, their partner app and the provider can now correct an order's address, billing]]></summary>
        <content type="html"><![CDATA[<p>A customer, their partner app and the provider can now correct an order's address, billing
address and appointment.</p>
<ul>
<li class=""><code>PATCH /v1/customers/me/orders/:id</code> and <code>PATCH /v1/orders/:id</code> while the order is <code>PENDING_PAYMENT</code>
or <code>CONFIRMED</code>; the provider, from the portal and Handl Orders, while it is <code>CONFIRMED</code> or <code>IN_PROGRESS</code>.</li>
<li class="">The checks of placing an order apply again, and the tax country may not move.</li>
<li class="">An unpaid order whose invoice address changed gets a new invoice: <code>paymentSecretChanged: true</code>
means fetch the payment secret again.</li>
<li class="">New webhook <code>order.updated</code>; every order read carries <code>changes</code>, newest first.</li>
<li class=""><code>GET /v1/customers/me/orders/:id/availability</code> and <code>GET /v1/orders/:id/availability</code> list the
times an appointment can move to, counting the order's own booking as free.</li>
</ul>
<!-- -->
<p>See <a class="" href="https://docs.demo.myhandl.de/docs/customer-ordering#changing-an-order">Customer ordering</a> for the rules and the errors.</p>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Four service methods, not two, and a service listing has to say how long]]></title>
        <id>https://docs.demo.myhandl.de/changelog/service-methods-say-where</id>
        <link href="https://docs.demo.myhandl.de/changelog/service-methods-say-where"/>
        <updated>2026-09-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[ServiceMethod used to be PICKUP/DELIVERY, with an empty serviceMethods array]]></summary>
        <content type="html"><![CDATA[<p><code>ServiceMethod</code> used to be <code>PICKUP</code>/<code>DELIVERY</code>, with an empty <code>serviceMethods</code> array
standing in for "on site" — a Dienstleistung had no method of its own, just the absence
of one. That's gone: every listing now names at least one of four methods, and a service
listing has to state how long the job takes.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_s7X_" id="what-changed">What changed<a href="https://docs.demo.myhandl.de/changelog/service-methods-say-where#what-changed" class="hash-link" aria-label="Direct link to What changed" title="Direct link to What changed" translate="no">​</a></h2>
<p><code>ServiceMethod</code> gains two values, <code>AT_CUSTOMER</code> and <code>AT_PROVIDER</code>. Goods keep <code>PICKUP</code>
and <code>DELIVERY</code>; a service names one or both of <code>AT_CUSTOMER</code>/<code>AT_PROVIDER</code>. A listing's
<code>serviceMethods</code> can never be empty and can never mix a goods method with a service
method — every listing that used to answer <code>[]</code> became <code>["AT_CUSTOMER"]</code>, since every
surface that showed that listing as "on site" meant the customer's own place.</p>
<p><code>Order.serviceMethod</code> is likewise never null. A past order with no method recorded also
became <code>AT_CUSTOMER</code>; it carries no <code>deliveryAddress</code>, only the billing address taken at
checkout, so the provider portal's route endpoint falls back to that billing address for
an <code>AT_CUSTOMER</code> order with nothing else to route to.</p>
<p><code>AT_CUSTOMER</code> works exactly like <code>DELIVERY</code> did: it requires <code>deliveryAddress</code> and is
checked against the provider's service area at order time, the same 400 either one gets
outside it. <code>AT_PROVIDER</code> works like <code>PICKUP</code> — no address, refused if one is sent.</p>
<p>A service listing (<code>AT_CUSTOMER</code>/<code>AT_PROVIDER</code>) now requires <code>durationMinutes</code>; a goods
listing (<code>PICKUP</code>/<code>DELIVERY</code>) must not carry one. This is checked on every save, not only
on publish — a migrated listing that predates the rule and has no duration is exempt from
just that part, so it can still be taken offline or published from the listings table.
Only a <code>PATCH</code> whose body is exactly <code>{ "isActive": ... }</code> gets that exemption; any
content edit on such a listing is refused until it gets a duration. See the new
<a class="" href="https://docs.demo.myhandl.de/docs/error-codes#listing-shape">error codes</a> for the four listing-shape messages and
the order-time ones.</p>
<p>The provider portal's edit page is no longer a separate one-page editor — it's the same
step wizard <code>/provider/listings/new</code> uses, reachable at every step, saving from every
step, with a red marker on any step still carrying an unmet rule. Opening it shows a
loading skeleton while the listing loads and a not-found page for an id that isn't the
provider's own. The offer-type step (Ware/Dienstleistung) resets the category only when
the provider actively switches type — never on load, so a loaded listing's category is
never silently defaulted — and the handover step is two cards for goods
(Abholung/Lieferung) and two for a service (beim Kunden/im Geschäft), replacing the old
multi-select where picking none of them meant on-site work.</p>
<p><strong>Fixed:</strong> <code>POST /v1/customers/me/orders</code> with <code>addressId</code> and no <code>serviceMethod</code> on a
listing that only offers <code>DELIVERY</code> used to send the saved address through as the billing
address and 400 for a missing <code>deliveryAddress</code>. It now resolves the order's method
before deciding where the address goes, so it arrives as <code>deliveryAddress</code> as it should.</p>
<p>See <a class="" href="https://docs.demo.myhandl.de/docs/getting-started#how-customer-and-provider-meet">How customer and provider meet</a>
for the four methods and adr/0017 in the repo for why two methods rather than a single
<code>serviceLocation</code> field.</p>]]></content>
        <category label="v1" term="v1"/>
    </entry>
</feed>