<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
		<title>Biklo Partner API changelog</title>
		<link>https://developers.biklo.com/changelog/</link>
		<description>API changes, breaking ones first.</description>
		<item>
			<title>Returns: you inspect the product before the buyer is refunded</title>
			<link>https://developers.biklo.com/changelog/2026-11-03-returns-seller-inspection/</link>
			<guid>https://developers.biklo.com/changelog/2026-11-03-returns-seller-inspection/</guid>
			<pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>A returned product now comes back to you <strong>before</strong> anyone is refunded. Once the return
parcel is delivered you have <strong>48 hours</strong> to accept it (the buyer is refunded) or reject
it (the Biklo team decides). Until then the money for the returned lines stays held —
nothing is refunded or paid out on a timer.</p>
<h2>What changes</h2>
<ul>
<li><strong><code>POST /v1/returns/{id}/accept</code> changed behaviour.</strong> It used to refund the buyer for
the whole order at once, whether or not the parcel had come back. Now it works only
while the inspection is open — the parcel delivered to you, before
<code>inspection.deadline_at</code> — and refunds <strong>the price of the returned lines</strong>. Otherwise
<code>409</code>: <code>return_not_delivered</code>, <code>return_not_open</code>, <code>inspection_deadline_passed</code> or
<code>return_decision_failed</code>. It still takes the <strong>order</strong> id and returns the order.
Orders paid before Biklo Payments behave as before.</li>
<li><strong>New <code>POST /v1/returns/{id}/reject</code></strong> with a <code>reason</code> (10–2000 characters): opens a
dispute; the Biklo team refunds in full, refunds part, or refunds nothing.</li>
<li><strong><code>GET /v1/returns</code></strong> gained <code>return_order_id</code>, <code>reference</code>, <code>inspection</code> (status,
<code>delivered_at</code>, <code>deadline_at</code>, <code>refund</code>, <code>refunded</code>, <code>decided_at</code>, <code>decided_by</code>,
<code>can_accept</code>, <code>can_reject</code>) and <code>items</code> (the returned lines with <code>return_status</code>).</li>
<li><strong>New webhooks</strong>, each with the same payload as an item of <code>GET /v1/returns</code>:
<ul>
<li><code>return.delivered</code> — the parcel is back with you; decide before <code>deadline_at</code>.</li>
<li><code>return.escalated</code> — the Biklo team decides now (you rejected it, or the deadline
passed).</li>
<li><code>return.resolved</code> — decided: <code>accepted</code>, <code>partially_refunded</code> or <code>rejected</code>
(no refund).</li>
</ul>
</li>
<li><strong><code>GET /v1/wallet/lots</code></strong> gained <code>held_for_return</code>; <code>is_withdrawable</code> is now <code>false</code>
while a return holds the order's money.</li>
</ul>
<p>If you subscribed to every event (an empty <code>events</code> list), the new webhooks arrive
without any change on your side.</p>
]]></description>
		</item>
		<item>
			<title>Labels in A6, A5 or A4 — A6 by default</title>
			<link>https://developers.biklo.com/changelog/2026-10-27-label-page-size/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-27-label-page-size/</guid>
			<pubDate>Tue, 27 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p><code>GET /v1/orders/{id}/label</code> now returns PDF labels in a page size, and the default is
<strong>A6</strong> (105 × 148 mm) — the size label printers and adhesive AWB pouches take.</p>
<h2>What changes</h2>
<ul>
<li><strong>New <code>size</code> query parameter:</strong> <code>a6</code>, <code>a5</code>, <code>a4</code> or <code>original</code>.</li>
<li><strong>Default:</strong> the size the seller chose in their Biklo settings, and <strong>A6</strong> if they never
chose. Until now you always received the courier's own PDF — usually an A4 sheet with
the label printed twice. <strong>If your printing is set up for A4, send <code>size=original</code>
(unchanged output) or <code>size=a4</code>.</strong></li>
<li><strong>One parcel per page.</strong> Each distinct label is cut out and placed on its own portrait
page; the duplicate copy couriers print on the same sheet is dropped, and every parcel
of a multi-parcel order is kept.</li>
<li><strong>A4 and A5 print at real size</strong> (never enlarged); <strong>A6 fills the page.</strong></li>
<li><strong><code>X-Label-Size</code> response header</strong> with the size actually served. It reads <code>original</code>
while label formatting is switched off on Biklo's side.</li>
<li>An unknown <code>size</code> is <code>400 invalid_size</code>. <code>size</code> does not apply to <code>format=zpl</code>.</li>
</ul>
]]></description>
		</item>
		<item>
			<title>Quantities: ship fewer than ordered, sell-outs, restocks</title>
			<link>https://developers.biklo.com/changelog/2026-10-10-quantities-and-short-shipments/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-10-quantities-and-short-shipments/</guid>
			<pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Buyers can now order several pieces of one product (company listings, up to the stock).
