All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Because the Picnic API is unofficial and changes without notice, entries here also record shape changes that this library absorbs on your behalf.

Unreleased

Removed

  • Breaking. Picnic.order/3, Picnic.order!/3, Picnic.Resources.Orders and Picnic.Schema.Order are gone. They called an endpoint that does not exist in Picnic's API and never returned anything but a 404. Orders are reached two other ways: a placed order comes back under "orders" from Picnic.delivery/3, and the cart is itself the open order, which is why Picnic.get_cart/2 returns an order-shaped document. For an order's checkout status, Picnic.request/4 reaches /cart/checkout/order/{order_id}/status without needing library support.

Changed

  • Breaking. Picnic.search/3 now returns a flat list of products, most relevant first, instead of category groups holding their matches under "items". Picnic moved product search to a display-page endpoint that nests products at arbitrary depth among banners and headers; the presentation is dropped and only the products are returned. Fetch the untouched page with Picnic.request(client, :get, "/pages/search-page-results", params: [search_term: term]) if you need the rest.
  • Breaking. Picnic.Schema.SearchResult describes a product rather than a result group. :items and :links are gone; :display_price, :price, :image_id, :unit_quantity and :decorators take their place. Prices are integer cents.

Fixed

  • Picnic.deliveries/2 returned %Picnic.Error{category: :http, status: 404} for every call. The delivery list moved to a separate summary endpoint on the same API version; the verb, the status-filter body and the response shape are unchanged, so only the path differs. Note that the list has always returned delivery summaries — fields the full record carries can be nil — and Picnic.delivery/3 is how you get the complete record. This is now documented rather than implied.

  • Product search returned %Picnic.Error{category: :http, status: 404} for every query. The endpoint it used no longer exists; search now goes to the page-results endpoint on the same API version. No configuration change is needed.

  • The two-factor handshake could not be completed as documented. login/3 returns an error rather than a client on that path, so the client callers still held carried no token and both Picnic.Auth.generate_2fa/2 and verify_2fa/2 were sent unauthenticated, failing with 401. The error now carries the provisional client and both functions accept the login result directly, so no manual Picnic.Client.put_token/2 step is needed.

  • A two-factor challenge returned with a non-success HTTP status discarded the provisional token from the x-picnic-auth header, leaving the handshake unrecoverable. The challenge is now detected before the status is classified, so both paths produce the same resumable error.

  • The default base URL pointed at a host that does not resolve, so every client built without an explicit :base_url failed with %Picnic.Error{category: :network, reason: :nxdomain} before reaching the API. Countries now resolve to their real storefronts under storefront-prod.<country>.picnicinternational.com. :nl and :de previously resolved to the same host, making :country a no-op for URL building; they are now distinct. Clients that passed :base_url explicitly are unaffected.

Added

  • Picnic.Page.selling_units/1 flattens Picnic's display-page payloads into a plain list of products, skipping tiles it does not recognise and degrading to [] with a drift warning on an unfamiliar page.
  • Picnic.login/4 takes options: send_2fa: true requests the second-factor code as part of logging in, with :channel (default "SMS") choosing how it is delivered.
  • Picnic.HTTP.response/4 returns the response for any HTTP status, with status_error/1 now public for callers that classify it themselves.
  • Picnic.Client inspects compactly as #Picnic.Client<…> with the auth token redacted, instead of dumping the whole Req request template.
  • :fr as a supported :country value alongside :nl and :de.

0.1.0 - 2026-08-03

Initial release.

Added

  • Picnic.Client — the client as a plain struct, not a process. new/1 resolves configuration once and stores a Req.Request template; there is nothing to start or supervise.
  • Picnic.request/4 — generic passthrough to any endpoint, including ones this library does not model. A first-class part of the public API.
  • Picnic.Config — country, API version, base URL, client id, and default headers as data. An API version bump is a configuration change, not a code change.
  • Picnic.Auth — login, hash_password/1 (MD5, as Picnic's scheme requires), token capture from the x-picnic-auth header, and an explicit two-factor flow via generate_2fa/2 and verify_2fa/2.
  • Picnic.Decode — lenient decoding. Plain maps by default; opt-in Picnic.Schema.* structs that ignore unknown fields, leave missing fields nil, and keep the untouched payload under :raw.
  • Picnic.Error — a categorised error struct (:network | :auth | :rate_limit | :http | :decode | :schema) so callers can match on classes of failure.
  • Resources for search, user, cart, orders, deliveries, and recipes, each with a bang variant on the Picnic facade.
  • Picnic.Session — optional GenServer that holds the client and re-authenticates once on an :auth error, with a pluggable Picnic.Session.TokenStore and an in-memory default.
  • Telemetry: [:picnic, :request, :start | :stop | :exception] and [:picnic, :schema, :drift].

  • Retries for transient failures only — network errors, 5xx, and 429 with backoff. Never for :auth or other 4xx.

Notes

  • The req dependency is declared as ~> 0.7, the range the test suite runs against.