UTC --:--:--
Docs·API reference·POST /v1/score/explain-counterfactual

Counterfactual analysis of a score

"What's holding the verdict down?", "If wind picked up 3 knots, would we go?", "Is there a better window today?", "Is a nearby spot better?". One endpoint, four answers, pure math (no LLM in the analytical blocks).

POSThttps://api.goable.io/v1/score/explain-counterfactual

Any authenticated plan. The four analytical blocks below are available on Free and up; the math typically runs in tens of milliseconds (the engine directly probes per-dimension perturbations without re-fetching weather, plus one extra provider fetch for the 24h window scan). The optional natural_language block also works on any plan, but needs your own Anthropic key set; with no key, natural_language.degraded_mode: true and a deterministic template stands in for a generated sentence.

What it answers

Four analyses returned in a single response:

  • binding_constraint: which dimension is dragging the verdict down hardest.
  • marginal_sensitivities: ∂score/∂metric for the top-5 dimensions, so you can answer "by how much does X need to change to flip the verdict?".
  • best_window_24h: hourly scan over a ±12h window, returning the single best-scoring hour + its top-3 explanatory drivers (null if nothing beats the baseline meaningfully).
  • best_nearby_spots: sub-spots within radius re-scored against the same request (baseline excluded by slug).

Request

Unlike the intelligence endpoints, this one re-runs the baseline score itself: there's no session_id / scoreResult passthrough. You always send activity + spot + window; the four analytical blocks are opt-out (default on) via a nested include object, not a string array:

{
 "activity": "kitesurfing",
 "spot": { "lat": 36.013, "lng": -5.604 },
 "window": { "from": "2026-07-04T09:00:00Z", "to": "2026-07-04T18:00:00Z" },
 "include": {
   "binding_constraint": true,
   "marginal_sensitivities": true,
   "best_window_24h": true,
   "best_nearby_spot_km": 25,
   "natural_language": true
 }
}

Every key in include is optional and defaults to true (50km for best_nearby_spot_km, capped at 200km) except natural_language, which defaults to false; set it explicitly to opt into the LLM block.

binding_constraint

Drag analysis on the dimension level: which dimension, if it alone moved to its optimal value, would lift the score the most. The result carries the dimension name + metric, its current contribution, the drag_pts it's costing, the would_become_score if it moved to optimum, and a limiting_class tag (wind / wave / thermal / visibility / precipitation / hard_gate / other). Useful for UI copy like "wind is OK but waves are the blocker, try after 3pm when the swell drops".

marginal_sensitivities

Numerical Jacobian: for each dimension, the engine perturbs the underlying metric by ±σ and ±2σ and observes the resulting score_delta. Each entry carries name, metric, current_value, weight, current_contribution, the per-perturbation array, and local_slope_per_unit, the least-squares slope ∂score/∂metric. Use for: "by how much does the wind need to increase to push us from marginal to favorable?".

best_window_24h

Hourly scan over a ±12h window around the request's midpoint (24h total, clipped to whatever horizon the provider actually returns), scoring each hour with the same activity profile. Returns the single best time + score + score_delta_vs_request, with the top-3 drivers that explain the lift over the baseline. Null when the provider fetch fails or returns too few samples.

best_nearby_spots

Finds sub-spots within include.best_nearby_spot_km (max 200km) via the spatial resolver's R-tree, re-scores each against the same baseline conditions, excludes the baseline spot by slug (so the source spot doesn't appear twice), drops any candidate that isn't at least 2 points better than the baseline, and returns up to the top 3 survivors sorted by lift, each with slug, score, score_delta_vs_request, distance_km, tier, and its top-3 drivers. Empty array when no spatial resolver is wired or nothing clears the bar.