What that changes for integrations:</p>
<h2>Orders</h2>
<ul>
<li><strong>Short shipment on accept.</strong> <code>POST /v1/orders/{id}/accept</code> takes
<code>items: [{id, quantity}]</code> when you have fewer pieces than were ordered. The courier is
booked for those; only once it returned an AWB is the line reduced and the buyer
refunded the missing pieces' product price. A failed booking changes nothing.</li>
<li><strong>Order lines</strong> carry <code>quantity</code> (ships), <code>quantity_ordered</code> and <code>quantity_short</code>.</li>
<li><strong><code>refunded</code></strong> on every order and <strong><code>refunds</code></strong> on <code>GET /v1/orders/{id}</code>: what went back
to the buyer and why (<code>short_shipped</code>, <code>buyer_returned</code>, …). <code>total</code> stays what was paid.</li>
<li><strong>Packing</strong>: a line's pieces can be split across boxes — never more boxes than pieces
(it used to be &quot;never more boxes than lines&quot;).</li>
</ul>
<h2>Webhooks</h2>
<ul>
<li><strong><code>order.created</code> is sent once the order's lines exist</strong> and now includes <code>items</code>.
It used to arrive a moment before them, so a fast <code>GET</code> could find no items.</li>
<li><strong>New <code>order.items_changed</code></strong> after a short shipment, with every line and the refund.</li>
<li><strong>A sell-out sends <code>product.sold</code>.</strong> When Biklo orders take the last pieces you get
<code>product.stock_changed</code> (to <code>0</code>) then <code>product.sold</code> — it used to say <code>product.archived</code>.</li>
</ul>
<h2>Products</h2>
<ul>
<li><strong>Restocking relists.</strong> A listing Biklo orders sold out comes back on sale when you send
stock above <code>0</code> (<code>PATCH /v1/products/{id}/stock</code> or a full update). No <code>unarchive</code> needed.</li>
<li><strong><code>unarchive</code></strong> answers <code>product_reserved</code> (409) only for a listing with no stock left
while an order for it is in progress — never for a listing that still has stock.</li>
</ul>
]]></description>
		</item>
		<item>
			<title>`confirm-shipped` is deprecated</title>
			<link>https://developers.biklo.com/changelog/2026-10-09-confirm-shipped-deprecated/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-09-confirm-shipped-deprecated/</guid>
			<pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p><code>POST /v1/orders/{id}/confirm-shipped</code> no longer does anything. It still answers <code>200</code>
with the order, unchanged, and carries a <code>Deprecation: true</code> header — remove the call
when convenient.</p>
<h2>Why</h2>
<p>Accepting an order books the courier, and the courier reports the parcel's progress
itself. A &quot;handed to the courier&quot; signal from you added nothing: no pickup, no money and
no message depended on it. It moved the order to <code>ship_to_courier</code> without checking
where the order was, so calling it late could pull a delivered order backwards.</p>
<h2>What changes for you</h2>
<ul>
<li>The order stays <code>accepted</code> after you hand the parcel over, and through delivery.</li>
<li>Where the parcel is: <code>GET /v1/orders/{id}/tracking</code> — <code>status</code> is the courier's latest
status, <code>events[].is_final</code> marks the delivery. The order also carries <code>delivered_at</code>.</li>
<li>The PHP SDK marks <code>orders()-&gt;confirmShipped()</code> <code>@deprecated</code>, and the WooCommerce
plugin drops its &quot;Confirm handed to courier&quot; button.</li>
</ul>
]]></description>
		</item>
		<item>
			<title>Sandbox courier steps behave like production; `delivered_at` on every order</title>
			<link>https://developers.biklo.com/changelog/2026-10-09-sandbox-courier-steps/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-09-sandbox-courier-steps/</guid>
			<pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>In production an order's <code>status</code> goes <code>pending → accepted</code> and stays <code>accepted</code> until
the end: the courier's progress is tracking, and a delivery fills in <code>delivered_at</code>.
The sandbox now does exactly that, and <code>delivered_at</code> is on every order, list included.</p>
<h2>Sandbox: <code>POST /v1/sandbox/orders/{id}/advance</code></h2>
<ul>
<li><code>accepted</code> is still the order's own step. <code>picked_up</code>, <code>in_delivery</code> and <code>delivered</code> are
now <strong>courier steps</strong>: they book the sandbox courier if nobody has, add courier events
(<code>Picked-Up</code>, <code>Out for Delivery</code>, <code>Delivered</code>) to the AWB's tracking, and leave the order
<code>accepted</code>. Before, they changed the order status to <code>picked_up</code> / <code>in_delivery</code> /
<code>delivered</code>, which production never does.</li>
<li><code>delivered</code> stamps <code>delivered_at</code> and starts the return window, as a real delivery does.
<code>settled</code> is unchanged.</li>
<li><code>ship_to_courier</code> is gone (see the <code>confirm-shipped</code> deprecation). A step behind the
current one answers <code>already_past</code>.</li>
<li>Jumping ahead still works in one call (<code>pending</code> → <code>delivered</code> accepts on the way and
writes the courier steps in between).</li>
</ul>
<h2><code>delivered_at</code> on every order</h2>
<p><code>GET /v1/orders</code> now carries <code>delivered_at</code> on each order, not only <code>GET /v1/orders/{id}</code>.
<code>status</code> stays <code>accepted</code> through delivery, so this is how a poll sees the parcel
arrived — a delivery also bumps <code>updated_at</code>, so <code>?updated_since=</code> picks it up.</p>
]]></description>
		</item>
		<item>
			<title>Tracking: `status` is now the courier&#039;s status</title>
			<link>https://developers.biklo.com/changelog/2026-10-09-tracking-courier-status/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-09-tracking-courier-status/</guid>
			<pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>On <code>GET /v1/orders/{id}/tracking</code>, <code>status</code> now tells you where the parcel is — the
