{
	"openapi": "3.1.0",
	"info": {
		"title": "onRun Public API",
		"version": "1.0.0",
		"description": "Agent-facing API for onRun: local running events, runners, clubs, and a runner's public race highlights. Most endpoints are unauthenticated and only ever return data that is already public on onrun.com. Two surfaces require a Firebase ID token and act on behalf of that specific user: /api/v1/me (read) and /api/v1/checkout_sessions* (write — an Agentic Commerce Protocol checkout that registers the user for a paid event). See /auth.md, and /.well-known/acp.json for ACP discovery.",
		"x-service-info": {
			"title": "onRun Agent Commerce",
			"description": "Running race registrations, club memberships, and event ticketing",
			"categories": ["sports", "fitness", "events", "ticketing", "community"],
			"pricing": "freemium"
		}
	},
	"servers": [{ "url": "https://onrun.com" }],
	"security": [],
	"paths": {
		"/api/v1/events": {
			"get": {
				"summary": "Search public events",
				"operationId": "searchEvents",
				"parameters": [
					{ "name": "name", "in": "query", "schema": { "type": "string" }, "description": "Case-insensitive prefix match against the event's name." },
					{ "name": "city", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive city filter." },
					{ "name": "state", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive state filter." },
					{ "name": "type", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive filter against the organizer-entered run type (e.g. \"Social Run\", \"Track Workout\", \"Tempo Run\") — not a fixed enum." },
					{ "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "description": "Capped at 50 regardless of requested value." },
					{ "name": "distanceMin", "in": "query", "schema": { "type": "number", "minimum": 0 }, "description": "Minimum event distance in miles. Best-effort filter applied within the capped result window, not an exact cross-database filter." },
					{ "name": "distanceMax", "in": "query", "schema": { "type": "number", "minimum": 0 }, "description": "Maximum event distance in miles. Best-effort filter applied within the capped result window, not an exact cross-database filter." },
					{ "name": "dateFrom", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Only events on or after this ISO 8601 date/datetime. Best-effort filter applied within the capped result window, not an exact cross-database filter." },
					{ "name": "dateTo", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Only events on or before this ISO 8601 date/datetime. Best-effort filter applied within the capped result window, not an exact cross-database filter." },
					{ "name": "isFree", "in": "query", "schema": { "type": "boolean" }, "description": "Filter to free (`true`) or paid (`false`) events. Events with no known price are retained regardless of this filter." },
					{ "name": "priceMax", "in": "query", "schema": { "type": "number", "minimum": 0 }, "description": "Maximum price in dollars. Events with no known price are retained regardless of this filter." }
				],
				"responses": {
					"200": {
						"description": "Matching public events.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"count": { "type": "integer" },
										"events": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } }
									},
									"required": ["count", "events"]
								}
							}
						}
					},
					"503": {
						"description": "The event search backend failed (e.g. a missing Firestore index) — not the same as zero matching events.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpstreamUnavailable" } } }
					}
				}
			}
		},
		"/api/v1/events/{id}": {
			"get": {
				"summary": "Get a single public event",
				"operationId": "getEvent",
				"parameters": [
					{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
				],
				"responses": {
					"200": {
						"description": "The event.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Event" } } }
					},
					"404": {
						"description": "The event does not exist or is private.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFound" } } }
					}
				}
			}
		},
		"/api/v1/profile/{id}/races": {
			"get": {
				"summary": "Get a runner's public race highlights",
				"operationId": "getProfileRaces",
				"parameters": [
					{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The runner's user id." }
				],
				"responses": {
					"200": {
						"description": "The runner's profile and public race highlights.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"id": { "type": "string" },
										"displayName": { "type": "string" },
										"handle": { "type": "string", "nullable": true },
										"url": { "type": "string", "format": "uri" },
										"races": { "type": "array", "items": { "$ref": "#/components/schemas/RaceHighlight" } }
									},
									"required": ["id", "displayName", "url", "races"]
								}
							}
						}
					},
					"404": {
						"description": "The profile does not exist or is private.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFound" } } }
					},
					"503": {
						"description": "The profile metadata or race-highlights backend failed — not the same as a runner with no public race highlights.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpstreamUnavailable" } } }
					}
				}
			}
		},
		"/api/v1/runners": {
			"get": {
				"summary": "Search public runners",
				"operationId": "searchRunners",
				"parameters": [
					{ "name": "q", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Case-insensitive prefix match against the runner's display name." },
					{ "name": "city", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive city filter." },
					{ "name": "state", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive state filter." },
					{ "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "description": "Capped at 50 regardless of requested value." }
				],
				"responses": {
					"200": {
						"description": "Matching public runners.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"count": { "type": "integer" },
										"runners": { "type": "array", "items": { "$ref": "#/components/schemas/Runner" } }
									},
									"required": ["count", "runners"]
								}
							}
						}
					},
					"400": {
						"description": "Missing required `q` query parameter.",
						"content": { "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string", "const": "missing_query" } }, "required": ["error"] } } }
					},
					"503": {
						"description": "The runner search backend failed — not the same as zero matching runners.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpstreamUnavailable" } } }
					}
				}
			}
		},
		"/api/v1/clubs": {
			"get": {
				"summary": "List public running clubs",
				"operationId": "listClubs",
				"parameters": [
					{ "name": "name", "in": "query", "schema": { "type": "string" }, "description": "Case-insensitive prefix match against the club's name." },
					{ "name": "city", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive city filter." },
					{ "name": "state", "in": "query", "schema": { "type": "string" }, "description": "Exact-match, case-sensitive state filter." },
					{ "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }, "description": "Capped at 50 regardless of requested value." },
					{ "name": "cursor", "in": "query", "schema": { "type": "string" }, "description": "Opaque pagination cursor from a previous response's `nextCursor`." }
				],
				"responses": {
					"200": {
						"description": "Public running clubs.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"count": { "type": "integer" },
										"clubs": { "type": "array", "items": { "$ref": "#/components/schemas/Club" } },
										"nextCursor": { "type": "string", "nullable": true, "description": "Opaque cursor for the next page, or null when there are no more results." }
									},
									"required": ["count", "clubs", "nextCursor"]
								}
							}
						}
					},
					"503": {
						"description": "The club listing backend failed — not the same as zero clubs.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpstreamUnavailable" } } }
					}
				}
			}
		},
		"/api/v1/me": {
			"get": {
				"summary": "Get the authenticated user's own profile",
				"operationId": "getMe",
				"description": "Requires a Firebase ID token for an onRun user, obtained through that user's own sign-in flow — see /auth.md. Returns only the caller's own data, scoped server-side to the token's uid.",
				"security": [{ "bearerAuth": [] }],
				"responses": {
					"200": {
						"description": "The caller's own profile.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
					},
					"401": {
						"description": "Missing or invalid bearer token."
					},
					"404": {
						"description": "The caller's user document does not exist.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFound" } } }
					}
				}
			}
		},
		"/api/v1/checkout_sessions": {
			"post": {
				"summary": "Create an ACP checkout session for a paid event",
				"operationId": "createCheckoutSession",
				"description": "Agentic Commerce Protocol (2026-04-17) agentic-checkout create. Holds a spot for the authenticated user through onRun's production capacity/discount transaction and opens a real Stripe PaymentIntent on the organizer's Connect account; the PaymentIntent id and client secret come back in `metadata` so the buyer can confirm it. Auth is the end user's Firebase ID token, not an agent-platform credential — onRun is not enrolled for Stripe Shared Payment Tokens. See /auth.md.",
				"security": [{ "bearerAuth": [] }],
				"x-payment-info": {
					"intent": "session",
					"method": "stripe",
					"amount": "dynamic",
					"currency": "USD",
					"description": "Event registration checkout session via Stripe Connect"
				},
				"parameters": [{ "$ref": "#/components/parameters/AcpIdempotencyKey" }],
				"requestBody": {
					"required": true,
					"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpCheckoutSessionCreateRequest" } } }
				},
				"responses": {
					"201": {
						"description": "Session created; a spot is held until `expires_at`.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpCheckoutSession" } } }
					},
					"400": { "description": "Malformed request, or a missing `Idempotency-Key` header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"401": { "description": "Missing or invalid Firebase ID token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"404": { "description": "The event does not exist or is private.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"409": { "description": "The event is sold out, or the caller already holds a reservation.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"422": { "description": "The event cannot be checked out (no organizer, currency mismatch, or an unusable discount code).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"429": { "description": "Rate limited.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"503": { "description": "Checkout backend temporarily unavailable.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } }
				}
			}
		},
		"/api/v1/checkout_sessions/{id}": {
			"get": {
				"summary": "Retrieve an ACP checkout session",
				"operationId": "getCheckoutSession",
				"description": "Returns the caller's own session. A session belonging to another user is reported as 404, so this cannot be used to probe for other reservation ids. The Stripe client secret is returned only on create, never here.",
				"security": [{ "bearerAuth": [] }],
				"parameters": [{ "$ref": "#/components/parameters/AcpSessionId" }],
				"responses": {
					"200": {
						"description": "The session.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpCheckoutSession" } } }
					},
					"401": { "description": "Missing or invalid Firebase ID token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"404": { "description": "No such session for this caller.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } }
				}
			},
			"post": {
				"summary": "Update a checkout session (not supported)",
				"operationId": "updateCheckoutSession",
				"description": "The ACP update endpoint is deliberately not implemented and always returns 405. An onRun session is a single event at quantity 1 with no shipping address and an immutable post-reservation price, so there is nothing an update could legitimately change — cancel and create a new session instead.",
				"security": [{ "bearerAuth": [] }],
				"parameters": [{ "$ref": "#/components/parameters/AcpSessionId" }, { "$ref": "#/components/parameters/AcpIdempotencyKey" }],
				"responses": {
					"405": { "description": "Not supported by this merchant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } }
				}
			}
		},
		"/api/v1/checkout_sessions/{id}/complete": {
			"post": {
				"summary": "Complete an ACP checkout session",
				"operationId": "completeCheckoutSession",
				"description": "Finalizes the registration. The credential must be the Stripe PaymentIntent id from the session's `metadata`, sent with `handler_id: \"onrun_stripe_delegated\"` and `credential.type: \"onrun_stripe_payment_intent\"`; onRun re-verifies it with Stripe and checks it belongs to this session before writing. If the charge has not succeeded the session comes back with status `requires_escalation` and a `continue_url` — a success is never reported that did not happen. Stripe Shared Payment Tokens are rejected with 422.",
				"security": [{ "bearerAuth": [] }],
				"x-payment-info": {
					"intent": "charge",
					"method": "stripe",
					"amount": "dynamic",
					"currency": "USD",
					"description": "Finalize event registration payment via Stripe Connect"
				},
				"parameters": [{ "$ref": "#/components/parameters/AcpSessionId" }, { "$ref": "#/components/parameters/AcpIdempotencyKey" }],
				"requestBody": {
					"required": true,
					"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpCheckoutSessionCompleteRequest" } } }
				},
				"responses": {
					"200": {
						"description": "Session with status `completed` (an `order` is attached) or `requires_escalation`.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpCheckoutSession" } } }
					},
					"400": { "description": "Malformed request, or a missing `Idempotency-Key` header.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"401": { "description": "Missing or invalid Firebase ID token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"404": { "description": "No such session for this caller.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"422": { "description": "Unsupported payment handler or credential type (including `spt`), or a credential that does not match this session.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } }
				}
			}
		},
		"/api/v1/checkout_sessions/{id}/cancel": {
			"post": {
				"summary": "Cancel an ACP checkout session",
				"operationId": "cancelCheckoutSession",
				"description": "Releases the held spot and cancels the Stripe PaymentIntent. A completed session cannot be canceled — a paid registration is refunded through onRun's refund flow instead.",
				"security": [{ "bearerAuth": [] }],
				"parameters": [{ "$ref": "#/components/parameters/AcpSessionId" }, { "$ref": "#/components/parameters/AcpIdempotencyKey" }],
				"responses": {
					"200": {
						"description": "Session with status `canceled`.",
						"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpCheckoutSession" } } }
					},
					"401": { "description": "Missing or invalid Firebase ID token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"404": { "description": "No such session for this caller.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } },
					"405": { "description": "The session has completed and can no longer be canceled.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcpError" } } } }
				}
			}
		}
	},
	"components": {
		"parameters": {
			"AcpIdempotencyKey": {
				"name": "Idempotency-Key",
				"in": "header",
				"required": true,
				"description": "Required on every ACP POST. onRun derives the reservation's deterministic id from this key, so retrying a create replays the original reservation instead of holding a second spot.",
				"schema": { "type": "string", "maxLength": 255 }
			},
			"AcpSessionId": {
				"name": "id",
				"in": "path",
				"required": true,
				"description": "The `checkout_session_id` returned by POST /api/v1/checkout_sessions.",
				"schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,256}$" }
			}
		},
		"securitySchemes": {
			"bearerAuth": {
				"type": "http",
				"scheme": "bearer",
				"bearerFormat": "Firebase ID token",
				"description": "See /auth.md and /.well-known/oauth-protected-resource (RFC 9728) — the real authorization server is Firebase Auth (https://securetoken.google.com/sportify-api), not onrun.com itself."
			}
		},
		"schemas": {
			"Event": {
				"type": "object",
				"properties": {
					"id": { "type": "string" },
					"name": { "type": "string" },
					"description": { "type": "string", "nullable": true },
					"city": { "type": "string", "nullable": true },
					"state": { "type": "string", "nullable": true },
					"type": { "type": "string", "nullable": true, "description": "Organizer-entered free text, lowercased (e.g. \"social run\", \"tempo run\") — not a fixed enum." },
					"date": { "type": "string", "nullable": true, "format": "date-time", "description": "ISO 8601 timestamp. Null only for events created before this field existed." },
					"endDate": { "type": "string", "nullable": true, "description": "Always null today — onRun does not yet model multi-day events." },
					"isFree": { "type": "boolean", "nullable": true },
					"priceCents": { "type": "integer", "nullable": true, "description": "Null for the large majority of events — organizers rarely enter a structured price today." },
					"currency": { "type": "string", "nullable": true },
					"imageUrl": { "type": "string", "nullable": true },
					"organizer": { "type": "string", "nullable": true },
					"distanceMinMiles": { "type": "number", "nullable": true, "description": "Lower bound of the event's distance in miles, normalized from the organizer's free-text distance field. Null when nothing parseable was entered. List endpoint only." },
					"distanceMaxMiles": { "type": "number", "nullable": true, "description": "Upper bound of the event's distance in miles. Equals distanceMinMiles for a single distance. List endpoint only." },
					"pace": { "type": "string", "nullable": true, "description": "Organizer-entered free text (e.g. \"8:00-9:00 min/mi\", \"conversational\"). List endpoint only." },
					"frequency": { "type": "string", "nullable": true, "description": "Organizer-entered free text for recurring events (e.g. \"weekly\", \"every Saturday\"). List endpoint only." },
					"organizerLink": { "type": "string", "nullable": true, "description": "Organizer-provided external URL. Detail endpoint only." },
					"isPrivate": { "type": "boolean", "nullable": true, "description": "Whether the event is private. Private events are excluded from the list endpoint and return 404 from the detail endpoint; this field is only ever present (and false) on a served detail response. Detail endpoint only." },
					"url": { "type": "string", "format": "uri" }
				},
				"required": ["id", "name", "url"]
			},
			"RaceHighlight": {
				"type": "object",
				"properties": {
					"eventName": { "type": "string" },
					"dateLabel": { "type": "string", "nullable": true },
					"result": { "type": "string", "nullable": true },
					"location": { "type": "string", "nullable": true },
					"rankLabel": { "type": "string", "nullable": true }
				},
				"required": ["eventName"]
			},
			"Runner": {
				"type": "object",
				"properties": {
					"id": { "type": "string" },
					"displayName": { "type": "string" },
					"handle": { "type": "string", "nullable": true },
					"city": { "type": "string", "nullable": true },
					"state": { "type": "string", "nullable": true },
					"photoURL": { "type": "string", "nullable": true },
					"url": { "type": "string", "format": "uri" }
				},
				"required": ["id", "displayName", "url"]
			},
			"Club": {
				"type": "object",
				"properties": {
					"id": { "type": "string" },
					"name": { "type": "string" },
					"city": { "type": "string", "nullable": true },
					"state": { "type": "string", "nullable": true }
				},
				"required": ["id", "name"]
			},
			"Me": {
				"type": "object",
				"properties": {
					"id": { "type": "string" },
					"displayName": { "type": "string", "nullable": true },
					"handle": { "type": "string", "nullable": true },
					"email": { "type": "string", "nullable": true },
					"isPrivate": { "type": "boolean" },
					"photoURL": { "type": "string", "nullable": true },
					"url": { "type": "string", "format": "uri" }
				},
				"required": ["id", "isPrivate", "url"]
			},
			"NotFound": {
				"type": "object",
				"properties": {
					"error": { "type": "string", "const": "not_found" },
					"message": { "type": "string" }
				},
				"required": ["error", "message"]
			},
			"UpstreamUnavailable": {
				"type": "object",
				"description": "The Firestore read backing this endpoint failed (for example a missing composite index or a transient outage). Distinct from a normal empty result — retry later rather than treating this as \"nothing matched\".",
				"properties": {
					"error": { "type": "string", "const": "upstream_unavailable" },
					"message": { "type": "string" }
				},
				"required": ["error", "message"]
			},
			"AcpTotal": {
				"type": "object",
				"description": "One line of the ACP total breakdown. Amounts are in the currency's minor unit (cents); `items_discount` is negative.",
				"properties": {
					"type": { "type": "string", "enum": ["items_base_amount", "items_discount", "subtotal", "fulfillment", "tax", "total"] },
					"display_text": { "type": "string" },
					"amount": { "type": "integer" }
				},
				"required": ["type", "display_text", "amount"]
			},
			"AcpMessage": {
				"type": "object",
				"properties": {
					"type": { "type": "string", "enum": ["info", "error"] },
					"code": { "type": "string" },
					"severity": { "type": "string" },
					"resolution": { "type": "string" },
					"content_type": { "type": "string", "const": "plain" },
					"content": { "type": "string" }
				},
				"required": ["type", "content_type", "content"]
			},
			"AcpCheckoutSession": {
				"type": "object",
				"description": "ACP CheckoutSession. onRun sessions always carry exactly one line item at quantity 1 (one event entry) and a single digital fulfillment option.",
				"properties": {
					"id": { "type": "string", "description": "checkout_session_id — also the onRun reservation id." },
					"protocol": {
						"type": "object",
						"properties": { "name": { "type": "string", "const": "acp" }, "version": { "type": "string" } },
						"required": ["name", "version"]
					},
					"status": { "type": "string", "enum": ["ready_for_payment", "requires_escalation", "complete_in_progress", "completed", "canceled", "expired"] },
					"currency": { "type": "string" },
					"line_items": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"id": { "type": "string" },
								"item": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "unit_amount": { "type": "integer" } }, "required": ["id"] },
								"quantity": { "type": "integer" },
								"totals": { "type": "array", "items": { "$ref": "#/components/schemas/AcpTotal" } }
							},
							"required": ["id", "item", "quantity", "totals"]
						}
					},
					"totals": { "type": "array", "items": { "$ref": "#/components/schemas/AcpTotal" } },
					"fulfillment_options": { "type": "array", "items": { "type": "object" } },
					"selected_fulfillment_options": { "type": "array", "items": { "type": "object" } },
					"messages": { "type": "array", "items": { "$ref": "#/components/schemas/AcpMessage" } },
					"links": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "title": { "type": "string" }, "url": { "type": "string", "format": "uri" } }, "required": ["type", "url"] } },
					"capabilities": {
						"type": "object",
						"description": "Advertises onRun's own payment handler. `requires_delegate_payment` is false — this deployment does not accept Shared Payment Tokens.",
						"properties": { "payment": { "type": "object", "properties": { "handlers": { "type": "array", "items": { "type": "object" } } }, "required": ["handlers"] } },
						"required": ["payment"]
					},
					"continue_url": { "type": "string", "format": "uri", "description": "The onRun event page, where the buyer can confirm payment in a browser." },
					"created_at": { "type": "string", "format": "date-time" },
					"updated_at": { "type": "string", "format": "date-time" },
					"expires_at": { "type": "string", "format": "date-time", "description": "When the held spot is released." },
					"metadata": {
						"type": "object",
						"additionalProperties": { "type": "string" },
						"description": "`event_id`, `event_url`, `stripe_payment_intent_id`, and — only on the create response — `stripe_payment_intent_client_secret`."
					},
					"order": { "type": "object", "description": "Present only when `status` is `completed`." }
				},
				"required": ["id", "protocol", "status", "currency", "line_items", "totals", "messages", "links", "capabilities", "continue_url", "metadata"]
			},
			"AcpCheckoutSessionCreateRequest": {
				"type": "object",
				"properties": {
					"line_items": {
						"type": "array",
						"minItems": 1,
						"maxItems": 1,
						"description": "Exactly one item. `id` is the onRun event id.",
						"items": { "type": "object", "properties": { "id": { "type": "string" }, "quantity": { "type": "integer", "const": 1 } }, "required": ["id"] }
					},
					"currency": { "type": "string", "description": "Must match the event's currency." },
					"capabilities": { "type": "object", "description": "Required by the ACP create schema; onRun does not require any specific capability to be declared." },
					"discounts": {
						"type": "object",
						"properties": { "codes": { "type": "array", "maxItems": 1, "items": { "type": "string" } } },
						"description": "At most one code, evaluated by onRun's real perk/discount engine."
					}
				},
				"required": ["line_items", "currency", "capabilities"]
			},
			"AcpCheckoutSessionCompleteRequest": {
				"type": "object",
				"properties": {
					"payment_data": {
						"type": "object",
						"properties": {
							"handler_id": { "type": "string", "const": "onrun_stripe_delegated" },
							"instrument": {
								"type": "object",
								"properties": {
									"type": { "type": "string" },
									"credential": {
										"type": "object",
										"properties": {
											"type": { "type": "string", "const": "onrun_stripe_payment_intent", "description": "`spt` (Stripe Shared Payment Token) is rejected with 422 — onRun is not enrolled to receive one." },
											"token": { "type": "string", "description": "The Stripe PaymentIntent id from this session's `metadata.stripe_payment_intent_id`, after the buyer has confirmed it." }
										},
										"required": ["type", "token"]
									}
								},
								"required": ["credential"]
							}
						},
						"required": ["instrument"]
					}
				},
				"required": ["payment_data"]
			},
			"AcpError": {
				"type": "object",
				"description": "ACP error body.",
				"properties": {
					"type": { "type": "string", "enum": ["invalid_request", "processing_error", "service_unavailable"] },
					"code": { "type": "string" },
					"message": { "type": "string" },
					"param": { "type": "string", "description": "JSONPath to the offending field, when applicable." }
				},
				"required": ["type", "code", "message"]
			}
		}
	}
}
