Optiply is a Dutch inventory forecasting and purchasing tool. It reads a webshop's sales history and stock, forecasts demand per product, and turns that into supplier purchase orders with the right quantity at the right time. It is bought by ecommerce operators who are tired of guessing reorder points. For our purposes it is the most tractable platform in the analytics group: it has a genuine public REST API at api.optiply.com/v1, it is a JSON:API implementation with predictable filtering and pagination, and Optiply maintains its own open source Singer tap and target on GitHub, which means the resource list, the field names, the mandatory fields on writes and the authentication flow can all be read from first party code rather than guessed. Optiply does not publish a rendered API reference at any resolvable address, so those repositories are the reference.
At a glance
Direction matters. Optiply is a planning system that sits downstream of the shop and upstream of the supplier. Writing a product or a stock level into Optiply changes what Optiply forecasts, it does not change a price or a quantity on Shopify or Amazon. If the goal is auto updating listings, Optiply is the brain, not the hand.
What it is
Optiply sells demand forecasting, stock optimisation and automated purchasing to ecommerce and retail operators, with the site published in English, Dutch and Spanish and the dashboard still served from dashboard.optiply.nl, which points to a Netherlands origin. The integration catalogue is heavily Benelux weighted and confirms the market: Picqer, Monta, Goedgepickt, QLS, BizBloqs, Sherpaan and ChannelDock on the warehouse side, Exact Online, Exact Globe, AFAS, Logic4, Unit4 and Bexio on the ERP side, Bol.com, Channable, ChannelEngine and EffectConnect on the marketplace side, and Lightspeed, Shopware, Gambio and Quickbutik among the shop systems. Alongside those it lists the global names: Shopify, WooCommerce, Magento, BigCommerce, PrestaShop, Wix, Ecwid, Salesforce Commerce Cloud, NetSuite, Odoo, Microsoft Dynamics 365, SAP Business One, Sage, Zoho, ShipBob, ShipHero, Ordoro, SkuVault, Ongoing WMS, Amazon Seller, Amazon Fulfillment and EasyEcom. Databases are a first class destination too: Redshift, PostgreSQL, Snowflake, BigQuery and Microsoft SQL. A "Custom API" option is listed under Other, described only as "easily connect your own systems and integrate seamlessly".
The data model is purchasing shaped rather than commerce shaped. Its nouns are products, suppliers, the relationship between a product and a supplier, orders out to customers, orders in to suppliers, and receipts against those purchase orders. There is no customer, no shipment, no return and no settlement anywhere in it. Sell orders exist mainly as demand history to forecast from.
API access
Credentials are issued to Optiply customers. The target's configuration table names exactly four required settings, username, client_id, client_secret and password, plus two optional ones that scope the calls, account_id and coupling_id. A coupling is Optiply's term for a connected shop or system inside an account, so a customer with two webshops has two couplings under one account, and that is how multi account handling works: one credential set, an accountId on every read filter and an accountId plus couplingId on writes.
Two official connectors are published and they are the practical SDK:
Optiply/optiply-integrations also exists and holds the taps and targets Optiply wrote for other systems, including Exact, ItsPerfect, Vendit and Colleqtive. It is a useful cross check on how Optiply itself models a connected shop, and it is not an Optiply API reference.
No API version other than v1 appears anywhere, and no deprecation notice was found.
Request https://api.optiply.com/v1/ with the trailing slash. Without it the host answers HTTP 302 to an internal Kubernetes service name, http://public-api-node-2.public-api.svc.cluster.local/v1/, which is unroutable from outside and will make a naive client fail in a confusing way. With the trailing slash it answers HTTP 401 as expected. Confirmed 2026-09-22.
Authentication
OAuth 2.0 password grant, with the client credentials carried as HTTP Basic on the token call. Taken from tap_optiply/auth.py and target_optiply/auth.py, where the token URL is a class constant commented as "Fixed token URL that will never change".
- Build
base64(client_id + ":" + client_secret)and send it asAuthorization: Basic <that>. POST https://dashboard.optiply.nl/api/auth/oauth/tokenwithContent-Type: application/x-www-form-urlencodedand a body ofgrant_type=password,usernameandpassword.- The response is JSON containing
access_tokenandexpires_inin seconds. Optiply's own code stores the absolute expiry asnow + expires_in. - Send
Authorization: Bearer <access_token>on every API call, together withContent-Type: application/vnd.api+jsonand, on writes,Accept: application/vnd.api+json. - Refresh before expiry rather than after. The tap refreshes when fewer than 300 seconds remain, the target when fewer than 120 remain. There is no separate refresh token in the flow: you repeat the password grant.
- On HTTP 401 the target refreshes the token and retries the request with exponential backoff, which is the documented behaviour to copy.
Note that the token host and the API host are different systems, dashboard.optiply.nl and api.optiply.com. Do not assume the token endpoint moves with the API base URL, and do not assume the .nl domain is a regional variant: dashboard.optiply.nl redirects human traffic to app.optiply.com, but the /api/auth/oauth/token path under the .nl host is what Optiply's own code calls.
Scopes are not published. The token appears to carry whatever the user account can do, with accountId and couplingId doing the scoping at request level.
obtain a token BASIC=$(printf '%s:%s' "$OPTIPLY_CLIENT_ID" "$OPTIPLY_CLIENT_SECRET" | base64) curl -sS -X POST "https://dashboard.optiply.nl/api/auth/oauth/token" \ -H "Authorization: Basic $BASIC" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=password" \ --data-urlencode "username=$OPTIPLY_USER" \ --data-urlencode "password=$OPTIPLY_PASS"
an authenticated, filtered, paged read curl -sS "https://api.optiply.com/v1/products?page%5Blimit%5D=100&filter%5BaccountId%5D=$ACCOUNT_ID&filter%5BupdatedAt%5D%5BGT%5D=2026-09-01T00:00:00+00:00" \ -H "Authorization: Bearer $OPTIPLY_TOKEN" \ -H "Content-Type: application/vnd.api+json"
Objects we can read
Every resource below is a GET on https://api.optiply.com/v1/<path>. Records come back under the JSON:API envelope, which Optiply's tap extracts with the JSONPath $.data[*], so each element has id, type and the attributes listed. Every resource has id as its primary key and updatedAt as its replication key, and every one supports the same three parameters: page[limit], filter[accountId] and filter[updatedAt][GT]. Optiply publishes no sample responses, so the field lists below are given as tables rather than invented JSON.
Orders
GET /sellOrders and GET /sellOrderLines
Sell orders are the customer demand Optiply forecasts from. They are deliberately thin: no customer, no address, no payment, no status beyond timestamps.
placed is the order date and completed is when it was fulfilled. remoteIdMap is the bridge back to the source system's identifiers, which is the field to mine when joining Optiply rows to Shopify or Amazon rows. createdFromPublicApi tells you whether the record was pushed in through this API or pulled in by an Optiply connector, which is useful for detecting double writes.
Sample response not published.
Order items
Covered by sellOrderLines above. Note the line carries productId, Optiply's own integer product id, not a SKU, so joining to a channel requires the product record.
Products and listings
GET /products
There is no listing concept, no channel and no URL. Optiply holds one product record per account per SKU, not one per sales channel.
Sample response not published.
Inventory
There is no inventory resource. Stock lives on the product as stockLevel, with minimumStock, maximumStock and unlimitedStock alongside it, and inbound stock is derived from buy orders and receipts rather than stored. Supplier side availability is on supplierProducts as freeStock, availability and availabilityDate.
Read this carefully before mapping: nothing in the published model carries a warehouse or location identifier, so a single Optiply product appears to hold one stock number, not a number per location. That is consistent with the product being a forecasting tool for a single stock pool. Confirm it with Optiply before modelling a multi warehouse client.
GET /receiptLines is the inbound record:
occurred is when the goods were received against a buy order line, which makes receiptLines the source for inventory.quantity_inbound settling.
Suppliers and purchase orders
These have no equivalent in the unified model and are the heart of the product, so they are worth listing in full.
preferred on a supplier product is the flag that decides which supplier Optiply buys from when several can supply the same product, and lotSize with minimumPurchaseQuantity are what round the forecast up to a real order quantity.
Promotions and compositions
Promotions are a forecasting input: an uplift percentage applied over a date range so that Optiply buys ahead of a sale. Product compositions are the bill of materials for bundles and assembled products.
Shipments, returns, settlements, customers, locations
None of these exist. There is no shipment, no waybill, no carrier, no return, no refund, no settlement, no customer record and no location or warehouse resource anywhere in the published model. Returnista appears in the integration list as a data provider, so returns data can reach Optiply, but no returns resource is exposed on the API.
Writing back: listings, price and stock
Optiply's API is genuinely read and write, which is unusual in this group. The semantics come from target_optiply/sinks.py and are consistent across every resource.
- Create:
POST /<resource>with a JSON:API request body. - Update:
PATCH /<resource>/<id>with the same body plusdata.idset to the record id. The target chooses the method purely on whether the incoming record carries anid. - Request body shape:
{"data": {"type": "<resource>", "attributes": { ... }}}, wheretypeis the resource path segment, for exampleproductsorbuyOrderLines. - Scoping:
accountIdandcouplingIdare appended as ordinary query parameters on the request URL, not placed in the body. - Headers:
Content-Type: application/vnd.api+jsonandAccept: application/vnd.api+json. - Response: the created or updated record comes back with
data.id, which is how the target records the new identifier.
Mandatory attributes per resource, as enforced by Optiply's own target:
Three practical rules Optiply states in its own README and that will cost you a day if ignored. First, productId must be the Optiply integer product id: if your source has a SKU, EAN or article code, resolve it to the Optiply id before writing. Second, promotions and promotion products must be written in the same run and in that order, because the promotion id only exists after the promotion is created. Third, records missing a mandatory field are skipped rather than rejected loudly, so a silent gap in your write is the expected failure mode and you need your own count check.
What this does not do is update a listing on a sales channel. There is no price push, no stock push and no listing activation toward Shopify, Amazon or Bol.com in this API. Optiply's own connectors read from those systems. If a brand wants Optiply's suggested order to become a real purchase order in their ERP, that is the ERP's API, not this one.
Batch size is not documented and the target writes one record per request.
Webhooks and notifications
None found. No subscription resource appears in either repository, no signature header is referenced anywhere in the client code, and the marketing site does not mention push notifications.
Poll instead, and the API is built for it: every resource carries updatedAt as a replication key and accepts filter[updatedAt][GT], which is a strictly greater than comparison against an ISO timestamp with an offset, written as +00:00 rather than Z in Optiply's own code. The pattern to copy is to keep a high water mark per resource, request filter[updatedAt][GT]=<last seen> and follow links.next until it is absent.
Cadence should follow the business rhythm rather than the clock: purchasing decisions are daily at best, so hourly is generous for products, supplierProducts, buyOrders, buyOrderLines and receiptLines, and daily is enough for suppliers, promotions, promotionProducts and productCompositions. If you are mirroring sellOrders for demand history, match whatever cadence the upstream shop connector runs at, because Optiply cannot show you an order it has not itself ingested yet.
Rate limits and pagination
No published limit, no quota headers observed. What Optiply's own tap does, and therefore what it considers safe, is the best available guidance: a 60 second request timeout, at most 3 retries, an exponential backoff factor of 1, and retry only on HTTP 408, 429, 500, 502, 503 and 504. The presence of 429 in that list confirms that a rate limit exists and is enforced, even though its value is not published. Treat a 429 as authoritative, honour any Retry-After you see, and keep to a single concurrent request per account until Optiply says otherwise.
Pagination is JSON:API style. Set page[limit], which the tap defaults to 100, and follow links.next from the response body. There is no page number to compute and no total count to rely on: the absence of a next link is the termination condition. Because paging is combined with a filter[updatedAt][GT] high water mark rather than a frozen snapshot, records edited mid scan can move, so re-read a small overlap window on the next run rather than trusting a single pass.
Mapping to the unified model
Purchasing has no home in the unified model at all. suppliers, supplierProducts, buyOrders, buyOrderLines and receiptLines are genuinely new objects, and they are the reason to integrate Optiply rather than read the shop directly. If the unified model grows a purchasing side, Optiply's shape is a reasonable starting point because it is already normalised and already exposed through a real API.
Gaps and open questions
- No rendered API reference could be found at any resolvable address.
optiply.stoplight.ioexists, which strongly suggests a Stoplight hosted reference is or was published, but no public project was reachable and the Internet Archive was offline on the day of checking. Ask Optiply for the documentation URL before assuming the connector code is complete. - Rate limits are unpublished. The retry list confirms 429 exists but not the threshold.
- No sandbox was found. Building against a live customer account is not acceptable, so this needs answering early.
- The absence of any location or warehouse field is the biggest modelling risk. Either Optiply is single stock pool by design, or multi warehouse is handled somewhere not exposed by the tap.
- Currency is not carried on any money field, and all money fields are strings. Confirm that an account is single currency before aggregating.
- Whether
filtersupports operators other thanGT, and whether other fields thanupdatedAtandaccountIdare filterable, could not be established: those are the only two the tap uses. - Response error shapes are not documented. The target logs 400, 404 and 500 class errors differently but does not record their bodies.
- The
/v1redirect to an internal cluster hostname is a small information leak on Optiply's side and is worth mentioning to them.
Sources
- github.com/Optiply/tap-optiply,
tap_optiply/auth.py,client.pyandstreams.py, read 2026-09-22: token URL and refresh behaviour, base URL, media type,page[limit],filter[accountId],filter[updatedAt][GT],links.nextpagination, retry and timeout policy, and the complete field list for all eleven readable resources. - github.com/Optiply/target-optiply,
README.md,auth.py,client.pyandsinks.py, read 2026-09-22: the password grant, the configuration contract, endpoint list, mandatory fields, JSON:API request body shape, POST versus PATCH selection,accountIdandcouplingIdquery parameters, error handling and the promotion ordering contract. - github.com/Optiply/optiply-integrations, directory listing read 2026-09-22: Optiply's own taps and targets for Exact, ItsPerfect, Vendit and Colleqtive.
- Live checks on
https://api.optiply.com/v1andhttps://api.optiply.com/v1/, 2026-09-22: the internal redirect without the trailing slash, HTTP 401 with it. - Host checks on
developers.,developer.,docs.,api-docs.anddashboard.optiply.com, plusdashboard.optiply.nlandoptiply.stoplight.io, 2026-09-22. - www.optiply.com/en/integrations, read 2026-09-22: the integration catalogue by category and the Custom API entry.
- www.optiply.com/sitemap.xml, read 2026-09-22: the public page list, which contains no developer section.