latest courier event, in the courier's own wording — and the order status moved to a new
field, <code>internal_status</code>.</p>
<h2>Why</h2>
<p>The order status stays <code>accepted</code> while the parcel travels and after it is delivered:
courier scans record the delivery (<code>delivered_at</code> on the order) but do not change the
order status. So a delivered parcel answered <code>&quot;status&quot;: &quot;accepted&quot;</code> on its own tracking
endpoint.</p>
<h2>What changes for you</h2>
<pre><code class="language-json">{
  &quot;order_id&quot;: 610,
  &quot;status&quot;: &quot;Delivered&quot;,
  &quot;internal_status&quot;: &quot;accepted&quot;,
  &quot;awb&quot;: { &quot;number&quot;: &quot;95328086&quot;, &quot;tracking_url&quot;: &quot;https://track.innoship.io/...&quot; },
  &quot;events&quot;: [ … ]
}
</code></pre>
<ul>
<li><code>status</code> — the latest courier event's <code>status</code>, or <code>null</code> until the courier has reported
anything. It is the courier's text, so to know the parcel arrived use
<code>events[].is_final</code>, not a match on the wording.</li>
<li><code>internal_status</code> — what <code>status</code> used to be: the order status, same vocabulary as
<code>GET /v1/orders/{id}</code>.</li>
<li><code>GET /v1/orders</code> and <code>GET /v1/orders/{id}</code> are unchanged, including the <code>?status=</code>
filter.</li>
</ul>
<p>If you read <code>status</code> from this endpoint, read <code>internal_status</code> instead.</p>
]]></description>
		</item>
		<item>
			<title>Accepting an order now books the courier</title>
			<link>https://developers.biklo.com/changelog/2026-10-08-accept-books-the-courier/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-08-accept-books-the-courier/</guid>
			<pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p><code>POST /v1/orders/{id}/accept</code> now does what Accept does in the seller dashboard: it books
the courier. The response carries the AWB, the label is ready, tracking starts, and the
buyer gets the same emails and SMS as for an order accepted on the website.</p>
<h2>What was wrong</h2>
<p>Accept only changed the order's status. The order then sat &quot;accepted&quot; with no courier.
An AWB requested afterwards with <code>POST /v1/orders/{id}/awb</code> was booked, but stored
without the tracking record courier updates are matched against — so the parcel's
progress never reached Biklo: no delivery, no confirmation, and no payout.</p>
<h2>What changes for you</h2>
<ul>
<li>Call accept as before. Read the AWB from its response (<code>awb.number</code>,
<code>awb.tracking_url</code>) and fetch the label from <code>GET /v1/orders/{id}/label</code>.</li>
<li>If the courier cannot be booked, accept fails and the order <strong>stays pending</strong> — retry
once the reason is fixed: <code>courier_required</code> (422), <code>buyer_phone_missing</code> (422, the
buyer has been asked to add one), <code>awb_generation_failed</code> (502).</li>
<li><code>POST /v1/orders/{id}/awb</code> still works and is idempotent: it returns the AWB accept
booked.</li>
</ul>
<p>Orders accepted through the API before this change were repaired on our side: the ones
without a courier have one now, and every AWB is tracked.</p>
]]></description>
		</item>
		<item>
			<title>Images in the product payload, and archiving a product</title>
			<link>https://developers.biklo.com/changelog/2026-10-08-images-in-payload-and-archive/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-08-images-in-payload-and-archive/</guid>
			<pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Two additions, both driven by the first shop plugin built on this API — and by the
request count of a first sync.</p>
<h2><code>images</code> in <code>POST /v1/products</code>, <code>PUT</code> and the batch</h2>
<p>A product's image URLs can now travel with the product:</p>
<pre><code class="language-json">{ &quot;external_id&quot;: &quot;ERP-1001&quot;, &quot;…&quot;: &quot;…&quot;,
  &quot;images&quot;: [&quot;https://cdn.example.com/erp/mtb-front.jpg&quot;] }
</code></pre>
<p>Until now images needed their own call per product, so the first sync of a
2 000-product catalogue was two batches plus <strong>2 000 image requests</strong> — most of a day's
rate limit. With <code>images</code> in the payload it is two batches.</p>
<p>The rules are the ones <code>attributes</code> already has: sent, the list <strong>replaces</strong> the
listing's images (an identical list changes nothing, so a re-push does not churn them);
omitted, they are left alone. Every entry must be an http(s) URL — a bad one is
<code>invalid_image_url</code> naming its position, rather than being dropped and the listing
published with an image missing. At most 20 per product.</p>
<p><code>POST /v1/products/{id}/images</code> stays, for multipart uploads.</p>
<h2><code>POST /v1/products/{id}/archive</code> and <code>/unarchive</code></h2>
<p>Setting stock to <code>0</code> never took a product off the marketplace: the listing stayed
visible, just unbuyable. Archiving does — out of search, the storefront and carts —
the same way the seller's own &quot;disable ad&quot; does, so a Biklo order cancelled later does
not republish it. <code>unarchive</code> puts it back on sale — including a product a Biklo order
marked sold, once that order is delivered — and is refused with <code>product_reserved</code>
(409) only while an order for the product is still in progress.</p>
<p>Both are idempotent and need <code>products.write</code>.</p>
]]></description>
		</item>
		<item>
			<title>Sandbox test orders now reach your webhooks, and sold products can be relisted</title>
			<link>https://developers.biklo.com/changelog/2026-10-08-sandbox-webhooks-and-relisting/</link>
			<guid>https://developers.biklo.com/changelog/2026-10-08-sandbox-webhooks-and-relisting/</guid>
			<pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Two fixes found while building the WooCommerce plugin's order handling.</p>
