{"openapi":"3.1.0","info":{"title":"BowlerIQ Partner API","summary":"Read-only bowling ball data for BowlerIQ partner platforms.","description":"\nRead-only bowling ball data for BowlerIQ partner platforms: specs per weight,\ncover and core, ball motion plotter positions with suggestions, and links to\nBowlerDepot Learn reviews and BowlerDepot product pages.\n\n## Getting started\n\n1. **Get a key.** BowlerIQ issues one key per partner platform. Keep it\n   server-side: never ship it in a browser or mobile app.\n2. **Send it on every request** as `Authorization: Bearer <your key>`.\n   To try calls on this page, click **Authentication** and paste your key;\n   it's sent only to `api.bowleriq.io`.\n3. **Load everything once**, then **keep in sync** with `GET /v1/changes`\n   (see *Syncing* below).\n\n```bash\ncurl -s -H \"Authorization: Bearer $BOWLERIQ_PARTNER_KEY\" \\\n  \"https://api.bowleriq.io/partner/v1/changes?limit=50\"\n```\n\n## Basics\n\n- **Format:** JSON. Every response includes `\"api_version\": \"1\"`.\n- **Rate limit:** about 10 requests/second, bursts of 20. Over that you get\n  `429`; back off and retry.\n- **Paging:** `limit` defaults to 200, maximum 500. Follow `next_cursor`.\n- **Ids** are stable UUIDs; use them as your keys. Each colorway is its own ball.\n\n## The v1 promise\n\nv1 only **grows**: we may add optional fields and new endpoints, so ignore\nfields you don't recognise. Within v1 we will never rename or remove a field\nor endpoint, change a field's type, units or meaning, or make an optional\nfield required. Any of those ships as `/v2`, and `/v1` keeps running while\nyou migrate.\n\n## Syncing\n\n```text\ncursor = load_saved_cursor()                    # None on the very first run\nloop:\n    page = GET /v1/changes?cursor=<cursor>      # omit cursor the first time\n    for change in page.items:\n        if change.change == \"upsert\": save(change.ball)   # create/replace by id\n        if change.change == \"remove\": delete(change.id)\n    cursor = page.next_cursor\n    save_cursor(cursor)                         # after applying the page\n    if not page.has_more: break\n```\n\n- Run it every 15-60 minutes. The **first run** returns every published ball\n  as an `upsert`; to start from a point in time pass `?since=2026-10-01T00:00:00Z`.\n- **Always store `next_cursor`** and send it next time. Don't use the time of\n  your last run as `since`: that can skip changes.\n- Re-running from your last saved cursor is safe; upserts and removes can be\n  applied more than once.\n- A ball appears once per page with its latest state. Changes appear about\n  60 seconds after they happen.\n- A ball counts as changed when its own fields, any weight's specs, its\n  images or its Learn review link change.\n- `demand` and `bigcommerce_product_id` are refreshed daily and do **not**\n  count as a change (they'd make every ball change every day). Read them\n  from `GET /v1/balls`; `demand.computed_at` says how fresh they are.\n\n## Errors\n\n| Status | Meaning |\n|---|---|\n| `400` | Bad `cursor` or `since`. Restart from your last good cursor. |\n| `401` / `403` | Missing or invalid key |\n| `404` | Ball not found or not published |\n| `429` | Rate limited; retry with backoff |\n| `5xx` | Our problem; retry with backoff |\n","version":"1"},"servers":[{"url":"https://api.bowleriq.io/partner","description":"Production"}],"paths":{"/v1/brands":{"get":{"tags":["Brands"],"summary":"Brands with at least one published ball","operationId":"get_brands_v1_brands_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandList"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/balls":{"get":{"tags":["Balls"],"summary":"All published balls (full load), paged by id","description":"Every published ball, current and retired. Good for a one-time full load; use /v1/changes to stay in sync.","operationId":"get_balls_v1_balls_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"next_cursor from the previous page.","title":"Cursor"},"description":"next_cursor from the previous page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BallPage"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"400":{"description":"Bad cursor or since.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/balls/{ball_id}":{"get":{"tags":["Balls"],"summary":"One published ball","operationId":"get_ball_v1_balls__ball_id__get","parameters":[{"name":"ball_id","in":"path","required":true,"schema":{"type":"string","title":"Ball Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BallResponse"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"404":{"description":"Not found or not published.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/changes":{"get":{"tags":["Sync"],"summary":"Sync feed: balls changed since your last cursor","description":"First sync: call with no cursor (or ?since=ISO time) and page until has_more is false. Store next_cursor; next time call ?cursor=<stored> to get only newer changes. 'upsert' = create/replace your copy; 'remove' = delete it. Changes appear about 60 seconds after they happen.","operationId":"get_changes_v1_changes_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"next_cursor from your previous call.","title":"Cursor"},"description":"next_cursor from your previous call."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO 8601 time to start from when you have no cursor.","title":"Since"},"description":"ISO 8601 time to start from when you have no cursor."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePage"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"400":{"description":"Bad cursor or since.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/plotter":{"get":{"tags":["Plotter"],"summary":"Ball motion plotter: positions + similar balls and directional steps for every current ball","operationId":"get_plotter_v1_plotter_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlotterList"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/patterns":{"get":{"tags":["Patterns"],"summary":"All published oil patterns (full load), paged by id","operationId":"get_patterns_v1_patterns_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"next_cursor from the previous page.","title":"Cursor"},"description":"next_cursor from the previous page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatternPage"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"400":{"description":"Bad cursor or since.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/patterns/changes":{"get":{"tags":["Patterns"],"summary":"Pattern sync feed: patterns changed since your last cursor","description":"Same rules as /v1/changes, for patterns: first sync with no cursor (or ?since=), page until has_more is false, store next_cursor, and next time pass ?cursor=<stored>. 'upsert' = create or replace your copy; 'remove' = delete it. Changes appear about 60 seconds after they happen.","operationId":"get_pattern_changes_v1_patterns_changes_get","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"next_cursor from your previous call.","title":"Cursor"},"description":"next_cursor from your previous call."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO 8601 time to start from when you have no cursor.","title":"Since"},"description":"ISO 8601 time to start from when you have no cursor."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatternChangePage"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"400":{"description":"Bad cursor or since.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/patterns/{pattern_id}":{"get":{"tags":["Patterns"],"summary":"One published oil pattern","operationId":"get_pattern_v1_patterns__pattern_id__get","parameters":[{"name":"pattern_id","in":"path","required":true,"schema":{"type":"string","title":"Pattern Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatternResponse"}}}},"401":{"description":"Missing key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"403":{"description":"Invalid key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"Rate limited; retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"404":{"description":"Not found or not published.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Ball":{"properties":{"id":{"type":"string","title":"Id","description":"Stable ball id (UUID). Colorways are separate balls."},"name":{"type":"string","title":"Name"},"brand":{"$ref":"#/components/schemas/Brand"},"status":{"type":"string","enum":["current","retired"],"title":"Status","description":"'retired' = no longer made; still a valid ball."},"release_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Release Date","description":"Manufacturer release date, when known."},"color":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Color","description":"Colorway as the manufacturer names it."},"coverstock":{"$ref":"#/components/schemas/Coverstock"},"core":{"$ref":"#/components/schemas/Core"},"has_particle":{"type":"boolean","title":"Has Particle","description":"True when the cover contains particle (additive) material."},"factory_finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Factory Finish","description":"Manufacturer's finish text, e.g. '500, 1000, 1500 Siaair / Factory Compound'."},"finish_category":{"anyOf":[{"type":"string","enum":["polished","satin","dull"]},{"type":"null"}],"title":"Finish Category","description":"The factory finish, grouped as polished, satin or dull."},"weights":{"items":{"$ref":"#/components/schemas/Weight"},"type":"array","title":"Weights","description":"Specs per weight, heaviest first. May be empty."},"plotter":{"anyOf":[{"$ref":"#/components/schemas/PlotterPosition"},{"type":"null"}],"description":"Position on the ball motion plotter; null if the ball isn't plotted."},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"Product photo (resized, CDN-hosted)."},"manufacturer_url":{"type":"string","title":"Manufacturer Url","description":"The ball's page on the manufacturer's site."},"learn_article_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Learn Article Url","description":"BowlerDepot Learn review, when one is published."},"bowlerdepot_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bowlerdepot Url","description":"BowlerDepot product page, when BowlerDepot carries the ball."},"content_changed_at":{"type":"string","format":"date-time","title":"Content Changed At","description":"When anything in this object last changed."},"bigcommerce_product_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bigcommerce Product Id","description":"The ball's product id in BowlerDepot's BigCommerce store, only when BowlerIQ matched it unambiguously and no other ball claims the same BigCommerce product; null otherwise. Not covered by content_changed_at."},"demand":{"anyOf":[{"$ref":"#/components/schemas/Demand"},{"type":"null"}],"description":"Demand signals, refreshed daily; null until the ball's first refresh. Not covered by content_changed_at: read it from /v1/balls, it won't appear in /v1/changes on its own."}},"additionalProperties":false,"type":"object","required":["id","name","brand","status","coverstock","core","has_particle","weights","manufacturer_url","content_changed_at"],"title":"Ball","examples":[{"bigcommerce_product_id":1234,"bowlerdepot_url":"https://bowlerdepot.com/storm-phaze-ii/","brand":{"id":"6a1f0c1e-2b5d-4c55-9a51-0f4e1b2c3d4e","name":"Storm"},"color":"Pitch Purple","content_changed_at":"2026-10-05T15:42:10.123Z","core":{"name":"Velocity","type":"symmetric"},"coverstock":{"material":"reactive_resin","name":"TX-16 Solid","type":"solid"},"demand":{"computed_at":"2026-10-29T06:00:03.512Z","daily_movement":0.42,"movement_skus":4,"popularity_score":48213.7,"popularity_videos":6,"score":0.8125},"factory_finish":"1500 Grit Polished","finish_category":"polished","has_particle":false,"id":"ff32beb3-57fc-4c61-abb4-c4cab2b46240","image_url":"https://img.bowleriq.io/product-images/ff32beb3/detail.png?w=800","learn_article_url":"https://learn.bowlerdepot.com/articles/storm-phaze-ii/","manufacturer_url":"https://www.stormbowling.com/phaze-ii","name":"PHAZE II","plotter":{"motion":14.4,"oil":9.1,"source":"estimated"},"release_date":"2018-01-25","status":"current","weights":[{"differential":0.051,"rg":2.48,"weight_lbs":16},{"differential":0.05,"rg":2.49,"weight_lbs":15}]}]},"BallPage":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"items":{"items":{"$ref":"#/components/schemas/Ball"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Pass as ?cursor= for the next page; null on the last page."}},"additionalProperties":false,"type":"object","required":["items"],"title":"BallPage"},"BallResponse":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"item":{"$ref":"#/components/schemas/Ball"}},"additionalProperties":false,"type":"object","required":["item"],"title":"BallResponse"},"BoardTotals":{"properties":{"forward_ul":{"items":{"type":"integer"},"type":"array","title":"Forward Ul","description":"µL laid on each board by the forward pass; 39 values, board 1 from the left first."},"reverse_ul":{"items":{"type":"integer"},"type":"array","title":"Reverse Ul","description":"µL laid on each board by the reverse pass; 39 values."}},"additionalProperties":false,"type":"object","required":["forward_ul","reverse_ul"],"title":"BoardTotals"},"Brand":{"properties":{"id":{"type":"string","title":"Id","description":"Stable brand id (UUID)."},"name":{"type":"string","title":"Name","description":"Brand name as the manufacturer writes it, e.g. 'Storm', 'Roto Grip'."}},"additionalProperties":false,"type":"object","required":["id","name"],"title":"Brand"},"BrandList":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"items":{"items":{"$ref":"#/components/schemas/Brand"},"type":"array","title":"Items"}},"additionalProperties":false,"type":"object","required":["items"],"title":"BrandList"},"Change":{"properties":{"id":{"type":"string","title":"Id","description":"Ball id."},"change":{"type":"string","enum":["upsert","remove"],"title":"Change","description":"'upsert' = create or replace your copy with `ball`; 'remove' = delete your copy (the ball is no longer published)."},"changed_at":{"type":"string","format":"date-time","title":"Changed At","description":"When the change happened."},"ball":{"anyOf":[{"$ref":"#/components/schemas/Ball"},{"type":"null"}],"description":"Present for 'upsert', null for 'remove'."}},"additionalProperties":false,"type":"object","required":["id","change","changed_at"],"title":"Change"},"ChangePage":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"items":{"items":{"$ref":"#/components/schemas/Change"},"type":"array","title":"Items"},"next_cursor":{"type":"string","title":"Next Cursor","description":"ALWAYS present. Store it and pass it as ?cursor= on your next sync to get only newer changes."},"has_more":{"type":"boolean","title":"Has More","description":"True if more changes are ready now; keep paging until false."}},"additionalProperties":false,"type":"object","required":["items","next_cursor","has_more"],"title":"ChangePage"},"Core":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Manufacturer's core name, e.g. 'Velocity'."},"type":{"anyOf":[{"type":"string","enum":["symmetric","asymmetric"]},{"type":"null"}],"title":"Type","description":"Symmetric or asymmetric core."}},"additionalProperties":false,"type":"object","title":"Core"},"Coverstock":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Manufacturer's coverstock name, e.g. 'HK22 - Aggression Pearl'."},"material":{"anyOf":[{"type":"string","enum":["reactive_resin","urethane","polyester_plastic"]},{"type":"null"}],"title":"Material","description":"Cover chemistry family."},"type":{"anyOf":[{"type":"string","enum":["solid","pearl","hybrid"]},{"type":"null"}],"title":"Type","description":"Solid, pearl or hybrid cover."}},"additionalProperties":false,"type":"object","title":"Coverstock"},"Demand":{"properties":{"score":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Score","description":"Demand score, 0 to 1 inclusive: a 50/50 blend of this ball's percentile rank on popularity_score and on daily_movement across the whole BowlerIQ catalog. Use it only when popularity_videos and movement_skus are both above 0."},"popularity_score":{"type":"number","title":"Popularity Score","description":"Video popularity: half-life-weighted average view count of approved review videos x ln(1 + video count). 0 when popularity_videos is 0."},"popularity_videos":{"type":"integer","title":"Popularity Videos","description":"Approved, view-counted videos popularity_score rests on. 0 = no data, NOT low popularity."},"daily_movement":{"type":"number","title":"Daily Movement","description":"Units sold per day at BowlerDepot, summed over the ball's SKUs (stock drops over the trailing 30 days). 0 when movement_skus is 0."},"movement_skus":{"type":"integer","title":"Movement Skus","description":"SKUs with qualifying stock history in the trailing 30 days that daily_movement rests on. 0 = no data, NOT 'nothing moved'."},"computed_at":{"type":"string","format":"date-time","title":"Computed At","description":"When the daily refresh that produced these values ran."}},"additionalProperties":false,"type":"object","required":["score","popularity_score","popularity_videos","daily_movement","movement_skus","computed_at"],"title":"Demand","description":"BowlerIQ's demand signals for one ball (runbook 6fg). Added to v1 as\nan optional field for Spurline (sourceware-sync), which bands `score`\ninto custom_label_4 of bowlerdepot.com's Google Merchant feed -- and\nuses it only when BOTH counts are above zero, because the underlying\ncolumns (migration 030) store 0 for \"no data\". The counts are what let\na partner tell \"unpopular\" from \"we have no videos for this ball\"."},"ErrorDetail":{"properties":{"detail":{"type":"string","title":"Detail","description":"What went wrong."}},"additionalProperties":false,"type":"object","required":["detail"],"title":"ErrorDetail"},"GatewayError":{"properties":{"message":{"type":"string","title":"Message","description":"'Unauthorized', 'Forbidden' or 'Too Many Requests'."}},"additionalProperties":false,"type":"object","required":["message"],"title":"GatewayError"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"Pattern":{"properties":{"id":{"type":"string","title":"Id","description":"Stable pattern id (UUID)."},"name":{"type":"string","title":"Name"},"short_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Name"},"category":{"type":"string","enum":["house","sport","challenge","recreational","tournament"],"title":"Category"},"series":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series"},"source":{"type":"string","enum":["kegel","pba","usbc","wtba","brunswick","center","manual","upload"],"title":"Source","description":"Where BowlerIQ got it; 'upload' = a sheet uploaded by BowlerIQ staff."},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"sheet_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sheet Url","description":"The original publisher's sheet; null for uploads."},"image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Image Url","description":"The original publisher's graph image; null for uploads."},"status":{"type":"string","enum":["current","retired"],"title":"Status"},"length_feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Length Feet","description":"Oil pattern distance, feet."},"reverse_brush_drop_feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reverse Brush Drop Feet"},"forward_brush_drop_feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Forward Brush Drop Feet","description":"Where the buffer brush comes down on the forward pass, feet from the foul line (Kegel's forward drop brush, published in inches: 9 in = 0.75). Null = down from the foul line."},"oil_per_board_ul":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Oil Per Board Ul","description":"µL laid on one board by one load."},"forward_ml":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Forward Ml"},"reverse_ml":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reverse Ml"},"volume_ml":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Volume Ml"},"ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Ratio","description":"Headline ratio: the largest track zone ratio."},"download_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Download Count","description":"Times the pattern has been downloaded from its publisher's library (Kegel), as of BowlerIQ's last import; a popularity signal for ordering. Null for patterns from uploaded sheets."},"track_zones":{"items":{"$ref":"#/components/schemas/TrackZone"},"type":"array","title":"Track Zones"},"machine":{"$ref":"#/components/schemas/PatternMachine"},"steps":{"items":{"$ref":"#/components/schemas/PatternStep"},"type":"array","title":"Steps","description":"The machine program: forward steps, then reverse, each in order."},"board_totals":{"anyOf":[{"$ref":"#/components/schemas/BoardTotals"},{"type":"null"}]},"profile":{"anyOf":[{"$ref":"#/components/schemas/PatternProfile"},{"type":"null"}],"description":"Oil laid on each board in each foot (computed from steps)."},"profile_source":{"type":"string","enum":["steps","graph"],"title":"Profile Source"},"content_changed_at":{"type":"string","format":"date-time","title":"Content Changed At","description":"When anything in this object last changed."}},"additionalProperties":false,"type":"object","required":["id","name","category","source","status","track_zones","machine","steps","profile_source","content_changed_at"],"title":"Pattern"},"PatternChange":{"properties":{"id":{"type":"string","title":"Id","description":"Pattern id."},"change":{"type":"string","enum":["upsert","remove"],"title":"Change","description":"'upsert' = create or replace your copy with `pattern`; 'remove' = delete it (no longer published)."},"changed_at":{"type":"string","format":"date-time","title":"Changed At"},"pattern":{"anyOf":[{"$ref":"#/components/schemas/Pattern"},{"type":"null"}],"description":"Present for 'upsert', null for 'remove'."}},"additionalProperties":false,"type":"object","required":["id","change","changed_at"],"title":"PatternChange"},"PatternChangePage":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"items":{"items":{"$ref":"#/components/schemas/PatternChange"},"type":"array","title":"Items"},"next_cursor":{"type":"string","title":"Next Cursor","description":"ALWAYS present. Store it and pass it as ?cursor= next time."},"has_more":{"type":"boolean","title":"Has More"}},"additionalProperties":false,"type":"object","required":["items","next_cursor","has_more"],"title":"PatternChangePage"},"PatternMachine":{"properties":{"make":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Make","description":"e.g. 'Kegel'."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"e.g. 'Flex'."},"tank_configuration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tank Configuration"},"tank_a_conditioner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tank A Conditioner"},"tank_b_conditioner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tank B Conditioner"},"cleaner_ratio_main":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cleaner Ratio Main"},"cleaner_ratio_back_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cleaner Ratio Back End"},"cleaner_back_end_distance_feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cleaner Back End Distance Feet"},"buffer_rpm":{"anyOf":[{"additionalProperties":{"type":"number"},"type":"object"},{"type":"null"}],"title":"Buffer Rpm","description":"Buffer setting -> RPM, e.g. {'3': 500}."}},"additionalProperties":false,"type":"object","title":"PatternMachine"},"PatternPage":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"items":{"items":{"$ref":"#/components/schemas/Pattern"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Pass as ?cursor= for the next page; null on the last page."}},"additionalProperties":false,"type":"object","required":["items"],"title":"PatternPage"},"PatternProfile":{"properties":{"foot_step":{"type":"number","title":"Foot Step","description":"Row height in feet (1)."},"units":{"type":"string","const":"ul_per_board_per_foot","title":"Units"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"How the machine program was turned into lane oil. \"transfer_buffer_carry_v4\": each step's loads go onto the transfer brush, the buffer brush picks them up and lays them on the lane, so oil drags out in each run's direction of travel and thins; on the reverse run the buffer brush is up until reverse_brush_drop_feet and on the forward run until forward_brush_drop_feet; oil left on the brushes carries into the next run and the next lane, and rows are the lane after a burn pair (see the patterns spec)."},"burn_lanes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Burn Lanes","description":"Lanes oiled first to condition the brushes; rows are the next lane (2 = a burn pair)."},"rows":{"items":{"items":{"type":"number"},"type":"array"},"type":"array","title":"Rows","description":"Row i covers feet [i*foot_step, (i+1)*foot_step) from the foul line; 39 values per row, board 1 from the left first, µL to 0.1. Sums to volume_ml x 1000."}},"additionalProperties":false,"type":"object","required":["foot_step","units","rows"],"title":"PatternProfile"},"PatternResponse":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"item":{"$ref":"#/components/schemas/Pattern"}},"additionalProperties":false,"type":"object","required":["item"],"title":"PatternResponse"},"PatternStep":{"properties":{"direction":{"type":"string","enum":["forward","reverse"],"title":"Direction"},"order":{"type":"integer","title":"Order","description":"Position in that direction's program, from 1."},"start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start","description":"Start board as the sheet labels it (e.g. '2L')."},"stop":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stop","description":"Stop board label (e.g. '2R')."},"from_board":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"From Board","description":"Lower board, 1-39 from the left."},"to_board":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To Board","description":"Upper board, 1-39 from the left."},"loads":{"type":"integer","title":"Loads","description":"Passes of oil; 0 = a buff-only pass."},"speed_ips":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Speed Ips","description":"Machine speed, inches per second."},"buffer":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Buffer","description":"Buffer setting (1-4)."},"tank":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tank"},"crossed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Crossed","description":"Board crossings = boards x loads."},"mics":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Mics","description":"µL per board per load for this step (the sheet's MICS column, else the pattern's oil_per_board_ul)."},"start_feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Start Feet","description":"Feet from the foul line; greater than end_feet for reverse."},"end_feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"End Feet"},"feet":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Feet","description":"The sheet's distance column (negative for reverse)."},"oil_ul":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Oil Ul","description":"Oil this step lays, µL = crossed x mics."}},"additionalProperties":false,"type":"object","required":["direction","order","loads"],"title":"PatternStep"},"PlotterEntry":{"properties":{"id":{"type":"string","title":"Id","description":"Ball id."},"plotter":{"$ref":"#/components/schemas/PlotterPosition"},"neighbors":{"$ref":"#/components/schemas/PlotterNeighbors","description":"Suggestions are limited to balls BowlerDepot sells; colorways of the same ball are collapsed."}},"additionalProperties":false,"type":"object","required":["id","plotter","neighbors"],"title":"PlotterEntry"},"PlotterList":{"properties":{"api_version":{"type":"string","const":"1","title":"Api Version","default":"1"},"items":{"items":{"$ref":"#/components/schemas/PlotterEntry"},"type":"array","title":"Items","description":"Every current, published ball."}},"additionalProperties":false,"type":"object","required":["items"],"title":"PlotterList"},"PlotterNeighbor":{"properties":{"id":{"type":"string","title":"Id","description":"Ball id."},"distance":{"type":"number","title":"Distance","description":"Distance on the plotter, in plotter units."}},"additionalProperties":false,"type":"object","required":["id","distance"],"title":"PlotterNeighbor"},"PlotterNeighbors":{"properties":{"twins":{"items":{"$ref":"#/components/schemas/PlotterNeighbor"},"type":"array","title":"Twins","description":"Closest balls from OTHER brands (within 1.5 units)."},"more_oil":{"items":{"$ref":"#/components/schemas/PlotterNeighbor"},"type":"array","title":"More Oil","description":"Nearest step toward heavier oil."},"less_oil":{"items":{"$ref":"#/components/schemas/PlotterNeighbor"},"type":"array","title":"Less Oil","description":"Nearest step toward lighter oil."},"more_angular":{"items":{"$ref":"#/components/schemas/PlotterNeighbor"},"type":"array","title":"More Angular","description":"Nearest step toward a more angular backend."},"smoother":{"items":{"$ref":"#/components/schemas/PlotterNeighbor"},"type":"array","title":"Smoother","description":"Nearest step toward a smoother backend."}},"additionalProperties":false,"type":"object","required":["twins","more_oil","less_oil","more_angular","smoother"],"title":"PlotterNeighbors"},"PlotterPosition":{"properties":{"oil":{"type":"number","title":"Oil","description":"Oil the ball handles best: 1.0 (light) to 16.0 (heavy), to 0.1."},"motion":{"type":"number","title":"Motion","description":"Backend motion shape: 1.0 (smooth) to 18.0 (angular), to 0.1."},"source":{"type":"string","enum":["chart","estimated","manual"],"title":"Source","description":"'chart' = read off a manufacturer's published ball motion chart; 'estimated' = BowlerIQ's model; 'manual' = staff correction."}},"additionalProperties":false,"type":"object","required":["oil","motion","source"],"title":"PlotterPosition"},"TrackZone":{"properties":{"from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From","description":"Zone start board, as the sheet labels it (e.g. '3L')."},"to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To","description":"Zone end board label (e.g. '7L')."},"against_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Against From","description":"Comparison zone start label (e.g. '18L')."},"against_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Against To","description":"Comparison zone end label (e.g. '18R')."},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label","description":"The sheet's description, e.g. 'Outside:Middle'."},"ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Ratio","description":"Oil ratio between the two zones: the average total oil per board (board_totals forward + reverse) of the zone nearer the middle over the other's, to 0.01. Computed from the steps."}},"additionalProperties":false,"type":"object","title":"TrackZone"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Weight":{"properties":{"weight_lbs":{"type":"integer","title":"Weight Lbs","description":"Ball weight in pounds (typically 10-16)."},"rg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rg","description":"Radius of gyration, inches (e.g. 2.49)."},"differential":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Differential","description":"Total differential, inches (e.g. 0.054)."},"mass_bias":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Mass Bias","description":"Intermediate differential, inches. Null for symmetric cores."}},"additionalProperties":false,"type":"object","required":["weight_lbs"],"title":"Weight"}},"securitySchemes":{"PartnerKey":{"type":"http","scheme":"bearer","description":"Your BowlerIQ partner key, sent as `Authorization: Bearer <key>`."}}},"tags":[{"name":"Sync","description":"Keep your copy of the catalog up to date. Start here."},{"name":"Balls","description":"Full loads and single-ball lookups."},{"name":"Brands","description":"Brands with published balls."},{"name":"Plotter","description":"Ball motion plotter positions and suggestions."},{"name":"Patterns","description":"Oil patterns: the machine program, board totals and a per-board, per-foot oil grid (`profile`), synced like balls with their own feed. Every pattern is checked against its sheet and approved by BowlerIQ staff before it appears here."}],"security":[{"PartnerKey":[]}]}