<h2>Sandbox orders fire your webhooks</h2>
<p>An event caused by your own API call is not sent back to you — a catalogue sync does
not want a delivery for every product it just wrote. That rule also swallowed the
events from <code>POST /v1/sandbox/orders</code> and <code>POST /v1/sandbox/orders/{id}/advance</code>, so
the one tool meant for testing webhook handling produced no webhooks.</p>
<p>The sandbox helpers stand in for a buyer and a courier, so their events are now
delivered to you: create a test order and <code>order.created</code> arrives; advance it and
<code>order.status_changed</code> follows.</p>
<h2>Relisting a product a Biklo order sold</h2>
<p>A Biklo order marks its products sold and archives them — one sale, one listing.
<code>POST /v1/products/{id}/unarchive</code> puts such a product back on sale once its order is
delivered. It is refused with <code>product_reserved</code> (409) only while an order for the
product is still in progress; a cancelled or rejected order republishes the product by
itself.</p>
]]></description>
		</item>
		<item>
			<title>Customers, so you can actually raise the invoice</title>
			<link>https://developers.biklo.com/changelog/2026-09-21-customers-and-invoicing/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-21-customers-and-invoicing/</guid>
			<pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p><code>POST /v1/orders/{id}/invoice</code> has always accepted a finished PDF. Nothing gave you
what goes in it — the order payload carries a delivery block because a courier needs a
city and a postcode, and an invoice needs a street, and for a business buyer a company
name and a VAT number. Sellers were reading those off the dashboard and retyping them.</p>
<pre><code class="language-bash">curl https://api.biklo.com/v1/orders/711/customer -H &quot;Authorization: Bearer $KEY&quot;
</code></pre>
<pre><code class="language-json">{
  &quot;id&quot;: 9000001,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;email&quot;: &quot;john.doe@example.com&quot;,
  &quot;phone&quot;: &quot;+40700000000&quot;,
  &quot;billing&quot;: {
    &quot;address_line&quot;: &quot;Str. Exemplu 12&quot;,
    &quot;city&quot;: &quot;Brasov&quot;,
    &quot;county&quot;: &quot;Brasov&quot;,
    &quot;postal_code&quot;: &quot;500002&quot;,
    &quot;country_code&quot;: &quot;RO&quot;,
    &quot;country&quot;: &quot;România&quot;,
    &quot;is_company&quot;: true,
    &quot;company&quot;: {
      &quot;name&quot;: &quot;Example Bike Shop SRL&quot;,
      &quot;vat_code&quot;: &quot;RO12345678&quot;,
      &quot;registration_no&quot;: &quot;J16/123/2020&quot;
    },
    &quot;is_live&quot;: true,
    &quot;delivered_to_locker&quot;: false
  }
}
</code></pre>
<p>Three endpoints:</p>
<ul>
<li><strong><code>GET /v1/orders/{id}/customer</code></strong> — the buyer as they were for that sale. This is
the one to call when you are about to invoice: a customer who has moved has two
orders at two addresses, and each invoice carries its own.</li>
<li><strong><code>GET /v1/customers</code></strong> — everyone who has bought from you, with their latest billing
details. Paginated, filterable on <code>updated_since</code> and searchable on name or email.</li>
<li><strong><code>GET /v1/customers/{id}</code></strong> — one buyer plus every order they placed with you, each
flagged <code>has_invoice</code>. <code>has_invoice: false</code> is your queue of orders still to bill.</li>
</ul>
<h2><code>is_company</code> is the field to branch on</h2>
<p>A buyer who checked out with a company address must be invoiced as that company, with
its CUI — not as the person. Read the flag; do not infer it from whether <code>company</code>
came back populated.</p>
<h2>A new scope, off by default</h2>
<p>These sit behind <strong><code>customers.read</code></strong>, which is not part of <code>orders.read</code>. An order
carries a delivery address because you cannot ship without one. A full postal address,
an email and a company's fiscal identity is a different thing to hand out, and a
stock-sync integration has no use for it. Sellers grant it under &quot;Customers and
invoicing&quot; in their dashboard.</p>
<h2>One caveat worth reading</h2>
<p><code>is_live: true</code> means <code>address_line</code> was read from an address the buyer still controls
and can edit. Only the city, county, postcode and country were frozen at checkout. If
your invoices have to be reproducible, store the billing block at the moment you issue
the invoice rather than re-fetching it later.</p>
<p>Where a buyer has since deleted that address, <code>address_line</code> is <code>null</code> and <code>is_live</code> is
<code>false</code> — the rest of the block still stands.</p>
]]></description>
		</item>
		<item>
			<title>Sixteen new webhook events, and we stopped echoing your own writes back at you</title>
			<link>https://developers.biklo.com/changelog/2026-09-21-webhook-events/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-21-webhook-events/</guid>
			<pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>The catalogue went from ten events to twenty-six. Products, variant groups, courier
tracking, company settings and invoices can now all be pushed instead of polled.</p>
<h2>The new events</h2>
<p><strong>Products</strong> — <code>product.created</code>, <code>product.updated</code>, <code>product.stock_changed</code>,
<code>product.price_changed</code>, <code>product.sold</code>, <code>product.archived</code>, <code>product.deleted</code>.</p>
<p><strong>Variant groups</strong> — <code>product_group.created</code>, <code>product_group.updated</code>,
<code>product_group.deleted</code>, keyed by your <code>external_group_id</code>.</p>
<p><strong>Orders</strong> — <code>order.awb_created</code> (the waybill, with its number and tracking URL) and
<code>order.tracking_updated</code> (one per courier scan).</p>
<p><strong>Everything else</strong> — <code>question.answered</code>, <code>company.updated</code>, <code>company.bands_updated</code>,
<code>invoice.issued</code>.</p>
<p>Every one is listed with its exact payload in the
<a href="/guides/webhook-events/">event reference</a>.</p>
<h2>We no longer send you your own changes</h2>
<p>A change <strong>your</strong> integration made through the API is not delivered back to you. You
already know about it, and on a nightly catalogue sync it was thousands of deliveries
telling you what you had just done.</p>
<p>It still reaches the seller's <em>other</em> integrations — a stock feed and an accounting
system both connected will each hear about the other's writes — and anything changed in
the Biklo dashboard reaches everyone.</p>
<p>You do not need to do anything. If you were filtering out your own writes at your end,
you can stop.</p>
<h2>One product edit is one event</h2>
<p>A write that touches several columns does not send several deliveries:</p>
<ol>
<li>A lifecycle change wins outright — deleted, archived, sold, approved, rejected —
and nothing else is sent.</li>
<li>Otherwise stock and price are announced, and both can fire.</li>
<li>Otherwise a single <code>product.updated</code> naming what moved.</li>
</ol>
<p>So a sync that changes a title <strong>and</strong> the stock sends <code>product.stock_changed</code> only,
not that plus an <code>updated</code>.</p>
<h2>Two things to know before you subscribe to everything</h2>
<p><code>order.tracking_updated</code> is the highest-volume event here — a parcel produces several
scans and every shipment in flight is polled. If you only want to know it arrived,
subscribe and filter on <code>is_final</code>; you keep the audit trail and lose nothing.</p>
<p><code>notification.sent</code> is the next highest: one delivery per email and SMS we send about
your orders.</p>
<p>Both are genuinely useful. Neither is something to switch on without meaning to.</p>
<h2>Smaller things in the same release</h2>
<p><strong>Returns are identifiable.</strong> A return is an order in its own right, so it always
emitted <code>order.status_changed</code> — you just could not tell it apart from a normal order.
Every order event now carries <code>is_return</code>, and a return also carries
<code>parent_order_id</code>.</p>
<p><strong><code>payout.updated</code> gained <code>needs_review</code></strong>, for a payout that failed and is waiting on
a person.</p>
<p><strong><code>product.price_changed</code> is in minor units</strong>, like every other amount in this API.
<code>posts</code> stores price in major units internally; the event does not leak that.</p>
]]></description>
		</item>
		<item>
			<title>Custom request headers on a webhook endpoint</title>
			<link>https://developers.biklo.com/changelog/2026-09-21-webhook-gateway-headers/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-21-webhook-gateway-headers/</guid>
			<pubDate>Mon, 21 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>If your receiver sits behind an API gateway that checks a key before routing anything
through, you can now register that key with the endpoint instead of allow-listing our
IP addresses.</p>
<pre><code class="language-http">POST /v1/webhooks

{
  &quot;url&quot;: &quot;https://gateway.example.com/biklo/webhooks&quot;,
  &quot;headers&quot;: [
    { &quot;name&quot;: &quot;X-Api-Key&quot;, &quot;value&quot;: &quot;the value your gateway expects&quot; }
  ]
}
</code></pre>
<p>Up to five per endpoint. Values are stored encrypted, never returned and never written
to a delivery record — <code>GET /v1/webhooks</code> shows the name and the first four characters
so you can tell two credentials apart.</p>
<h2>Rotating one no longer costs you your signing secret</h2>
<p><code>PUT /v1/webhooks/{id}</code> is new. It edits an endpoint in place:</p>
<pre><code class="language-http">PUT /v1/webhooks/42

{ &quot;headers&quot;: [ { &quot;name&quot;: &quot;X-Api-Key&quot;, &quot;value&quot;: &quot;the rotated value&quot; } ] }
</code></pre>
<p>It also takes <code>events</code>, so narrowing or widening a subscription no longer means
deleting and re-registering. Neither touches the signing secret, which was the real
cost of the old approach: rotating a gateway credential meant a new endpoint, a new
secret, and a deploy of your verification code for a change that had nothing to do
with signing.</p>
<p><code>headers</code> replaces the whole set rather than merging — we cannot show you what is
stored, so there is nothing sensible to merge into. An empty list clears them.</p>
<h2>It is not authentication</h2>
<p>A fixed header value says nothing about the body it arrived with. It gets our request
past your gateway; <code>X-Biklo-Signature</code> is what tells you the payload is ours and
unmodified. Verify it either way.</p>
<h2>Also fixed</h2>
<p>The reference for <code>POST /v1/webhooks</code> said an empty <code>events</code> list &quot;subscribes to
nothing, so the endpoint is registered and silent&quot;. It has always meant the opposite —
an empty list subscribes to <strong>every</strong> event, including ones added later, and that is
what the page says now. Nothing about the behaviour changed.</p>
]]></description>
		</item>
		<item>
			<title>Addresses are now in the API, and a listing&#039;s location comes from your default one</title>
			<link>https://developers.biklo.com/changelog/2026-09-14-listing-location-from-address/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-14-listing-location-from-address/</guid>
			<pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Where a product says it is, and where the courier actually collects it, are now the
same thing by construction. They were not before.</p>
<h2>What changed</h2>
<p>When you create or update a product without a <code>city_id</code>, the listing takes the <strong>city,
county and street of the seller's default shipping address</strong>.</p>
<p>It used to take the city of the company's registered address, and failing that the city
of any listing the seller already had. Meanwhile the parcel's origin was built from the
default shipping address — a different row, which can name a different county. A seller
whose warehouse was not their registered office published a catalogue that said Cluj
while every waybill was raised in Ilfov, and nothing reconciled the two.</p>
<p>The street is new as well: partner-written listings carried no address text at all.</p>
<h2>It is re-read on every write</h2>
<p>Not copied once at creation. A stock-only <code>PUT</code> re-syncs the location, so a seller who
changes their default address sees their catalogue follow on the next push, and
listings created before this rule repair themselves the first time you touch them.</p>
<p>This is the same self-healing behaviour <code>ship_with_us</code> already had.</p>
<h2><code>city_id</code> still overrides, for a real second location</h2>
<p>Send <code>city_id</code> (and optionally <code>county_id</code>) when the item genuinely ships from
somewhere else. The override sets the city and county and <strong>deliberately leaves the
street empty</strong> — putting the default address's street under a different city would name
a place that does not exist.</p>
<p>If you have been sending <code>city_id</code> on every product out of habit, stop. You will get a
more accurate listing by leaving it out.</p>
<h2>Addresses are now part of the API</h2>
<p>Shipped in the same release: <code>/v1/addresses</code> — list, create, update, delete, and
<code>POST /v1/addresses/{id}/default</code>. Under the <strong>Company profile</strong> permission group, so
an existing key with company write access already has it.</p>
<pre><code class="language-bash">curl -X POST https://api.biklo.com/v1/addresses \
  -H &quot;Authorization: Bearer $KEY&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;city_id&quot;: 1, &quot;street&quot;: &quot;Bulevardul Decebal 14&quot;, &quot;postal_code&quot;: &quot;200001&quot;}'
</code></pre>
<p>Your first address becomes the default automatically. <code>GET /v1/addresses</code> is worth
making the first call of your setup routine: an account with no address publishes a
perfectly valid catalogue and then cannot fulfil a single order, and nothing warns you
until the first real sale.</p>
<p>Two deletes are refused on purpose. <code>409 last_address</code> when it is your only one, and
<code>409 default_address</code> when it is the default and you have others — in both cases
removing it would leave future orders with no origin while your listings stayed up.</p>
<p>Two things the endpoint decides for you: addresses are always company addresses, and
they never modify your company profile. If you need to change the company's registered
details, that is <code>PUT /v1/company</code>.</p>
<p>If the account still has no address, creating a product falls back to the company city
or an existing listing as before, and the error now names what is actually missing
rather than asking for a <code>city_id</code>.</p>
]]></description>
		</item>
		<item>
			<title>Send yourself a webhook, and check your setup on the first call</title>
			<link>https://developers.biklo.com/changelog/2026-09-14-webhook-testing/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-14-webhook-testing/</guid>
			<pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Two additions aimed at the same thing: finding out an integration is misconfigured
while you are building it, rather than when a real order arrives.</p>
<h2><code>POST /v1/webhooks/{id}/test</code></h2>
<pre><code class="language-bash">curl -X POST https://sandbox-api.biklo.com/v1/webhooks/1/test -H &quot;Authorization: Bearer $KEY&quot;
</code></pre>
<p>Queues a <code>test.ping</code> to that endpoint. Every other event needs something to happen
first, which made &quot;is my receiver wired up correctly?&quot; a question you could only answer
by creating a real order.</p>
<p>It travels the <strong>same path as a real event</strong> — same queue, same headers, same HMAC
signature — so a delivery that arrives and verifies here proves the real ones will. It
is sent regardless of what the endpoint subscribes to, because an integrator testing a
receiver has not necessarily decided which events they want yet; <code>test.ping</code> is not
itself subscribable, so you will never get one you did not ask for.</p>
<p><code>202</code> means queued, not delivered. The outcome appears on
<code>GET /v1/webhooks/{id}/deliveries</code> a moment later. A switched-off endpoint is refused
with <code>409 endpoint_disabled</code> rather than silently accepting.</p>
<p>The webhooks guide now has a <strong>Testing before you go live</strong> section, including the
three things worth deliberately breaking before you trust your receiver: a wrong
secret, a <code>500</code>, and a slow reply.</p>
<h2><code>GET /v1/me</code> now tells you whether you can ship</h2>
<pre><code class="language-json">{
  &quot;shipping_address&quot;: {
    &quot;ready&quot;: false,
    &quot;reason&quot;: &quot;no_address&quot;,
    &quot;message&quot;: &quot;This account has no shipping address, so orders have nowhere to ship from. Create one with POST /v1/addresses.&quot;,
    &quot;default_address&quot;: null,
    &quot;details_url&quot;: &quot;/v1/addresses&quot;
  }
}
</code></pre>
<p><code>/v1/me</code> is the first call every integration makes, and this is the failure that is
hardest to see coming: products publish perfectly well without an address, look correct
in the dashboard, and then the first real order has nowhere to ship from.</p>
<p>Deliberately not scope-gated, unlike the payout summary next to it. The integration
that most needs this is the catalog-only one — it publishes, sees nothing wrong, and has
no orders endpoint to discover the problem through.</p>
<p>Check <code>shipping_address.ready</code> on startup. If it is <code>false</code>, <code>POST /v1/addresses</code> is
one call away.</p>
]]></description>
		</item>
		<item>
			<title>Products always ship with Biklo, and external_id and stock are now required</title>
			<link>https://developers.biklo.com/changelog/2026-09-13-product-write-rules/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-13-product-write-rules/</guid>
			<pubDate>Sun, 13 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Three changes to how a product is written. None of them affect an existing
integration — the API is not live yet — but they change what a correct <code>POST /v1/products</code> looks like, so read this before you write the mapper.</p>
<h2><code>shipping.ship_with_us</code> is always on, and no longer yours to set</h2>
<p>Every listing created through this API ships with Biklo. We quote the courier, the
buyer pays delivery at checkout, and we book the waybill.</p>
<p>It used to default to <code>false</code> and honour whatever the payload said. That meant an ERP
which simply omitted the <code>shipping</code> object published a catalogue <strong>nobody could order
from</strong> — the listings looked fine, and nothing in the response said why no orders
arrived. There was never a good reason to switch it off, because a listing a courier
cannot be booked for is unbuyable here.</p>
<p>Sending <code>ship_with_us</code> still succeeds; it is ignored. You can drop it from your
payload. A listing that somehow has it off is repaired by the next write.</p>
<p>The field is still returned, and is still worth reading: an older listing created in
the dashboard can read <code>false</code> until it is next written through this API.</p>
<h2><code>external_id</code> is required</h2>
<p>It is the only thing tying a listing here back to a row in your system, and it is what
a repeated push matches on. Without it, a retry after a timeout creates a <strong>second
listing</strong> instead of updating the first — and you have no way to find either.</p>
<p>It was optional, which made the safe path the one you had to know about. Now the safe
path is the only one.</p>
<p>This matters most on <code>POST /v1/products/batch</code>: a retried batch without it would
duplicate every product in it.</p>
<h2><code>stock</code> is required</h2>
<p>Send <code>0</code> for something you are out of — that is a real answer and it is accepted. The
field is rejected only if it is negative or not a number (<code>invalid_stock</code>).</p>
<p>Previously a create could omit it and the listing carried no stock figure at all, which
is not a state worth being able to reach.</p>
<p><code>PATCH /v1/products/{id}/stock</code> is still the cheaper way to move it afterwards.</p>
<h2>Corrections to the documentation, not to the API</h2>
<p>Separately, a large number of response examples were wrong — they showed two to four
fields where the endpoint returns the whole object. <code>POST /v1/products</code>, <code>PUT /v1/products/{id}</code> and both <code>PATCH</code> routes all return the complete product, and always
did. <code>PUT /v1/company/shipping-absorption</code> documented <code>country_id</code> and
<code>min_amount_cents</code>, which no response has ever contained.</p>
<p>Every endpoint now documents every field it returns, with its type and meaning. If you
built against an example rather than against what came back, it is worth a second look.</p>
]]></description>
		</item>
		<item>
			<title>See the fees Biklo charges you, and download the invoice</title>
			<link>https://developers.biklo.com/changelog/2026-09-12-fee-invoices/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-12-fee-invoices/</guid>
			<pubDate>Sat, 12 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>When your company absorbs the platform commission or the delivery, the buyer is not
charged it — Biklo withholds it at payout and invoices you. You could previously watch
the wallet shrink with no way to see why. Now you can.</p>
<h2>New</h2>
<h3><code>GET /v1/invoices</code></h3>
<p>The fee invoices issued to you. One covers <strong>several orders</strong>, because liabilities are
raised per order and invoiced in batches at settlement.</p>
<h3><code>GET /v1/invoices/{number}/pdf</code></h3>
<p>Raw PDF bytes — what your accountant files. Pass <code>series</code> when a number is ambiguous;
a number is only unique within its series.</p>
<h3><code>GET /v1/fee-liabilities</code></h3>
<p>What is owed per order, <strong>including before an invoice exists</strong> — which is most of a
current month.</p>
<h2>Two fields worth reading carefully</h2>
<p><strong><code>owed_amount</code> and <code>charged_amount</code> are not the same number.</strong> A liability can settle
partially, so reading one and assuming the other is how books stop balancing.</p>
<p><strong><code>written_off_amount</code></strong> is what Biklo gave up when the absorbed fees exceeded the
order itself. You were never charged it. It appears because it happened, and your
reconciliation has to account for the difference.</p>
<h2>In the sandbox</h2>
<p>Invoices are generated in code — nothing reaches our fiscal provider. They carry an
<code>SBX_</code> series, are stamped <strong>NOT A FISCAL DOCUMENT</strong>, and <code>is_sandbox</code> is <code>true</code> so you
never have to infer it from the prefix. The PDF is real and downloadable, so you can
build the filing side of your integration against it.</p>
]]></description>
		</item>
		<item>
			<title>See what we emailed and texted about your orders</title>
			<link>https://developers.biklo.com/changelog/2026-09-12-notification-log/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-12-notification-log/</guid>
			<pubDate>Sat, 12 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>The sandbox never delivers email or SMS — which until now meant you had no way to know
a test order had produced a notification at all. It records them instead, and you can
read them.</p>
<h2>New</h2>
<h3><code>GET /v1/notifications</code></h3>
<p>Every message the platform sent about <strong>your</strong> orders. Scoped by seller, not by
recipient: most of this mail goes to buyers, so filtering on &quot;addressed to you&quot; would
show you almost nothing of your own orders.</p>
<pre><code>GET /v1/notifications?order_id=721&amp;channel=mail
</code></pre>
<h3><code>GET /v1/notifications/{id}</code></h3>
<p>Adds the recipient and the rendered body.</p>
<h3>The <code>notification.sent</code> webhook</h3>
<p>Subscribe rather than poll. Thin, like every other event here — ids and a status, with
the content behind the endpoint above:</p>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;notification.sent&quot;,
  &quot;data&quot;: {
    &quot;notification_id&quot;: 7,
    &quot;channel&quot;: &quot;mail&quot;,
    &quot;status&quot;: &quot;suppressed&quot;,
    &quot;template&quot;: &quot;email_shipping_sent&quot;,
    &quot;order_id&quot;: 721
  }
}
</code></pre>
<h2>The one field to read carefully</h2>
<p><strong><code>status: suppressed</code></strong> is the sandbox's normal state. The message was composed in
full and deliberately not delivered. It is not <code>sent</code>, and treating it as such would
have you waiting on mail that was never going to arrive.</p>
<p>In production the same rows carry <code>sent</code> or <code>failed</code> — and only metadata, because a
rendered order email carries the buyer's name, address and phone. <code>body_available</code>
tells you which you are looking at.</p>
]]></description>
		</item>
		<item>
			<title>Parcel tiers, packing, and pricing visibility</title>
			<link>https://developers.biklo.com/changelog/2026-09-12-parcel-tiers-and-pricing/</link>
			<guid>https://developers.biklo.com/changelog/2026-09-12-parcel-tiers-and-pricing/</guid>
			<pubDate>Sat, 12 Sep 2026 00:00:00 +0000</pubDate>
			<description><![CDATA[<p>Delivery absorption is now set <strong>per parcel size</strong>, orders carry a <strong>packing plan</strong>,
and you can finally see the <strong>commission</strong> before you list.</p>
<h2>Breaking</h2>
<h3><code>PUT /v1/company/shipping-absorption</code> requires <code>parcel_tier</code></h3>
<p>Absorption used to be one promise per destination. It is now one per destination
<strong>and parcel size</strong>, because the two are not remotely the same purchase: on RO→HU the
courier charges about 26 RON for a small parcel and 539 for a large one.</p>
<pre><code class="language-json">{
  &quot;country_code&quot;: &quot;RO&quot;,
  &quot;parcel_tier&quot;: &quot;large&quot;,
  &quot;currency&quot;: &quot;RON&quot;,
  &quot;bands&quot;: [
    { &quot;min_amount&quot;: 0, &quot;max_amount&quot;: 150000, &quot;absorbs&quot;: false },
    { &quot;min_amount&quot;: 150000, &quot;max_amount&quot;: null, &quot;absorbs&quot;: true }
  ]
}
</code></pre>
<p>Omitting it returns <code>400 parcel_tier_required</code>. We made it required rather than
defaulting to <code>small</code> on purpose — a default would have let you set bike thresholds
while silently rewriting your accessory bands.</p>
<p><code>GET /v1/company/shipping-absorption</code> returns one set per tier, and now uses
<code>min_amount</code> / <code>max_amount</code> (it previously returned <code>min_amount_cents</code> /
<code>max_amount_cents</code>, which you could not round-trip into the write).</p>
<h2>New</h2>
<h3><code>GET /v1/products/{id}/pricing</code></h3>
<p>What a buyer is charged, and what it costs you — from the same service that renders
the product page, so you and your buyer never see different numbers.</p>
<h3>Commission on every category</h3>
<p><code>GET /v1/categories</code> now carries <code>commission</code> per category, inherited down the tree.
Set margins before listing instead of inferring the fee from a completed order.
<code>max_amount: null</code> means <strong>uncapped</strong>.</p>
<h3>Packing: <code>parcels</code> on accept</h3>
<p><code>POST /v1/orders/{id}/accept</code> takes an optional <code>parcels</code> array — which order lines go
in which box. The courier prices per parcel, so three products declared as one box is
one charge rather than three. Send nothing and one box per line is assumed, exactly as
before.</p>
<p>Packing is <strong>merge-only</strong>: an order cannot ship in more boxes than it has lines.</p>
<h3>Smaller additions</h3>
<ul>
<li><code>GET /v1/orders/{id}</code> returns <code>parcels</code>, and each order item now carries its <code>id</code> —
that id is what a parcel plan references.</li>
<li>Products carry a derived <code>shipping_class</code> and <code>parcel_tier</code>, so you can tell which
of your two band sets governs a listing.</li>
<li><code>GET /v1/wallet/balance</code> gained <code>settled_to_fees</code>: terminal, where absorbed fees
consumed the whole order. It was previously missing from the response entirely.</li>
<li><code>GET /v1/wallet/lots</code> gained <code>net_transferred_amount</code> — what actually reached the
seller after absorbed fees were withheld. <strong>Reconcile against this, not <code>amount</code>.</strong></li>
</ul>
]]></description>
		</item>
</channel></rss>