{"id":"87330d2e-cd16-454c-8569-12646bb0b832","entityType":"agent","slug":"clawhub-zmtucker-sportsinc-sportslink","name":"sportsinc-sportslink","canonicalUrl":"https://www.xpersona.co/agent/clawhub-zmtucker-sportsinc-sportslink","canonicalPath":"/agent/clawhub-zmtucker-sportsinc-sportslink","generatedAt":"2026-10-11T08:46:38.681Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:25:36.608Z","emptyReason":null},"description":"Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\") from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports Inc is a buying group that does NOT send individual vendor invoices; its SportsLink REST API is where the invoices live. Use when you need to retrieve Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull SportsLink documents\", \"fetch this month's SI invoices to match against POs\". This is the SOURCE adapter only: it authenticates, pages, normalises each SI document into a common invoice shape (po_number, invoice_number/date, lines[], charges, total, is_credit), and marks documents historical once imported. Some documents are scanned rather than EDI, so the API returns their header totals with NO line items (`has_lines: false`) — every retrieval names those in `needs_line_recovery`, and they are not billable as returned. For each, this skill logs in to the SportsWeb portal, downloads the invoice PDF, OCRs the scanned vendor invoice into text for the agent to read, and then checks the extracted lines against the API's own merchandise total before any of it is billable. It is customer-agnostic (every Sports Inc dealer uses this same API) and touches no ERP — pair it with a payables workflow (e.g. `drivethru-payable-matching`) to match against POs and create the bill.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:sportsinc-sportslink","sourceUrl":"https://clawhub.ai/zmtucker/sportsinc-sportslink","homepage":"https://clawhub.ai/zmtucker/skills/sportsinc-sportslink","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/zmtucker/sportsinc-sportslink","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/zmtucker/skills/sportsinc-sportslink","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"sportsinc-sportslink technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:25:36.608Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:25:36.608Z","emptyReason":null},"stars":null,"forks":null,"downloads":1134,"packageName":null,"latestVersion":"0.7.1","tractionLabel":"1.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:25:36.469Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T06:25:36.608Z","lastCrawledAt":"2026-10-11T06:25:36.469Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T06:25:36.469Z","lastVerifiedAt":null,"highlights":[{"version":"0.7.1","createdAt":"2026-08-07T13:59:35.182Z","changelog":"Version 0.7.1 introduces scanned invoice PDF retrieval, OCR extraction, and new dependencies for full header-only document recovery: - Added support for header-only (scanned) invoices: identifies these in every retrieval via a new needs_line_recovery field. - New commands to fetch scanned invoice PDFs from the SportsWeb portal and reconcile their lines after manual/OCR review. - Introduced OCR extraction for scanned PDFs, extracting readable text via rapidocr/tesseract or providing images if OCR is off. - Significant increase in environment variables for portal credentials, OCR, and PDF handling. - Added several dependencies: pypdf, pillow, playwright, rapidocr-onnxruntime. - Updated documentation to detail scanned-document recovery workflow, configuration, and command usage.","fileCount":12,"zipByteSize":96114},{"version":"0.4.0","createdAt":"2026-08-03T14:54:38.964Z","changelog":"Version 0.4.0 - Removed the unused file skill-card.md for a leaner project structure. - SKILL.md updated to bump the version to 0.4.0; no changes to documented functionality or usage.","fileCount":5,"zipByteSize":15193},{"version":"0.3.2","createdAt":"2026-07-25T14:47:31.103Z","changelog":"sportsinc-sportslink 0.3.2 - Documentation updated in SKILL.md to clarify how credential brokering works in Agent-to-Agent (A2A) mode, including added details about runtime environment setup and credential privacy. - Removed redundant file: skill-card.md. - No changes to interface, usage, or runtime behavior.","fileCount":5,"zipByteSize":13936},{"version":"0.3.1","createdAt":"2026-07-23T18:48:39.213Z","changelog":"- Changed environment gating for SPORTSINC_API_KEY: it is no longer listed in openclaw.requires.env, to ensure the skill remains visible in Agent-to-Agent (A2A) mode where credentials are delegated at runtime. - Environment variable documentation for SPORTSINC_API_KEY remains in primaryEnv/envVars, and runtime scripts will handle missing credentials with explicit errors. - Removed the file skill-card.md.","fileCount":5,"zipByteSize":13622},{"version":"0.3.0","createdAt":"2026-07-23T15:33:26.462Z","changelog":"- Updated to version 0.3.0 with improved documentation for agent-to-agent (A2A) credential handling. - Clarified how delegated credentials are sourced and how the `SPORTSINC_API_KEY` is accessed in A2A mode. - Expanded and updated the Agent-to-Agent (A2A) section to reflect current credential workflow. - Removed the obsolete skill-card.md file.","fileCount":5,"zipByteSize":13391},{"version":"0.2.0","createdAt":"2026-07-23T14:17:12.689Z","changelog":"Version 0.2.0 introduces agent-to-agent (A2A) support and updates CLI interface - Added get-for-a2a CLI action for agent-to-agent (A2A) usage, providing a structured contract for inter-agent requests and responses. - Documented how the Sports Inc agent should honor credential scope signalled by the Knoxville platform when serving delegated calls. - Updated SKILL.md with contract details for A2A calls, including parameters and response design. - CLI usage expanded—see new usage example for get-for-a2a with parameters like customer_ref and date_range. - Removed obsolete skill-card.md file.","fileCount":5,"zipByteSize":13475},{"version":"0.1.0","createdAt":"2026-07-21T21:26:32.734Z","changelog":"Initial release of the Sports Inc SportsLink API adapter. - Provides a source adapter to retrieve and normalize Sports Inc invoices from the SportsWeb Invoice Center. - Supports listing active EDI invoices, fetching specific documents, and marking documents as historical (imported). - Ensures normalized invoice data for seamless integration with payables workflows. - Requires only the SportsLink API Key for authentication; supports dry-run marking for safe operation. - Does not interact with any ERP system—intended to be paired with a separate payables workflow skill.","fileCount":5,"zipByteSize":11197}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:sportsinc-sportslink","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq291581evd78xzn1930b0n87bfny:sportsinc-sportslink` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/zmtucker/sportsinc-sportslink before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T08:46:38.677Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-sportsinc-sportslink/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T06:25:36.608Z","emptyReason":null},"readme":"Skill: sportsinc-sportslink\n\nOwner: zmtucker\n\nSummary: Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\") from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports Inc is a buying group that does NOT send individual vendor invoices; its SportsLink REST API is where the invoices live. Use when you need to retrieve Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull SportsLink documents\", \"fetch this month's SI invoices to match against POs\". This is the SOURCE adapter only: it authenticates, pages, normalises each SI document into a common invoice shape (po_number, invoice_number/date, lines[], charges, total, is_credit), and marks documents historical once imported. Some documents are scanned rather than EDI, so the API returns their header totals with NO line items (`has_lines: false`) — every retrieval names those in `needs_line_recovery`, and they are not billable as returned. For each, this skill logs in to the SportsWeb portal, downloads the invoice PDF, OCRs the scanned vendor invoice into text for the agent to read, and then checks the extracted lines against the API's own merchandise total before any of it is billable. It is customer-agnostic (every Sports Inc dealer uses this same API) and touches no ERP — pair it with a payables workflow (e.g. `drivethru-payable-matching`) to match against POs and create the bill.\n\nTags: latest:0.7.1\n\nVersion history:\n\nv0.7.1 | 2026-08-07T13:59:35.182Z | auto\n\nVersion 0.7.1 introduces scanned invoice PDF retrieval, OCR extraction, and new dependencies for full header-only document recovery:\n\n- Added support for header-only (scanned) invoices: identifies these in every retrieval via a new needs_line_recovery field.\n- New commands to fetch scanned invoice PDFs from the SportsWeb portal and reconcile their lines after manual/OCR review.\n- Introduced OCR extraction for scanned PDFs, extracting readable text via rapidocr/tesseract or providing images if OCR is off.\n- Significant increase in environment variables for portal credentials, OCR, and PDF handling.\n- Added several dependencies: pypdf, pillow, playwright, rapidocr-onnxruntime.\n- Updated documentation to detail scanned-document recovery workflow, configuration, and command usage.\n\nv0.4.0 | 2026-08-03T14:54:38.964Z | auto\n\nVersion 0.4.0\n\n- Removed the unused file skill-card.md for a leaner project structure.\n- SKILL.md updated to bump the version to 0.4.0; no changes to documented functionality or usage.\n\nv0.3.2 | 2026-07-25T14:47:31.103Z | auto\n\nsportsinc-sportslink 0.3.2\n\n- Documentation updated in SKILL.md to clarify how credential brokering works in Agent-to-Agent (A2A) mode, including added details about runtime environment setup and credential privacy.\n- Removed redundant file: skill-card.md.\n- No changes to interface, usage, or runtime behavior.\n\nv0.3.1 | 2026-07-23T18:48:39.213Z | auto\n\n- Changed environment gating for SPORTSINC_API_KEY: it is no longer listed in openclaw.requires.env, to ensure the skill remains visible in Agent-to-Agent (A2A) mode where credentials are delegated at runtime.\n- Environment variable documentation for SPORTSINC_API_KEY remains in primaryEnv/envVars, and runtime scripts will handle missing credentials with explicit errors.\n- Removed the file skill-card.md.\n\nv0.3.0 | 2026-07-23T15:33:26.462Z | auto\n\n- Updated to version 0.3.0 with improved documentation for agent-to-agent (A2A) credential handling.\n- Clarified how delegated credentials are sourced and how the `SPORTSINC_API_KEY` is accessed in A2A mode.\n- Expanded and updated the Agent-to-Agent (A2A) section to reflect current credential workflow.\n- Removed the obsolete skill-card.md file.\n\nv0.2.0 | 2026-07-23T14:17:12.689Z | auto\n\nVersion 0.2.0 introduces agent-to-agent (A2A) support and updates CLI interface\n\n- Added get-for-a2a CLI action for agent-to-agent (A2A) usage, providing a structured contract for inter-agent requests and responses.\n- Documented how the Sports Inc agent should honor credential scope signalled by the Knoxville platform when serving delegated calls.\n- Updated SKILL.md with contract details for A2A calls, including parameters and response design.\n- CLI usage expanded—see new usage example for get-for-a2a with parameters like customer_ref and date_range.\n- Removed obsolete skill-card.md file.\n\nv0.1.0 | 2026-07-21T21:26:32.734Z | auto\n\nInitial release of the Sports Inc SportsLink API adapter.\n\n- Provides a source adapter to retrieve and normalize Sports Inc invoices from the SportsWeb Invoice Center.\n- Supports listing active EDI invoices, fetching specific documents, and marking documents as historical (imported).\n- Ensures normalized invoice data for seamless integration with payables workflows.\n- Requires only the SportsLink API Key for authentication; supports dry-run marking for safe operation.\n- Does not interact with any ERP system—intended to be paired with a separate payables workflow skill.\n\nArchive index:\n\nArchive v0.7.1: 12 files, 96114 bytes\n\nFiles: references/pdf_extraction.md (12615b), references/sportslink_api.md (6258b), references/sportsweb_flow_notes.md (21421b), scripts/_selftest.py (44792b), scripts/invoice_lines.py (17622b), scripts/invoice_pdf.py (20179b), scripts/ocr.py (12657b), scripts/sportslink.py (36974b), scripts/sportsweb_browser.py (59954b), skill-card.md (2873b), SKILL.md (33037b), _meta.json (139b)\n\nFile v0.7.1:SKILL.md\n\n---\nname: sportsinc-sportslink\ndescription: >\n  Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\")\n  from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports\n  Inc is a buying group that does NOT send individual vendor invoices; its\n  SportsLink REST API is where the invoices live. Use when you need to retrieve\n  Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull\n  SportsLink documents\", \"fetch this month's SI invoices to match against POs\".\n  This is the SOURCE adapter only: it authenticates, pages, normalises each SI\n  document into a common invoice shape (po_number, invoice_number/date,\n  lines[], charges, total, is_credit), and marks documents historical once\n  imported. Some documents are scanned rather than EDI, so the API returns their\n  header totals with NO line items (`has_lines: false`) — every retrieval names\n  those in `needs_line_recovery`, and they are not billable as returned. For\n  each, this skill logs in to the SportsWeb portal, downloads the invoice PDF,\n  OCRs the scanned vendor invoice into text for the agent to read, and then\n  checks the extracted lines against the API's own merchandise total before any\n  of it is billable. It is customer-agnostic (every Sports Inc\n  dealer uses this same API) and touches no ERP — pair it with a payables\n  workflow (e.g. `drivethru-payable-matching`) to match against POs and create\n  the bill.\nversion: 0.7.1\nemoji: 🏟️\nhomepage: https://www.sportsinc.com\nmetadata:\n  openclaw:\n    requires:\n      # SPORTSINC_API_KEY is deliberately NOT listed here. openclaw gates a\n      # skill OUT of the model's view when a `requires.env` key is absent from\n      # the *boot* environment — but in A2A mode (see \"Agent-to-Agent (A2A)\n      # Mode\" below) this key is brokered per-turn by the platform and is\n      # absent at boot by design, so gating on it would hide this skill from\n      # the very delegated flow it exists to serve. The key stays fully\n      # documented via `primaryEnv`/`envVars`, and the helper self-guards at\n      # runtime (`config_error`/`auth_error`) when it is genuinely missing.\n      # `python3` stays gated because it must be present at boot.\n      bins: [python3]\n    primaryEnv: SPORTSINC_API_KEY\n    envVars:\n      SPORTSINC_API_KEY:\n        required: true\n        description: >\n          SportsLink API key, sent as the `X-API-KEY` header. Request one from\n          mhoerner@hq.sportsinc.com. Treat as a secret; never paste into chat.\n      SPORTSINC_API_URL:\n        required: false\n        description: Base URL, default `https://api.sportsinc.com/`.\n      SPORTSINC_DRY_RUN:\n        required: false\n        description: If truthy, `mark-historical` is simulated (no state change).\n      SPORTSINC_WEB_USERNAME:\n        required: false\n        description: >\n          SportsWeb portal login, used only by `fetch-invoice-doc` to pull a\n          scanned document's PDF. This is the dealer's own portal user and is\n          SEPARATE from SPORTSINC_API_KEY. Treat as a secret; never ask for it\n          in chat.\n      SPORTSINC_WEB_PASSWORD:\n        required: false\n        description: SportsWeb portal password. Treat as a secret.\n      SPORTSINC_WEB_BASE_URL:\n        required: false\n        description: >\n          Invoice Center host, default `https://swv2h.sportsinc.com/`.\n      SPORTSINC_WEB_HOME_URL:\n        required: false\n        description: >\n          Home screen, default `https://swv3.sportsinc.com/home` — a DIFFERENT\n          host from the Invoice Center. This is where login lands and the only\n          page carrying the search box.\n      SPORTSINC_WEB_TIMEOUT_MS:\n        required: false\n        description: >\n          Per-step wait, default 25000. Every wait is for a concrete element;\n          nothing waits on `networkidle`, which the Vue home screen may never\n          reach.\n      SPORTSINC_WEB_HEADLESS:\n        required: false\n        description: >\n          Default `true`. Set falsy to run the portal browser headed — on a\n          Linux host with no display an Xvfb virtual display is started\n          automatically (needs the `xvfb` system package).\n      SPORTSINC_WEB_STATE_PATH:\n        required: false\n        description: >\n          File path for a persisted logged-in session (Playwright\n          `storage_state`). Set it to skip the Auth0 round trip on later runs,\n          and as the escape hatch if the login ever requires MFA — log in once\n          by hand with this set and subsequent runs reuse the session.\n      SPORTSINC_OCR:\n        required: false\n        description: >\n          `auto` (default — use the best installed engine), `off`, or an engine\n          name (`rapidocr`, `tesseract`). Controls whether scanned pages are\n          OCR'd into text before the images are offered.\n      SPORTSINC_PDF_DIR:\n        required: false\n        description: >\n          Where a scanned PDF is written so the agent can read it (defaults to\n          the system temp dir). Only image-only PDFs are ever written; a\n          document with a text layer never touches disk.\n    install:\n      uv:\n        - requests>=2.28\n        # Text-layer extraction for the header-only PDF fallback.\n        - pypdf>=4.0\n        # Converts the scanned vendor-invoice pages to PNGs the agent can read.\n        # Needs a build with JPEG 2000 support (SI's scans are JPX); without it\n        # the raw stream is written instead and the tool says so.\n        - pillow>=10.0\n        # Browser automation for pulling those PDFs from the SportsWeb portal.\n        # As with drivethru-adidas-click, this installs the Playwright *package*;\n        # the Chromium binary is a separate download handled on first use.\n        - playwright>=1.40\n        # OCR for the scanned vendor-invoice pages, so text rather than a 300dpi\n        # image goes into context. Pip-only and cross-platform — no system\n        # package, unlike tesseract (which is used instead when present).\n        # Without it the pipeline still works, falling back to the images.\n        - rapidocr-onnxruntime>=1.3\n---\n\n# Sports Inc SportsLink adapter\n\nSports Inc is a buying group: BaconCo (and every other SI dealer) buys through\nSports Inc, and Sports Inc does **not** email individual vendor invoices —\nthey're published in the SportsWeb Invoice Center and exposed through the\n**SportsLink REST API**. This skill is the source adapter for that API. It does\none job: hand a payables workflow a clean, normalised list of invoices, and\nmark them consumed once they've been imported. It never touches Odoo.\n\nThe single helper is `scripts/sportslink.py`:\n\n```bash\n# The un-imported inbox: every active document, normalised. Scanned documents\n# come back with no lines and are named in `needs_line_recovery` — see the\n# retrieval procedure below. Do NOT pass `ediOnly: true` for a billing run: it\n# filters those documents out of the result entirely, so they are never billed\n# and simply age.\npython3 scripts/sportslink.py list '{\"active\": true, \"lines\": true}'\n\n# A specific document (ignores the active-only filter)\npython3 scripts/sportslink.py get '{\"poNumber\": \"P13189\"}'\n\n# Mark documents consumed — AFTER they've been billed (honors SPORTSINC_DRY_RUN)\npython3 scripts/sportslink.py mark-historical '{\"siDocNumbers\": [12345, 23456]}'\n\n# A2A-safe action for agent-to-agent calls (structured request/response contract)\npython3 scripts/sportslink.py get-for-a2a '{\"customer_ref\": \"DEALER-001\", \"date_range\": {\"start\": \"2024-01-01\", \"end\": \"2024-12-31\"}, \"statuses\": [\"open\"]}'\n\n# Header-only (scanned) document: get its PDF, read it yourself, then get checked\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 23962348}'\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 23962348, \"lines\": [...]}'\n```\n\nEvery command prints one JSON object, or `{\"error\": {...}}` with a non-zero\nexit. Needs `SPORTSINC_API_KEY` (if unset, exits `config_error` — stop and tell\nthe user to configure it; never ask for the key in chat).\n\n## ⚠ Retrieval procedure — required, every time\n\n**A Sports Inc retrieval is not finished when `list` returns.** Some documents\ncome back with header totals and no line items, because Sports Inc scanned the\nsupplier's invoice instead of receiving it as EDI. Those are **not billable as\nreturned** — there is nothing to match against a PO.\n\nEvery `list` / `get` / `get-for-a2a` result names them:\n\n```json\n\"needs_line_recovery\": [24682750, 24684277],\n\"credits\": [24690002],\n\"next_step\": \"2 document(s) have no line detail from the API …\"\n```\n\n**If `needs_line_recovery` is non-empty, you must resolve every document in it\nbefore billing any of them.** For each one:\n\n```bash\n# 1. Fetch it. Logs in to the portal, downloads the PDF, and OCRs the scanned\n#    vendor invoice into text. Active tab first, Archived automatically after.\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 24684277}'\n\n# 2. Read the returned `text` and extract the line items yourself.\n#    See references/pdf_extraction.md — which pages to skip, the field mapping,\n#    and the rules (transcribe don't compute, never infer a quantity).\n\n# 3. Hand them back to be checked against the API's own merchandise total.\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 24684277, \"lines\": [...]}'\n```\n\nStep 3 returns `status: \"verified\"` — a normalised invoice with real\n`lines[]`, indistinguishable downstream from an EDI one — or\n`status: \"needs_review\"` with the variance named.\n\nNon-negotiables:\n\n- **Bill only a `verified` invoice.** `needs_review` means the extracted lines\n  do not tie to Sports Inc's own header; escalate and leave the document active.\n- **Never bill a document from header totals alone.** A `docTotal` with no lines\n  behind it cannot be matched to a PO, and that is the whole point of matching.\n- **Never skip a document because it is inconvenient.** It reappears on the next\n  run, unbilled, and ages.\n- `credits` (`is_credit: true`) are never billed at all — they go to a human,\n  whatever their line detail looks like.\n- Exactly-once is unchanged: `mark-historical` only **after** the bill exists.\n\nIf `fetch-invoice-doc` reports `portal_tab: \"archived\"`, the document has\nalready been marked historical — which happens only after a bill was created.\nCheck whether it has already been paid before creating another.\n\n## Normalised invoice shape\n\n`list`/`get` return `{count, total_count, invoices: [...]}`, each invoice:\n\n```json\n{\n  \"source\": \"sports_inc\",\n  \"po_number\": \"P13189\",          // dealer PO number → the match key to a PO\n  \"si_doc_number\": 12345,          // SI's document id → used to mark-historical\n  \"invoice_number\": \"…\",           // supplierDocNumber (falls back to si_doc_number)\n  \"invoice_date\": \"…\",             // supplierDocDate, else siDocDate\n  \"due_date\": \"…\", \"supplier\": \"…\",\n  \"is_credit\": false,               // credit memo → handle separately, never a bill\n  \"has_lines\": true,                // false for scanned/OCR docs (header totals only)\n  \"placeholder_lines\": 0,           // empty rows the API returned and we dropped\n  \"placeholder_note\": null,         // what they said, e.g. \"SEE VENDOR INVOICE FOR DETAIL.\"\n                                    // has_lines false ⇒ this document appears in\n                                    // `needs_line_recovery`; see the retrieval\n                                    // procedure above before billing it\n  \"lines_source\": \"pdf\",            // present only when lines were recovered from a PDF\n  \"total\": 0,                       // docTotal\n  \"charges\": {\"merchandise\", \"freight\", \"freight_allowance\", \"si_upcharge\",\n              \"svc_handle\", \"sales_tax\", \"excise_tax\", \"discount\"},\n  \"lines\": [{\"item\",\"upc\",\"description\",\"size\",\"color\",\"unit\",\n             \"qty_ordered\",\"qty_shipped\",\"qty_backordered\",\n             \"list_price\",\"discount_pct\",\"net_price\",\"extension\"}]\n}\n```\n\nThis is the **same shape a PDF-extracted invoice would have**, so a payables\nworkflow reconciles it without caring that it came from SportsLink.\n\n## Rules that matter\n\n- **Don't pull before ~10:30am ET** — SI's internal processing runs first; earlier\n  reads can be incomplete. Schedule accordingly.\n- **Line data is EDI-only, so scanned documents need the portal.** They come\n  back with header totals and no usable `lines` (`has_lines: false`) and are\n  named in `needs_line_recovery`. Recover each one via the retrieval procedure\n  above; if that cannot produce a *verified* result, escalate rather than\n  blind-bill. `ediOnly: true` exists to fetch *only* EDI documents — useful for\n  a narrow query, wrong for a billing run, because the documents it filters out\n  are still owed and will simply age.\n- **A scanned document returns a placeholder line, not an empty array.** One row\n  with no item and no UPC, zeroes throughout, and the description\n  `\"SEE VENDOR INVOICE FOR DETAIL.\"` — SI's own instruction, the same sentence\n  printed on the PDF's cover page. A line counts only if it identifies a product\n  (item number or UPC) or carries a non-zero number; a description alone does\n  not, or that row reads as real. Dropped rows surface as `placeholder_lines` /\n  `placeholder_note`. See `references/sportslink_api.md`.\n- **`is_credit: true` is a credit memo** — route it to a human / vendor credit,\n  never create it as a payable.\n- **Exactly-once — the golden rule.** Import first, `mark-historical` **after**\n  the bill is created. This adapter deliberately does **not** use the API's\n  `moveToHistorical=true` GET flag (which marks on read, before billing) — a\n  crash between read and bill would silently drop the invoice. The natural loop:\n  `list active` → bill each in the ERP → `mark-historical` the ones that\n  succeeded; failures/escalations stay active and are retried next run.\n- **Paging** is automatic (`all: true`, the default). Max 1000 docs/call on SI's\n  side; the helper pages to the end (capped at 50 pages as a backstop).\n\n## Header-only documents: recovering lines from the PDF\n\nSports Inc scans some supplier invoices instead of receiving them as EDI. Those\ndocuments arrive with header money but no `lines`, and the line detail exists\nonly in the PDF in the SportsWeb Invoice Center — there is no API for it. This\nskill recovers those lines in three beats, with **you** as the middle one:\n\n```\nfetch-invoice-doc  →  you read the PDF  →  reconcile-lines\n   (Python: I/O)      (the only fuzzy step)   (Python: arithmetic)\n```\n\nNo extraction *model* is involved anywhere: OCR transcribes the scan, the agent\ninterprets the text, and arithmetic checks the result. Scanned pages are OCR'd\nbefore you see them, so what lands in context is text rather than a 300dpi\nimage — the images stay on disk for when the text is not good enough.\n\n**What a download contains.** Not one invoice — a stack of documents, each being\na landscape **SI cover page** (native text, no line detail, it says\n`SEE VENDOR INVOICE FOR DETAIL.`) followed by the **vendor invoice**, usually a\n300dpi scan. That scan is where the line items are; the SI cover is useless for\nextraction, since if it held line detail the API would have had it too. One PDF\nroutinely bundles several such pairs — one per SI document on the PO — so\n`fetch-invoice-doc` groups the pages into `documents` and flags which group is\nthe one you asked for.\n\n> **Build status.** Working end to end. Extraction and reconciliation are\n> covered by `scripts/_selftest.py` (33 tests) and were run against a real\n> two-document download (PO P13554), with both documents' lines reconciling to\n> the cent, and the **whole chain has since run end to end on a live document**:\n> API lookup → placeholder detected → portal login → download → segment → OCR →\n> reconcile → `verified`, variance 0.00 (SI 24684277, 183.20 + 215.20 = 398.40).\n> The portal automation was confirmed on the live portal: a\n> `capture-portal` run logged in, searched, parsed both rows, ticked one, and\n> pulled back a 121,619-byte PDF in 17.9s. `fetch-invoice-doc` has then been run\n> end to end for a single `si_doc_number` — it matched that one row, downloaded\n> a 2-page document (not the 4-page PO bundle), and extracted the scanned vendor\n> invoice to a PNG for reading.\n>\n> **Headless is confirmed** — a cold run completed the same chain in 20.7s.\n> Still open, and listed in\n> [`references/sportsweb_flow_notes.md`](references/sportsweb_flow_notes.md):\n> every run has been from a **local machine**, so a datacenter IP is untested\n> (Auth0 treats a cloud runner as a new device);\n> **multi-page results** are not handled (`hdnMaxPage` is read but the driver\n> takes page 1 only); and the Archived-tab fallback has never been exercised.\n> `{\"pdf_path\": \"/path/to/invoice.pdf\", \"si_doc_number\": 24682750}` still works\n> for a PDF pulled by hand.\n\n### The portal flow\n\n```\nwww.sportsinc.us  →  click DEALER LOGIN  →  sportsweb.us.auth0.com/u/login\n  →  swv3.sportsinc.com/home        ← the home screen, and the only search box\n  →  type the PO, click Search      → swv2h.sportsinc.com/Member/InvoiceCenter/…\n  →  tick the matching row(s)  →  Downloads  →  \"PDF File\"\n```\n\nThree steps in that chain are counter-intuitive, and each cost a debugging round:\nthe login redirect **403s** if navigated to directly (it must be clicked from the\npublic site); home and the Invoice Center are **different hosts**, so a session\ncheck pointed at the wrong one always fails and forces a needless re-login; and\nthe Invoice Center **404s** on a direct hit even though its URL appears in the\naddress bar after a search.\n\nNothing waits on `networkidle` — the Vue home screen holds connections open and\nmay never reach it, and a long wait for an event that will not come looks exactly\nlike a hang. Every wait targets a concrete element, and each step is recorded\nwith elapsed milliseconds in the `trace` that `capture-portal` returns. Downloading is **two clicks**: `Downloads` opens\nan in-page dialog, and `PDF File` inside it merges every ticked document into\none PDF.\n\nSelectors bind to what is durable. WebForms ids are\n`<framework prefix>_<authored control name>`, so controls are matched on the\n**suffix** (`table[id$='grdInvoices']`, `input[id$='_chkItem']`); Auth0's classes\nare per-build hashes and are never used, its ids are. Rows are matched on their\n`SI Doc No.` cell, never by position — downloading the wrong invoice is the one\nfailure reconciliation cannot catch, since another document's lines are\ninternally consistent and simply tie to a different header.\n\nOne trap worth knowing about, because it would fail quietly: the grid's\nfrozen-header script **clones the entire `<thead>` into body cells**, so a naive\ncell read returns every column heading followed by the value. Cell text is read\nwith a tree walker that skips cloned headers, and any value that still carries\none is rejected rather than trimmed.\n\n### `capture-portal` — run this first\n\n```bash\npython3 scripts/sportslink.py capture-portal '{\"search\": \"P13554\"}'\npython3 scripts/sportslink.py capture-portal '{\"search\": \"P13554\", \"probe_download\": true}'\n```\n\nAdd `{\"archived\": true}` to exercise the Archived-tab path (tab switch, column\nselection, re-search) — the one branch of the flow that has never run.\n\nA read-only diagnostic: logs in, runs the search, and reports the landing URL,\nwhich elements it located, the portal's own hidden state fields, and the rows it\nparsed — plus a full-page screenshot and the page HTML. With `probe_download` it\nwalks the download dialog and records how the file arrives (a download event or\nan inline response), its size, and whether it starts with `%PDF-`. Nothing is\nmodified either way. Add `{\"headless\": false}` to watch it run.\n\n`download_format` reaches the dialog's other options (`pdf_zip`, `csv`,\n`csv_items`, `pdf_and_csv`); `pdf` is the default and the only one this skill\nuses. In particular the portal's *CSV with Header and Item Detail* export is\n**not** a shortcut around reading the scan — for a header-only document it comes\nback with no item detail, because it is fed by the same EDI line data the\nSportsLink API exposes. If the CSV had the lines, the API would have had them.\n\n### `fetch-invoice-doc`\n\n```bash\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 24682750}'\n```\n\nFetches the PDF, classifies every page, and makes the readable parts readable:\n\n- `documents` — the stack split into `{si_cover_page, detail_pages,\n  si_doc_number_candidates, supplier_doc_candidates, detail_is_scanned,\n  matches_requested}`. Read the group with `matches_requested: true` and\n  reconcile one document at a time.\n- `text` — **every readable page, native or OCR'd**, page-marked. A transcribed\n  page is labelled `[OCR: rapidocr, mean confidence 0.98]` so it can never be\n  mistaken for the document's own text layer — only one of the two can misread a\n  digit.\n- `image_paths` — PNGs extracted from the scanned pages (`…-p2.png` for page 2).\n  Kept as the cross-check even when OCR succeeded. The scans sit two levels deep\n  inside Form XObjects, so they are pulled out explicitly rather than left to a\n  PDF renderer that may not exist on the host.\n- `pages[].kind` — `si_cover` | `text` | `image` | `blank`, per page.\n- `readable_text_covers_all_pages` — the flag that matters: every page has text,\n  from either source. (`has_text_layer` is true whenever *any* page has a native\n  layer, and the SI covers always do, so a naive read of it concludes a scanned\n  invoice is fully readable when it contains no line detail at all.\n  `text_covers_all_pages` means native text alone was enough.)\n- `ocr_pages` / `ocr_engine` / `pages_without_text` — which pages were\n  transcribed, by what, and which could not be read at all.\n- `pdf_path` — the whole PDF, kept as a fallback when any page is scanned.\n\nMulti-page results are handled: a single document is found by narrowing the grid\nsearch to its SI Doc No. rather than paging, and a whole-PO fetch reads every\npage and merges the per-page downloads (selection does not survive paging).\n`pages_searched` and `download_parts` in the response say what it took.\n\nIdentify what to fetch with `si_doc_number` (one document — preferred, less to\ndisambiguate) or `po_number` (every supplier invoice on that PO in one browser\ntrip; the Invoice Center searches by PO and returns a row per invoice, and its\n*Downloads* button combines the ticked rows into one PDF).\n\n**Active then Archived, automatically.** A miss on the Active tab re-runs the\nsearch on Archived — no flag, because a caller usually cannot know which tab a\ndocument is on. `portal_tab` in the response says where it was found, and a hit\non Archived also returns `already_historical_warning`: Sports Inc marks a\ndocument historical only after it has been billed, so finding it there is a\nduplicate-payment tell, not a routing detail. `stash`\n(`auto` | `always` | `never`) controls what gets written. Other options:\n`pdf_path` (read a PDF off disk instead of the portal), `force` (re-read a\ndocument that *does* have EDI lines — normally refused), and inline `username` /\n`password` / `base_url` portal overrides.\n\nThe response carries the document's identity — PO, invoice number, date,\nsupplier, credit flag — but **deliberately not its money**. Extraction should be\nblind to the total it will be checked against; an agent that knows the\nmerchandise total can unconsciously bend a misread line to hit it, which is the\nexact error the next step exists to catch.\n\n### You read it\n\n[`references/pdf_extraction.md`](references/pdf_extraction.md) is the extraction\nguide: which pages to read and which to skip, how to confirm you are on the\nright vendor invoice, the field mapping, the rules (transcribe don't compute,\nnever infer a quantity, bill the shipped quantity, one object per printed row,\nskip subtotals and freight), and a worked example from a real download. Read it\nbefore extracting.\n\n### `reconcile-lines`\n\n```bash\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 23962348, \"lines\": [...]}'\n```\n\nNormalises your lines into the canonical shape and checks them against the\nheader **fetched fresh from SportsLink** — never against numbers you supply,\nsince a check against what the same agent just read verifies nothing:\n\n1. **Per-line math** — `qty_shipped × net_price` = `extension`, ±$0.02.\n2. **The anchor** — extensions must sum to `merchandiseTotal`, allowing a cent\n   of rounding per line. Falls back to `docTotal` minus charges when the header's\n   merchandise total is missing (an OCR gap).\n3. **Doc-total reassembly** — informational.\n\nReturns `status: \"verified\"` — an invoice in the ordinary normalised shape, with\n`lines_source: \"pdf\"`, that the payables workflow bills exactly like an EDI one —\nor `status: \"needs_review\"` with a per-issue breakdown.\n\n**A `needs_review` invoice is never billed.** Re-reading the PDF and re-running\nis fine and often the fix (a variance equal to one line's extension usually means\na missed row on page two). Adjusting a line to make the total tie is not: the\nwhole value of this path is that a model's reading gets audited by arithmetic it\ndoesn't control. If a second reading agrees with the first, escalate and leave\nthe SI document active.\n\n`reconcile-lines` marks nothing historical — the exactly-once seam stays exactly\nwhere it is.\n\n## Where this fits\n\nSource adapter (this) → payables **workflow** (`drivethru-payable-matching`) →\nERP **adapter** (`drivethru-odoo` / `drivethru_mcp`). This skill owns only the\n\"get the invoices + mark them consumed\" half; matching to POs, correcting\npricing, and creating the draft bill live in the workflow. See that skill's\n`references/sportsinc_payables.md` for the end-to-end procedure.\n\nThat procedure currently escalates every header-only document to a human. Once\nthe portal capture lands and `fetch-invoice-doc` works unattended, its\n\"**No lines?** → escalate\" step should instead route through the PDF fallback and\nescalate only on `needs_review`.\n\n## References\n\n- [`references/sportslink_api.md`](references/sportslink_api.md) — the API\n  itself: parameters, document/line fields, and the semantics behind them.\n- [`references/pdf_extraction.md`](references/pdf_extraction.md) — how to read a\n  scanned invoice PDF into line items. **Read before extracting.**\n- [`references/sportsweb_flow_notes.md`](references/sportsweb_flow_notes.md) —\n  the portal capture checklist. **Start here to finish the browser flow.**\n\nOffline tests for the extraction/reconciliation halves:\n`python3 scripts/_selftest.py` (needs `pypdf`; `reportlab` optional).\n\n## Agent-to-Agent (A2A) Mode\n\nThe `get-for-a2a` action provides a **contract-driven interface** for inter-agent\ncommunication. Deploy this skill on a dedicated Sports Inc agent and let the\ninternal agent that needs invoices (e.g. an Accounts Payable agent) reach it via\na **delegation connection** in the Knoxville platform.\n\n### Where `SPORTSINC_API_KEY` comes from (credential broker)\n\nThe `SPORTSINC_API_KEY` is bound to the **calling** agent (the one that\nrepresents your company — e.g. Accounts Payable), not to this Sports Inc agent.\nOn that agent's delegation connection to this one, the operator chooses to\n**share** `SPORTSINC_API_KEY` with the connection.\n\nThe value is **pulled on demand**, not pushed. When this agent handles a\ndelegated call (`X-Knox-Caller-Kind: agent`), **the runtime** (not you) fetches\nthe shared `SPORTSINC_API_KEY` for this conversation and places it into the\nskill's **execution environment for this turn only**, before your `exec` runs.\nThe platform verifies this agent is the target of the delegated conversation and\nthat the connection shares the credential, and **logs the access in\n`agent_connection_audit_log`**. `sportslink.py` then reads `SPORTSINC_API_KEY`\nfrom the environment exactly as it does standalone. You do **not** see this value\nin your context — it is deliberately kept out of the model prompt.\n\n**So on a delegated turn, just run the tool.** Your first action for a Sports\nInc request is the `exec` call itself — e.g.\n`python3 scripts/sportslink.py get-for-a2a '{...}'`. Do **not**, before running\nit:\n\n- call `get_my_bundle`, `get_delegated_credentials`, or any tool to look for or\n  \"verify\" the key — it is intentionally invisible to you, so you will always\n  find nothing and wrongly conclude you have no access;\n- spawn a sub-agent (`sessions_spawn`) to do this skill's job — you are the agent\n  that runs it;\n- tell the caller you lack the key or access **before** you have actually run the\n  script and read its output.\n\nIf the script itself reports an `auth_error` (or `config_error`), the caller's\nconnection hasn't shared the credential — surface that error rather than\nguessing, and never print the credential value into the chat reply.\n\n### Request Contract\n\n`get-for-a2a` params (all optional):\n\n```json\n{\n  \"customer_ref\": \"DEALER-001\",\n  \"date_range\": { \"start\": \"2024-01-01\", \"end\": \"2024-12-31\" },\n  \"include_historical\": false\n}\n```\n\n- `include_historical` (default `false`) → active/un-imported invoices only;\n  set `true` to also include historical/consumed docs.\n- `customer_ref` is **advisory only** — the SportsLink API key is per-dealer and\n  the API has no customer filter, so this field does **not** scope the result. It\n  is echoed back in `metadata.customer_ref` for the caller's audit.\n\n### Response Contract\n\nUnlike the other actions (which exit non-zero on error), `get-for-a2a` **always\nexits 0** and reports failure in-band, so an A2A caller reads one envelope shape\neither way.\n\nSuccess:\n\n```json\n{\n  \"success\": true,\n  \"invoices\": [ { \"source\": \"sports_inc\", \"po_number\": \"P13189\", \"si_doc_number\": 12345, \"total\": 1500.00, \"lines\": [] } ],\n  \"metadata\": { \"count\": 42, \"total_count\": 50, \"pages_read\": 1, \"source\": \"sports_inc\", \"customer_ref\": \"DEALER-001\", \"include_historical\": false },\n  \"error\": null\n}\n```\n\nError:\n\n```json\n{\n  \"success\": false,\n  \"invoices\": null,\n  \"metadata\": null,\n  \"error\": {\n    \"type\": \"auth_error|connection_error|api_error|validation_error\",\n    \"message\": \"Human-readable error message\",\n    \"retriable\": true\n  }\n}\n```\n\n### Replying to a delegated *task* — compact markdown, not raw JSON\n\nThe JSON envelope above is the contract for a **synchronous** `send_message`\ncall, where the caller reads the object programmatically. But when the payables\nagent reaches you as an **async delegated task** (it `start_task`s you a request\nand you report back with an outcome/summary), your reply is **free text another\nagent reads**, and that summary field has a hard **~20,000-character limit**. A\nraw JSON dump of several POs' invoices overflows it and is **silently\ntruncated** — which hands the payables agent a half-parsed payload and corrupts\nits billing. (This is exactly what happened once: five POs of pretty-printed\nJSON, cut off mid-object.)\n\nSo on a delegated task, **run `get-for-a2a` / `list` as usual, then hand back a\ncompact markdown breakdown** in your outcome summary — never paste the raw\nJSON. Markdown carries all the same data in a fraction of the characters and the\npayables agent reads it directly (it does not need strict JSON). Include every\nfield it needs, terse, and **do not wrap it in a code fence** (fences add bulk\nand confuse parsing):\n\n- One `## <po_number>` heading per PO.\n- One bullet per SI document: `si_doc_number`, `invoice_number`,\n  `invoice_date`, `due_date`, `is_credit`, `has_lines`, and the money from\n  `charges` + `total` (`merchandise`, `freight`, `si_upcharge`, `total`).\n- Under each document with `has_lines: true`, one terse line per item:\n  `item`, `upc`, `size`, `qty_shipped`, `net_price`, `extension`, `description`.\n  For a `has_lines: false` document write `detail no` and omit item lines.\n\nExample — keep it this tight (normalised field values, no code fence around it\nin your real reply):\n\n```text\n## P09409\n- SI 23962348 | inv# 6164920830 | 2026-02-16 | due 2026-05-10 | credit no | detail yes | merch 51.00 freight 8.58 upcharge 0.48 total 60.06\n  - JP1477 | 197612326076 | S | qtyShip 1 | net 12.75 | ext 12.75 | TF SHRT TIGHT M BLACK\n  - JP1477 | 197612326083 | M | qtyShip 3 | net 12.75 | ext 38.25 | TF SHRT TIGHT M BLACK\n- SI 23972779 | inv# 6164929812 | 2026-02-17 | due 2026-05-10 | credit no | detail yes | merch 180.61 freight 0.00 upcharge 1.45 total 182.06\n  - JJ1179 | 196476717082 | L | qtyShip 1 | net 25.50 | ext 25.50 | GG SL HD MGREYH\n```\n\nIf even the compact form would be too large (many POs, many lines), **never cut\nit silently**: return the POs you can and end with an explicit\n`NOTE: truncated — returned N of M POs, ask again for the rest`, so the caller\nknows to re-request rather than bill from a partial payload.\n\nThe `retriable` flag indicates whether the caller should retry (transient\nconnection errors) or escalate (auth/config errors).\n\nFile v0.7.1:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"sportsinc-sportslink\",\n  \"version\": \"0.7.1\",\n  \"publishedAt\": 1786111175182\n}\n\nFile v0.7.1:references/pdf_extraction.md\n\n# Reading a Sports Inc invoice PDF — extraction guide\n\nYou are here because a SportsLink document came back `has_lines: false`. Sports\nInc scanned it rather than receiving it as EDI, so the API has the header money\nbut no line items.\n\nThe *interpretation* is *your* job. The Python around you does everything\ndeterministic first: it fetches the PDF, classifies its pages, extracts the\nscanned ones, **OCRs them into text**, and then audits the lines you hand back.\n\nSo you are normally reading text, not looking at a picture. That ordering is\ndeliberate — text is a fraction of the context cost, and it can be diffed,\ngrepped and logged. The images stay on disk for when the text is not good\nenough, and there is no extraction *model* anywhere in the pipeline: OCR\ntranscribes, you interpret, arithmetic checks.\n\n## The loop\n\n```bash\n# 1. Get the document and make it readable.\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 24682750}'\n\n# 2. You read it. (This document.)\n\n# 3. Hand the lines back to be checked against the API's header money.\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 24682750, \"lines\": [...]}'\n```\n\nStep 3 returns `status: \"verified\"` — a normalised invoice the payables workflow\nconsumes exactly like an EDI one — or `status: \"needs_review\"`. **Only a\n`verified` invoice may be billed.**\n\n## What a Sports Inc download actually contains\n\nNot one invoice. A stack of documents, each of which is two parts:\n\n| | Page | Content |\n|---|---|---|\n| **SI cover** | landscape, native text | Sports Inc's own invoice. **No line detail** — it says, in as many words, `SEE VENDOR INVOICE FOR DETAIL.` Carries the SI document number, the PO number, and the totals. |\n| **Vendor invoice** | portrait, usually a **300dpi scan** | The actual supplier invoice. **This is where the line items are.** |\n\nOne PDF routinely holds several of these pairs — one per SI document on the PO.\n\n**Ignore the SI cover pages.** They are useless for extraction; if they held line\ndetail, the API would have had it too. Their only jobs are to divide the stack\ninto documents and to tell you which SI document number each vendor invoice\nbelongs to.\n\n`fetch-invoice-doc` does that division for you:\n\n```json\n\"documents\": [\n  {\"si_cover_page\": 1, \"detail_pages\": [2], \"detail_is_scanned\": true,\n   \"si_doc_number_candidates\": [24682750], \"supplier_doc_candidates\": [\"SI3503366\"],\n   \"matches_requested\": true},\n  {\"si_cover_page\": 3, \"detail_pages\": [4], \"detail_is_scanned\": true,\n   \"si_doc_number_candidates\": [24684277], \"supplier_doc_candidates\": [\"SI3503509\"],\n   \"matches_requested\": false}\n]\n```\n\n**Extract the document with `matches_requested: true`, and reconcile one document\nat a time.** Its lines tie to *its* SI document's totals, not to the stack's.\nThe others in the same PDF are separate SI documents with their own\n`si_doc_number`; handle each with its own `fetch-invoice-doc` /\n`reconcile-lines` pair. If nothing matches, `notes` will say so — stop and check\nyou have the right PDF rather than extracting the first document you see.\n\n## How to read the pages — text first\n\n**Read `text`.** It carries every readable page in page order, including the\nscanned ones, which are OCR'd before you see them. A transcribed page is marked:\n\n```\n----- page 2 of 2 -----  [OCR: rapidocr, mean confidence 0.976]\n     A062RY       ROYAL     Adult1-1/2\"   80EA   80EA   0EA   $2.29   $183.20\n     AS1RY-L      ROYAL                   80PR   80PR   OPR   $2.69   $215.20\n                                                Subtotal:             $398.40\n```\n\nColumn positions are reconstructed from the OCR boxes, so a line still reads as\na row. Note what OCR gets wrong even on a clean scan: `OPR` for `0PR`, `$O` for\n`$0`, and the size `L` on the second line dropped entirely. **Digits and money\nare what matter, and those came through exactly.**\n\nThe other fields:\n\n- `image_paths` — PNGs of the scanned pages. Kept even when OCR succeeded, as\n  the cross-check for anything that looks wrong.\n- `readable_text_covers_all_pages` — every page has text, from either source.\n- `ocr_pages` / `ocr_engine` — which pages were transcribed and by what.\n- `pages_without_text` — pages nothing could read. If this is non-empty, the\n  images are the only way in.\n- `pages[].kind` — `si_cover` / `text` / `image` / `blank` per page.\n- `pdf_path` — the whole PDF, kept as a last resort.\n\nDo not trust `has_text_layer` on its own: it is true whenever *any* page has a\nnative text layer, and the SI covers always do.\n\n### When to look at the image instead\n\nOCR is the cheap first attempt, not the last word. Read the image when:\n\n- a row's numbers look implausible, or a column is obviously missing;\n- the row appears in `low_confidence_rows`. That list is deliberately narrow: a\n  row is flagged only when a token **carrying a number** is doubtful, and\n  `weakest_value` names it. A shaky colour or product name does not qualify,\n  because no arithmetic downstream consumes it — on a real scan the money read\n  at 1.00 while whole rows scored 0.68 on the backorder zeros, and flagging\n  those would mark every line item on every invoice as suspect. Each row also\n  reports `confidence` (worst token of any kind) next to `value_confidence`\n  (worst number), so you can see the difference;\n- **`reconcile-lines` comes back with a variance.** That is the designed\n  escalation: text → image → escalate to a human. A misread digit is exactly\n  what the merchandise-total check exists to catch, so a failed reconciliation\n  is a prompt to look at the scan, not to give up.\n\nThis ordering is why OCR is safe here at all. Nothing it transcribes reaches a\nbill without tying to the API's own header.\n\nNote what is **not** in the response: the merchandise total, the doc total, the\ncharges. That omission is deliberate. Extraction must be blind to the number it\nwill be checked against — knowing the target is how a misread line quietly gets\nbent to hit it. Transcribe what the page says and let the check do its job.\n\n## Cross-check you have the right invoice\n\nBefore extracting, confirm the vendor invoice you are reading belongs to the SI\ndocument you asked for:\n\n- The vendor invoice's **PO #** should match the SI document's `po_number`.\n- The vendor invoice's **INVOICE #** should match the SI cover's supplier\n  document number, ignoring punctuation — `SI-3503366` on the vendor invoice is\n  `SI3503366` in `supplier_doc_candidates` and in SportsLink's\n  `supplierDocNumber`.\n\nIf they disagree, stop. Reconciliation will *not* reliably catch a\nwrong-document mix-up: another invoice's lines are internally consistent, they\njust belong to a different header.\n\n## The fields\n\nOne object per line item, in reading order. Field names are flexible (`style`,\n`quantity`, `unit_price`, `amount` and other common aliases are accepted), but\nthese are the canonical ones:\n\n| Field | What it is | Required |\n|---|---|---|\n| `item` | The supplier's item/SKU number | strongly preferred |\n| `upc` | UPC/barcode, digits only | if printed |\n| `description` | The product description as printed | if printed |\n| `size` | Size as printed (`S/M`, `L`, `Adult 1-1/2\"`) | if printed |\n| `color` | Color as printed | if printed |\n| `unit` | Unit of measure — **`EA` and `PR` are not the same thing** | if printed |\n| `qty_shipped` | Quantity **shipped/billed** — the one the money is based on | **yes** |\n| `qty_ordered` | Quantity ordered, when both are shown | if printed |\n| `qty_backordered` | Quantity backordered (`B.ORDERED`) | if printed |\n| `list_price` | Undiscounted unit price | if printed |\n| `discount_pct` | Discount percentage | if printed |\n| `net_price` | The unit price actually billed (`UNIT PRICE`) | **yes** |\n| `extension` | The line's extended amount (`ITEM TOTAL`) | **yes** |\n\nA line missing any of the three required fields comes back `needs_review` — that\nis intended. An unreadable row is a reason to escalate, not to guess.\n\n## Rules\n\n**Transcribe, don't compute.** Report the numbers the invoice prints. If a page\nprints qty, unit price, and item total, give all three even when they look\ninconsistent — an inconsistency you preserve is a caught error, one you silently\nfix is a billing error. (When a value genuinely isn't printed, leave it out; the\nnormaliser derives the one missing leg of qty × net = extension and marks it\n`derived`.)\n\n**Never infer a quantity.** Price and extension can sometimes be recovered from\neach other. A quantity cannot. If you can't read it, leave it out.\n\n**Bill the shipped quantity.** Invoices show ORDERED, SHIPPED, and B.ORDERED.\nThe money follows SHIPPED; put the other two in their own fields.\n\n**One object per printed line.** Don't merge two sizes of one item into a\ncombined quantity, and don't split a line into per-unit rows. Size-broken\napparel invoices repeat the same item number many times — keep each row.\n\n**Skip everything that isn't a line item.** Subtotal, Freight Flatrate, TOTAL,\ntax, remittance blocks, \"continued\" markers. Freight and the SI upcharge are\nalready in the API header — adding either as a line will fail reconciliation,\nwhich is exactly what should happen.\n\n**Multi-page vendor invoices continue.** A per-page subtotal is not the invoice\ntotal. Read every detail page in the document's `detail_pages` before you stop.\n\n**Credits are negative.** On a credit memo (`is_credit: true` on the fetch\nresponse), transcribe amounts as negative. A credit memo is never billed\nregardless of what reconciliation says — it goes to a human.\n\n**UPCs are digits.** Strip spaces and hyphens; keep leading zeros.\n\n## Worked example — PO P13554\n\nThe download holds two documents. Reading the second one's vendor invoice\n(page 4, a Champro scan):\n\n```\nINVOICE #: SI-3503509        PO #: P13554\nPRODUCT / SKU   COLOR   SIZE          ORDERED  SHIPPED  B.ORDERED  UNIT PRICE  ITEM TOTAL\nMVP Belt\nA062RY          ROYAL   Adult 1-1/2\"    80 EA    80 EA      0 EA        $2.29     $183.20\nPro Sock\nAS1RY-L         ROYAL   L               80 PR    80 PR      0 PR        $2.69     $215.20\n                                                            Subtotal:            $398.40\n                                                            Freight Flatrate:         $0\n                                                            TOTAL:               $398.40\n```\n\nExtract:\n\n```json\n{\n  \"si_doc_number\": 24684277,\n  \"lines\": [\n    {\"item\": \"A062RY\", \"description\": \"MVP Belt\", \"color\": \"ROYAL\",\n     \"size\": \"Adult 1-1/2\\\"\", \"unit\": \"EA\",\n     \"qty_ordered\": 80, \"qty_shipped\": 80, \"qty_backordered\": 0,\n     \"net_price\": 2.29, \"extension\": 183.20},\n    {\"item\": \"AS1RY-L\", \"description\": \"Pro Sock\", \"color\": \"ROYAL\",\n     \"size\": \"L\", \"unit\": \"PR\",\n     \"qty_ordered\": 80, \"qty_shipped\": 80, \"qty_backordered\": 0,\n     \"net_price\": 2.69, \"extension\": 215.20}\n  ]\n}\n```\n\nSubtotal, Freight Flatrate, and TOTAL are **not** lines. Reconciliation sums the\ntwo extensions to 398.40 and ties them to the SI document's merchandise total —\nthe SI upcharge of 3.19 that makes up the 401.59 document total is SI's, not a\nvendor line.\n\n## What the check actually checks\n\n1. **Per-line math** — `qty_shipped × net_price` = `extension`, ±$0.02.\n2. **The anchor** — extensions must sum to the API's `merchandiseTotal`, allowing\n   a cent of rounding per line. Catches a dropped line, a doubled line, or a\n   misread that per-line math can't see. Falls back to `docTotal` minus charges\n   when the header has no merchandise total.\n3. **Doc total reassembly** — informational.\n\nA vendor invoice's own TOTAL normally equals the SI merchandise total, since SI\nadds its upcharge and freight on top. If the two disagree, that is a real\ndiscrepancy for a human — not something to reconcile away.\n\n## When it comes back `needs_review`\n\nRead the `issues` list; each names the line and the variance. Then:\n\n- **Re-read and re-run.** Usually the answer — a variance equal to one line's\n  extension means a row was missed, often on a second detail page.\n- **Escalate.** If a second reading agrees with the first, the document is the\n  problem, not your reading. Leave it; the SI document stays active and\n  reappears next run.\n- **Do not override the check**, adjust a line to make the total tie, or bill a\n  `needs_review` invoice. The whole point of this path is that a model's reading\n  gets audited by arithmetic it doesn't control.\n\n## After a verified invoice is billed\n\nNothing changes about the exactly-once rule: `reconcile-lines` marks nothing.\nThe bill gets created first, then `mark-historical`.\n\nFile v0.7.1:references/sportslink_api.md\n\n# SportsLink API — reference (distilled from the 2024 Dealers spec)\n\nThe `scripts/sportslink.py` helper wraps all of this; read here when you need a\nparameter, a field, or the semantics behind one.\n\n## Basics\n\n- **Base URL:** `https://api.sportsinc.com/`\n- **Auth:** API key in the `X-API-KEY` request header. Request one from\n  `mhoerner@hq.sportsinc.com`.\n- **Limits:** max **1000 documents per call** — page. Don't retrieve before\n  **~10:30am ET** (SI internal processing completes first).\n\n## GET `/dealers/documents/` — the invoices\n\nReturns JSON documents from the SportsWeb Invoice Center.\n\n### Query parameters (all optional)\n\n| Param | Type | Notes |\n|---|---|---|\n| `poNumber` | string | **Dealer PO number** — the match key to your ERP PO |\n| `supplierDocNumber` | string | The underlying supplier's document number |\n| `siDocNumber` | int | Sports Inc's document id (used to mark historical) |\n| `siDocDate` / `siDocStartDate` / `siDocEndDate` | date `yyyy-MM-dd` | SI processing date. A single date or `start,end`. Start-only = on/after; end-only = on/before |\n| `supplierDocDate` / `supplierDocStartDate` / `supplierDocEndDate` | date | Supplier document date, same range semantics |\n| `lines` | bool (default false) | Include line-item data — **EDI documents only** |\n| `active` | bool (default false) | Only documents **not** marked historical (the un-imported inbox) |\n| `moveToHistorical` | bool (default false) | Mark the returned docs historical **on read** — ⚠️ do NOT use for billing (marks before you've billed); use the PATCH endpoint after billing instead |\n| `excludeScannedDocuments` | bool (default false) | Only documents with line-item data (EDI); scanned/OCR docs have none |\n| `fields` | string[] | Sparse fieldset — return only these properties |\n| `page` / `pageSize` | int | Paging (`pageSize` default = total doc count) |\n| `orderBy` (default `SIDocDate`) / `orderByDescending` | string / bool | Sorting |\n\nExample: `GET /dealers/documents/?siDocDate=2024-01-31&lines=true&active=true`\n\n### Response envelope\n\n`{ items: [ …document… ], pageNumber, pageSize, totalPages, totalCount,\nhasPreviousPage, hasNextPage, orderBy, orderByDescending }`\n\n### Document (header) fields\n\n`poNumber`, `siDocNumber`, `siDocDate`, `dueDate`, `discountDate`,\n`requestedShipDate`, `shipDate`, `supplierDocNumber`, `supplierDocDate`,\n`supplier`, `isCredit`, plus money: `merchandiseTotal`, `freightAmount`,\n`freightAllowance`, `discountAmount`, `siUpcharge`, `svcHandleCharge`,\n`salesTax`, `exciseTax`, `docTotal`. Also `termsOfPayment`, `termsOfDelivery`,\n`carrier`, `weight`, `trackingNumber`, `methodOfPayment`, `supplierAddress{}`,\n`shippingAddress{}`. Missing/OCR-unavailable properties are simply omitted.\n\n### Line fields (EDI only)\n\n`supplierItemNumber`, `upc`, `quantityShipped`, `quantityOrdered`,\n`quantityBackOrdered`, `unit`, `listPrice`, `discountPercent`, `netPrice`,\n`extension`, `size`, `color`, `description`.\n\n`docTotal` = `merchandiseTotal` + `siUpcharge` + `svcHandleCharge` +\n`freightAmount` + `salesTax` + `exciseTax` − `discountAmount` −\n`freightAllowance` (SI-specific charges included — this is what you'll be\nbilled, and what the ERP bill's `expected_total` should tie to).\n\n## PATCH `/dealers/documents/status` — mark consumed\n\nBody: `{ \"siDocNumbers\": [12345, 23456], \"isActive\": false }`\n- `isActive: false` → historical (moves to the Invoice Center \"Historical\" tab).\n- `isActive: true` → back to active.\n\nResponses: `204 No Content` (success), `400` (bad body), `401` (bad key).\n\n**Use this to mark documents consumed AFTER a bill is created** — the exactly-once\nseam. Prefer it over `moveToHistorical=true` on the GET, which marks on read.\n\n## ⚠ Scanned documents return a **placeholder line**, not an empty array\n\nObserved live on `siDocNumber` 24684277 (merchandise total 398.40, whose vendor\ninvoice really has two lines) — the API returned `lines` containing exactly one\nrow:\n\n```json\n{\"supplierItemNumber\": \"\", \"upc\": null, \"size\": null, \"color\": null, \"unit\": \"\",\n \"quantityOrdered\": 0, \"quantityShipped\": 0, \"quantityBackOrdered\": 0,\n \"listPrice\": 0.0, \"discountPercent\": null, \"netPrice\": 0.0, \"extension\": 0.0,\n \"description\": \"SEE VENDOR INVOICE FOR DETAIL.\"}\n```\n\nThe row is **not blank**. It carries SI's own instruction — the same sentence\nprinted on the cover page of the PDF. So a free-text `description` is not\nevidence of a real line; here it is evidence of the opposite.\n\nThe spec's \"scanned/OCR documents have no line data\" is therefore not literally\ntrue at the wire level, and the naive test for it is wrong:\n\n```python\nhas_lines = bool(document.get(\"lines\"))   # ← WRONG: true for a stub\n```\n\nThat mistake is quiet and expensive. A scanned document announces itself as\nEDI-backed, and any consumer branching on `has_lines` — the payables workflow\ndoes, to decide between line-matching and escalating — line-matches against a\nrow of zeroes rather than escalating or reading the PDF.\n\n`scripts/sportslink.py` filters these out. A line is real if it **identifies a\nproduct** (`supplierItemNumber` or `upc`) or carries a **non-zero number**.\n`description`, `size`, `color` and `unit` are descriptive and do not qualify on\ntheir own: with every quantity and price at zero there is nothing billable, and\ntreating the description as identity is exactly what let this row through the\nfirst time.\n\n`has_lines` is computed from what survives; `placeholder_lines` counts the\ndropped rows and `placeholder_note` keeps their text, so neither the filtering\nnor SI's explanation is lost. A legitimately fully-backordered line — zero\nshipped, but with an item number and a backorder quantity — is kept.\n\n**Corollary worth applying elsewhere:** because the API can hand back line data\nthat is present but not complete, any line set is worth checking against\n`merchandiseTotal` before it is billed, whether it came from EDI or a PDF.\n\n## Failure modes to expect\n\n- `401` — bad/expired key (the helper surfaces `auth_error`).\n- Transient `429`/`5xx`/timeouts — the helper retries with backoff.\n- Missing fields — OCR gaps or supplier-not-provided; simply absent, not null.\n- Scanned docs — no `lines`; `has_lines: false` in the normalised shape.\n\nFile v0.7.1:references/sportsweb_flow_notes.md\n\n# SportsWeb portal — invoice PDF flow\n\nThe reverse-engineered flow behind `scripts/sportsweb_browser.py`, captured from\na live walkthrough of the portal.\n\n**Status: confirmed end to end on the live portal.** A `capture-portal` run with\n`probe_download` logged in, searched, parsed both P13554 rows, ticked one, and\npulled back a 121,619-byte PDF in 17.9 seconds — see \"What the live run proved\"\nbelow.\n\n## Hosts\n\n| | |\n|---|---|\n| Public site | `https://www.sportsinc.us/` — Wix marketing site. **The entry point**: its DEALER LOGIN button must be *clicked*. |\n| Login entry | `https://swv3.sportsinc.com/login-redirect` — the button's href. **Returns 403 on a direct hit**; only works as a click-through. |\n| Identity provider | `https://sportsweb.us.auth0.com/u/login?state=…` (Auth0 Universal Login) |\n| **Home screen** | `https://swv3.sportsinc.com/home` — where login lands, and the **only** page with a search box |\n| Invoice Center | `https://swv2h.sportsinc.com/Member/InvoiceCenter/Default.aspx` — where the search navigates to |\n\n**Home and the Invoice Center are different hosts.** That is not cosmetic: a\nsession check pointed at `swv2h` can never find the search box, so it always\nreports \"not logged in\" and sends every run through a full login it did not\nneed. `SPORTSINC_WEB_HOME_URL` overrides the home; `SPORTSINC_WEB_BASE_URL` the\nInvoice Center host.\n\nThe Invoice Center is **ASP.NET WebForms**; the home screen around it is a Vue\napp. Both appear in the flow.\n\n## Waiting — never on `networkidle`\n\nThe home screen is a Vue app that holds connections open, so `networkidle` may\nsimply never arrive. Waiting on it is indistinguishable from a hang, and it is\nwhat made the first live run appear to freeze. Every wait is for a concrete\nelement, timeouts default to 25s (`SPORTSINC_WEB_TIMEOUT_MS`), and every step is\nrecorded with elapsed milliseconds in the `trace` that `capture-portal` returns —\nso a slow run reports where it is instead of going quiet. A selftest guards\nagainst `networkidle` being reintroduced.\n\n## Selector strategy\n\nGenerated ids are avoided where they are truly generated, but WebForms ids have\na useful property: `ctl00_ContentPlaceHolder1_grdInvoices` is\n`<framework prefix>_<developer's control name>`. The **suffix** is authored and\nstable; the prefix is the framework's. So controls are matched on the suffix —\n`table[id$='grdInvoices']`, `input[id$='_chkItem']`,\n`a[id$='lbDisplayDownload']` — which survives a page restructure that moves the\nprefix. Auth0's classes are per-build hashes (`c72825458`) and are never used;\nits ids (`#username`, `#password`) are.\n\nA row is matched on its **SI Doc No.** cell, never by position. Downloading the\nwrong invoice is the one failure reconciliation cannot catch: another document's\nlines are internally consistent and simply tie to a different header.\n\n---\n\n## Step 1 — Login ✅ confirmed live\n\nLoad `https://www.sportsinc.us/` and **click** the DEALER LOGIN button\n(`a[href*='login-redirect']` — its Wix classes are generated, the href is not).\nThe button carries `target=\"_blank\"`, which is stripped before the click so the\nflow stays in one page; a popup is adopted if one opens anyway.\n\nDo **not** navigate to the button's href directly: it returns 403. (It does\neventually bounce to the login form on its own, which is why an early version\nappeared to work — it was just outlasting the 403 page with a 45s wait.)\n\nThe chain then reaches Auth0 with a per-session `state`. **Never construct that\nURL** — let the redirect produce it.\n\n| Element | Selector |\n|---|---|\n| Username / email | `input#username` |\n| Password | `input#password` |\n| Submit | `button[type=submit][name=action]` (\"Continue\") |\n| Session token | `input[name=state]`, hidden, submitted with the form |\n\nUnused: **Continue with Google** (`form[data-provider=google]`) and *Reset\npassword*. Staying on `sportsweb.us.auth0.com` after submit means failure; the\ndriver reads `#ulp-error-announcer` / `[role=alert]` for the reason, and a `/mfa`\nURL raises a distinct error pointing at `SPORTSINC_WEB_STATE_PATH`.\n\nLogin lands on `https://swv3.sportsinc.com/home`. The definitive \"we are in\"\nsignal is that page's search box — not a load state.\n\n**Still to confirm:** whether MFA/device verification appears from a datacenter\nIP (a local Windows run did not trigger one).\n\n### Session reuse\n\n`SPORTSINC_WEB_STATE_PATH` persists the session (Playwright `storage_state`) and\nrestores it next run, skipping Auth0 entirely — and it is the escape hatch if MFA\nturns out to be mandatory: log in once by hand with it set.\n\n## Step 2 — Reaching the Invoice Center ✅ confirmed live — **not by URL**\n\n```\nhttps://swv2h.sportsinc.com/Member/InvoiceCenter/Default.aspx?search=P13554\n```\n\nThat URL appears in the address bar after a search, and it is the `href` of the\nsearch button — **but navigating to it directly returns 404.** The page is only\nreachable by submitting the home screen's search box.\n\nSo after login, on the home screen:\n\n| Element | Selector |\n|---|---|\n| Search box | `.search-bar input[placeholder='Search for Invoice']` (Vue component) |\n| Search button | `.search-bar .search-button a` |\n\nFill, click, wait for the grid. The button's `href` is built by the page's own\nscript, so it is clicked rather than read and re-navigated.\n\n## Step 3 — The results grid ✅ confirmed live\n\nGrid: `table[id$='grdInvoices']`. Rows: `tr.gridview-row` / `tr.gridview-alt-row`\n(alternating classes). Columns, in order:\n\n`☐ · Supplier · Supplier Doc No. · PO No. · SI Doc No. · SI Doc Date · Due Date ·\nArchived Date (hidden on Active) · Discount Date · Total`\n\nP13554 returns two rows: SI3503366 / 24682750 / $5.23 and SI3503509 / 24684277 /\n$401.59.\n\n### ⚠ The frozen-header trap\n\nThe page's frozen-header script **clones the entire `<thead>` into individual\nbody cells**. The PO cell is really:\n\n```html\n<span id=\"…_dealerPONum\"><thead>…every column heading…</thead>P13554</span>\n```\n\nConsequences, all of which the driver handles explicitly:\n\n- A naive `td.textContent` returns `\"Supplier Supplier Doc No. PO No. … Total\n  P13554\"`. Cell text is therefore read with a `TreeWalker` that skips any node\n  inside a `thead`/`th`, and any value that still contains a heading is\n  **rejected**, not trimmed — trimming would be guessing at which invoice to\n  download.\n- An unscoped `table.locator(\"th\")` matches the clones too and shifts every\n  column index. Headers are read as `:scope > thead > tr > th`.\n- Each cloned header carries a copy of the **select-all** checkbox\n  (`chkAll`), so \"the first checkbox in the row\" finds the wrong control. Row\n  selection targets `input[id$='_chkItem']`.\n\n### Tabs — switching **resets the results**\n\n`#activeInv` / `#histInv` anchors, backed by hidden submits\n(`btnViewActive` / `btnViewHistorical`, both `class=\"hidden\"` — click the\nanchors, not the inputs).\n\nThe Archived tab does **not** inherit the home screen's search. Reaching an\narchived document is a three-step sequence, and skipping any of it silently\nreads the wrong result set:\n\n1. click `#histInv`;\n2. set `select[id$='ddlSearchBy']` to the column you are searching — normally\n   `DealerPONum` (PO No.), or `ASWDocNum` when only the SI doc number is known.\n   It defaults to `-- select search column --`, which matches nothing;\n3. fill `input[id$='tbSearch']` and click `input[id$='btnSearchInvoices']`.\n\nThen the checkbox and download flow is identical to Active.\n\n`fetch-invoice-doc` runs this fallback **automatically** when the Active tab has\nno matching row — callers do not pass a flag, because they usually cannot know\nwhich tab a document is on. The response reports `portal_tab`, and an Archived\nhit also carries `already_historical_warning`: under this skill's exactly-once\nrule a document becomes historical only after a bill was created, so finding one\nthere means it may already have been paid.\n\n`hdnInvoiceView` flips to the historical view and is asserted after the tab\nclick, so a switch that silently did not happen fails loudly rather than\nsearching the Active tab and reporting \"not found\".\n\nEvery one of those buttons is a real form submit, so each click is a\n*navigation*. The driver waits for it — reading rows in the gap between click\nand reload returns the **previous** search, which looks like a correct result\nfor the wrong query.\n\n### In-grid search\n\nBeyond the home-screen search there is a column-scoped search on the results\npage: `select[id$='ddlSearchBy']` + `input[id$='tbSearch']` +\n`input[id$='btnSearchInvoices']`. The dropdown's values answer the open question\nabout what is searchable:\n\n`SupplierName` · `SupplierDocNum` · **`ASWDocNum` (SI Doc No.)** · `ASWDocDate`\n· `HistoricalDate` · `DueDate` · `DiscountDate` · **`DealerPONum` (PO No.)** ·\n`DocTotal`\n\nNot currently used — the driver searches by PO from the home screen and matches\nrows client-side — but it is the way to narrow a PO with many invoices.\n\n### Paging ✅ handled\n\nThe portal reports its own position: `hdnPageIndex` (0-based) and `hdnMaxPage`\n(a count). Read those rather than inferring from the links — the whole pager is\n`display: none` when there is one page. Navigation is\n`lbFirstPage` / `lbPrevPage` / `lbNextPage` / `lbLastPage`, all `__doPostBack`;\nthe numbered links only ever show a five-page window, so Next/Prev is what the\ndriver walks, verifying `hdnPageIndex` actually moved after every hop.\n\nTwo rules, because page 1 alone cannot tell *\"this document is not here\"* from\n*\"this document is on page 3\"*:\n\n- **Looking for one document?** Narrow rather than page. `search_in_grid` by\n  `ASWDocNum` collapses the result to that single row on page 1 in one postback,\n  regardless of how many pages the PO spans.\n- **Fetching a whole PO?** Read every page, even when page 1 already matched —\n  otherwise the download silently covers a subset of the PO.\n\n**Selection does not survive paging.** The pager is a postback that re-renders\nthe grid, so ticks are lost. Rows spanning pages are therefore downloaded a page\nat a time and the parts merged with `pypdf`, which keeps the rest of the pipeline\noblivious: `prepare()` segments the combined file exactly as it does a\nsingle-page download. `download_parts` in the fetch metadata says how many\ndownloads went into it.\n\nOther hidden state worth reading: `hdnInvoiceView` (`Active`/`Historical`),\n`hdnIsSearch`.\n\n## Step 4 — Download ✅ confirmed live — **two clicks, not one**\n\n1. `a[id$='lbDisplayDownload']` (\"Downloads\") — a `__doPostBack` that opens an\n   in-page jQuery-UI dialog, `#downloadOptions`.\n2. A format link inside that dialog.\n\n| Format | Control | Note |\n|---|---|---|\n| **PDF File** | `a[id$='lbPDFPrint']` | \"all selected documents to a **single** PDF\" — what this skill uses |\n| PDF Zip File | `a[id$='btnDownloadPDF']` | one PDF per document, zipped |\n| CSV with Header Detail | `a[id$='btnDownloadCSV']` | header only |\n| CSV with Header and Item Detail | `a[id$='btnDownloadWithItemDetails']` | ✗ empty for these documents — see below |\n| PDF and CSV | `a[id$='btnDownloadBoth']` | zipped |\n\n### ✅ Settled: the CSV export is not a shortcut\n\n`CSV with Header and Item Detail` promises \"item details **if they exist**\", which\nlooked like it might beat reading a scan outright — structured data, no\nextraction, no reconciliation risk. It is **not** a way out: for a header-only\ndocument it comes back with no item detail, because it is fed by the same EDI\nline data the SportsLink API exposes. If the CSV had the lines, the API would\nhave had them, and this fallback would not exist.\n\nSo the PDF path is the primary and only route for scanned documents. The other\nformats stay mapped in `DOWNLOAD_FORMATS` because they are a faithful catalog of\nthe dialog and `capture-portal` can reach them, not because any of them help\nhere.\n\n### ✅ Confirmed: a browser download, from a session-bound URL\n\nThe file arrives as a real download (`mechanism: \"download\"`), from:\n\n```\nhttps://swv2h.sportsinc.com/Member/InvoiceCenter/InvoicePDF.aspx?v=SupplierName&x=ASC&t=1786041790403\n```\n\nThose parameters are the grid's **sort column**, **sort direction**, and a\ncache-buster. There is **no document identifier**. Which documents the PDF\ncontains is held in server-side session state from the ticked checkboxes, so:\n\n- `context.request.get(url)` is not an option — that URL means nothing without\n  the session's selection, and re-fetching it later would return whatever is\n  ticked then.\n- There is no way to skip the browser. Driving the page *is* the API.\n\nThe driver reads the download's bytes and unlinks the temp file. It still\nwatches for an inline PDF response as a fallback, in case the portal changes.\n\nThe dialog does appear with only one row ticked, and a single-row selection\nreturns just that document (121,619 bytes for SI 24682750, against 248,559 for\nthe two-document bundle).\n\n- [ ] Whether `View` differs from `Downloads` (inline viewer vs. file). Not\n      needed — noted only for completeness.\n\n## Step 5 — Bot mitigation / headless\n\nNo WAF interstitial, CAPTCHA, or device check appeared on a headed run from a\nlocal Windows machine. What remains untested is the environment this will\nactually run in.\n\n### What headless changes, and what is done about it\n\nHeadless differs from headed in three ways that break portals, none of them\nabout rendering:\n\n1. **The user agent** says `HeadlessChrome`. Overridden with a desktop Chrome UA.\n2. **`navigator.webdriver` is true.** Hidden by\n   `--disable-blink-features=AutomationControlled`, with an init script masking\n   the property as belt-and-braces for builds where the flag alone does not.\n3. **The default viewport is 1280×720** — small enough for a responsive layout\n   to collapse its desktop navigation. The home screen's search box is a Vue\n   component, and a collapsed layout could hide it outright, so the viewport is\n   pinned to 1440×900 rather than left to the default. This is the most likely\n   headless-specific failure and the least obvious.\n\n### Failure artefacts\n\nA headless run has no window to look at, so any failure now writes\n`failure-<label>.png` and `failure-<label>.html` to the scratch directory and\nnames them in the error, alongside the step trace. The first failing run is the\ndiagnostic one — there is no need to reproduce it to find out what happened.\n\n- [x] **Headless works.** A cold run (session file deleted, so a full Auth0\n      login) completed the whole chain in 20.7s and downloaded the same\n      121,619-byte PDF: `\"headless\": true`, `logged_in: true`, both rows parsed,\n      `mechanism: \"download\"`.\n\n      Worth recording precisely: that run was on the code from *before* the\n      hardening above — no pinned viewport, no webdriver mask, no stealth flag —\n      and it still worked at the default 1280×720. So the portal applies no\n      headless-specific blocking, and the search box survives a narrow viewport.\n      The hardening is insurance for the datacenter case, not a fix for\n      something observed.\n- [ ] Any WAF or device check on login from a **datacenter IP**? A local run is\n      not evidence here — Auth0 treats a cloud runner as a new device. This is\n      the last real unknown in the flow.\n- [ ] Does the deploy host's egress reach `swv2h.sportsinc.com`,\n      `swv3.sportsinc.com`, and `sportsweb.us.auth0.com`? This dev container\n      reaches none of them.\n\n### Timing\n\nA full cold run took 17.9s headed, 20.7s headless — the difference is noise, and\nthe public Wix site is 9–12s of either. Everything after login is under 4s. With\n`SPORTSINC_WEB_STATE_PATH` set, a warm run should skip the first ~15s entirely;\nthe trace will start at `session-restored` rather than `public-site-loaded`.\n\nNote that a run *writes* the session file on the way out, so testing a cold\nlogin twice in a row means deleting it in between — otherwise the second run\nsilently skips the step being tested.\n\n---\n\n## Closing the gaps: run the capture\n\n```bash\nexport SPORTSINC_WEB_USERNAME=... SPORTSINC_WEB_PASSWORD=...\n\n# Log in, search a PO, report what is there. Read-only.\npython3 scripts/sportslink.py capture-portal '{\"search\": \"P13554\"}'\n\n# Same, plus tick the first row and walk the download dialog.\npython3 scripts/sportslink.py capture-portal '{\"search\": \"P13554\", \"probe_download\": true}'\n```\n\n`download_format` reaches the dialog's other options (`pdf_zip`, `csv`,\n`csv_items`, `pdf_and_csv`) if one is ever needed; `pdf` is the default and the\nonly one this skill uses.\n\nIt reports the landing URL, which elements it located, the portal's own hidden\nstate fields, the parsed rows, and — with `probe_download` — the download\nmechanism, byte count, whether the bytes start with `%PDF-`, and where the file\nwas saved. It also writes a full-page screenshot and the page HTML.\n\nAdd `{\"headless\": false}` to watch it run.\n\n## What the live runs proved\n\nA `capture-portal` run (headed, Windows, `'{\"search\": \"P13554\"}'`) returned:\n\n```json\n\"landing_url\": \"https://swv3.sportsinc.com/home\",\n\"logged_in\": true,\n\"invoice_center_url\": \"https://swv2h.sportsinc.com/Member/InvoiceCenter/Default.aspx?search=P13554\",\n\"page\": {\n  \"title\": \"Dealer Invoice Center\",\n  \"results_grid_found\": true,\n  \"headers\": {\"Supplier\":1,\"Supplier Doc No.\":2,\"PO No.\":3,\"SI Doc No.\":4,\n              \"SI Doc Date\":5,\"Due Date\":6,\"Archived Date\":7,\"Discount Date\":8,\"Total\":9},\n  \"elements\": {\"grid\":1,\"row_checkboxes\":2,\"downloads_link\":1,\n               \"download_dialog\":1,\"active_tab\":1,\"archived_tab\":1},\n  \"state\": {\"hdnInvoiceView\":\"Active\",\"hdnMaxPage\":\"1\"},\n  \"row_count\": 2\n},\n\"rows\": [\n  {\"row_index\":0,\"Supplier\":\"CHAMPRO SPORTS\",\"Supplier Doc No.\":\"SI3503366\",\n   \"PO No.\":\"P13554\",\"SI Doc No.\":\"24682750\",\"Total\":\"$5.23\"},\n  {\"row_index\":1,\"Supplier\":\"CHAMPRO SPORTS\",\"Supplier Doc No.\":\"SI3503509\",\n   \"PO No.\":\"P13554\",\"SI Doc No.\":\"24684277\",\"Total\":\"$401.59\"}\n]\n```\n\nA second run with `probe_download` added:\n\n```json\n\"download\": {\"ok\": true, \"format\": \"pdf\", \"mechanism\": \"download\",\n             \"url\": \".../InvoicePDF.aspx?v=SupplierName&x=ASC&t=1786041790403\",\n             \"bytes\": 121619, \"looks_like_pdf\": true},\n\"trace\": [{\"step\":\"public-site-loaded\",\"ms\":9025},\n          {\"step\":\"login-form-visible\",\"ms\":11138},\n          {\"step\":\"credentials-submitted\",\"ms\":12949},\n          {\"step\":\"logged-in\",\"ms\":14580,\"url\":\"https://swv3.sportsinc.com/home\"},\n          {\"step\":\"search-submitted\",\"ms\":15446},\n          {\"step\":\"grid-loaded\",\"ms\":16048},\n          {\"step\":\"rows-selected\",\"ms\":17407,\"count\":1},\n          {\"step\":\"downloads-clicked\",\"ms\":17515},\n          {\"step\":\"download-dialog-open\",\"ms\":17539},\n          {\"step\":\"download-event\",\"ms\":17859}]\n```\n\nSettled by that:\n\n- Auth0 login works unattended; no MFA from a local Windows run.\n- The session carries across `swv3` → `swv2h`.\n- **The frozen-header defence works.** `\"PO No.\": \"P13554\"` — the tree walker\n  stripped the cloned headings, and the leak guard did not fire.\n- Column indices are right (0 is the unnamed checkbox column).\n- `row_checkboxes: 2` — `chkItem` targeting finds the real per-row boxes, not\n  the cloned select-all.\n- Both tabs, the Downloads link, and the dialog element are all present.\n- The two-click download works: the dialog opens on one ticked row, and\n  \"PDF File\" returns that one document as a browser download.\n- No MFA, WAF interstitial, or device check on a local headed run.\n\nA third run exercised the real entry point — `fetch-invoice-doc` with a specific\n`si_doc_number`, which matches one row rather than taking the first:\n\n```json\n\"pdf_source\": \"sportsweb\", \"page_count\": 2, \"pdf_bytes\": 127435,\n\"documents\": [{\"si_cover_page\": 1, \"detail_pages\": [2], \"detail_is_scanned\": true,\n               \"si_doc_number_candidates\": [24684277],\n               \"supplier_doc_candidates\": [\"SI3503509\"], \"matches_requested\": true}],\n\"text_covers_all_pages\": false, \"image_pages\": [2],\n\"image_paths\": [\"…/si-24684277-p2.png\"]\n```\n\nTwo pages, not the four of the PO-level bundle — so row matching selected the\nrequested document. The cover page's text layer carries 398.40 / 3.19 / 401.59\nand the sentinel \"SEE VENDOR INVOICE FOR DETAIL.\", the scan was extracted to a\nPNG, and the whole chain from API lookup to readable image ran unattended.\n\nNote `\"home_search_input\": 0` in that output: `describe()` ran on the results\npage (swv2h), where the home search box does not exist. Expected, not a fault.\n\n## What is still open\n\nThe portal flow is proven; these are the gaps around it.\n\n- **Headless, and from a datacenter IP.** Every run so far has been headed on a\n  local Windows machine. Auth0 treats a cloud runner as a new device.\n- ~~Multi-page results~~ — handled (see Paging above), but not yet exercised\n  against a PO that actually spans pages.\n- **The Archived tab fallback.** Now implements the full three-step sequence\n  (tab → column → search) rather than just clicking the tab, but is still\n  unexercised: no test document has been marked historical. Test it with\n  `capture-portal '{\"search\": \"<PO>\", \"archived\": true}'`.\n- **A wrong-document guard.** After fetching, the SI cover page's text layer\n  carries the SI document number and PO — cross-checking them against what was\n  requested would close the one hole reconciliation cannot.\n- `python3 scripts/_selftest.py` green (33 tests) — the offline half.\n\nFile v0.7.1:skill-card.md\n\n## Description:\n\nSports Inc SportsLink API adapter pulls dealer invoice documents from the SportsWeb Invoice Center, normalizes them for payables workflows, recovers scanned invoice line detail through PDF/OCR review, and marks documents consumed after import.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[zmtucker](https://clawhub.ai/user/zmtucker)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nAccounts payable agents and operators use this skill to retrieve Sports Inc dealer invoices, identify documents that need line recovery, reconcile scanned invoice lines, and pass verified invoices into a payable-matching workflow.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill requires live SportsLink API keys and may use SportsWeb portal credentials.\n\nMitigation: Install only for authorized Sports Inc dealer accounts, store secrets in the platform secret broker or protected environment variables, and never pass credentials in chat or command JSON.\n\nRisk: The skill handles financial records and can mark invoice documents historical.\n\nMitigation: Use dry-run or manual review until the billing workflow is confirmed, and mark documents historical only after the downstream bill has been created successfully.\n\nRisk: Endpoint overrides, persisted browser sessions, downloaded PDFs, and diagnostic captures can expose invoice data if stored loosely.\n\nMitigation: Keep SPORTSINC_API_URL at the default unless using a verified test endpoint, place SPORTSINC_WEB_STATE_PATH and SPORTSINC_PDF_DIR in private per-user directories, and clean diagnostic captures after use.\n\n## Reference(s):\n\n- [SportsLink API Reference](references/sportslink_api.md)\n- [PDF Extraction Guide](references/pdf_extraction.md)\n- [SportsWeb Portal Flow Notes](references/sportsweb_flow_notes.md)\n- [Sports Inc Homepage](https://www.sportsinc.com)\n- [SportsLink API Base URL](https://api.sportsinc.com/)\n- [SportsWeb Home](https://swv3.sportsinc.com/home)\n- [SportsWeb Invoice Center](https://swv2h.sportsinc.com/)\n- [ClawHub Skill Page](https://clawhub.ai/zmtucker/skills/sportsinc-sportslink)\n\n## Skill Output:\n\n**Output Type(s):** [JSON, Markdown, Shell commands, Guidance]\n\n**Output Format:** [JSON objects from helper commands and compact Markdown summaries for delegated tasks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Outputs normalized invoice records, scanned-document recovery status, reconciliation results, and operational next steps.]\n\n## Skill Version(s):\n\n0.7.1 (source: frontmatter and server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.4.0: 5 files, 15193 bytes\n\nFiles: references/sportslink_api.md (4044b), scripts/sportslink.py (16141b), skill-card.md (2701b), SKILL.md (12853b), _meta.json (139b)\n\nFile v0.4.0:SKILL.md\n\n---\nname: sportsinc-sportslink\ndescription: >\n  Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\")\n  from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports\n  Inc is a buying group that does NOT send individual vendor invoices; its\n  SportsLink REST API is where the invoices live. Use when you need to retrieve\n  Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull\n  SportsLink documents\", \"fetch this month's SI invoices to match against POs\".\n  This is the SOURCE adapter only: it authenticates, pages, normalises each SI\n  document into a common invoice shape (po_number, invoice_number/date,\n  lines[], charges, total, is_credit), and marks documents historical once\n  imported. It is customer-agnostic (every Sports Inc dealer uses this same\n  API) and touches no ERP — pair it with a payables workflow (e.g.\n  `drivethru-payable-matching`) to match against POs and create the bill.\nversion: 0.4.0\nemoji: 🏟️\nhomepage: https://www.sportsinc.com\nmetadata:\n  openclaw:\n    requires:\n      # SPORTSINC_API_KEY is deliberately NOT listed here. openclaw gates a\n      # skill OUT of the model's view when a `requires.env` key is absent from\n      # the *boot* environment — but in A2A mode (see \"Agent-to-Agent (A2A)\n      # Mode\" below) this key is brokered per-turn by the platform and is\n      # absent at boot by design, so gating on it would hide this skill from\n      # the very delegated flow it exists to serve. The key stays fully\n      # documented via `primaryEnv`/`envVars`, and the helper self-guards at\n      # runtime (`config_error`/`auth_error`) when it is genuinely missing.\n      # `python3` stays gated because it must be present at boot.\n      bins: [python3]\n    primaryEnv: SPORTSINC_API_KEY\n    envVars:\n      SPORTSINC_API_KEY:\n        required: true\n        description: >\n          SportsLink API key, sent as the `X-API-KEY` header. Request one from\n          mhoerner@hq.sportsinc.com. Treat as a secret; never paste into chat.\n      SPORTSINC_API_URL:\n        required: false\n        description: Base URL, default `https://api.sportsinc.com/`.\n      SPORTSINC_DRY_RUN:\n        required: false\n        description: If truthy, `mark-historical` is simulated (no state change).\n    install:\n      uv:\n        - requests>=2.28\n---\n\n# Sports Inc SportsLink adapter\n\nSports Inc is a buying group: BaconCo (and every other SI dealer) buys through\nSports Inc, and Sports Inc does **not** email individual vendor invoices —\nthey're published in the SportsWeb Invoice Center and exposed through the\n**SportsLink REST API**. This skill is the source adapter for that API. It does\none job: hand a payables workflow a clean, normalised list of invoices, and\nmark them consumed once they've been imported. It never touches Odoo.\n\nThe single helper is `scripts/sportslink.py`:\n\n```bash\n# The un-imported inbox: active documents that carry line items (EDI), normalised\npython3 scripts/sportslink.py list '{\"active\": true, \"lines\": true, \"ediOnly\": true}'\n\n# A specific document (ignores the active-only filter)\npython3 scripts/sportslink.py get '{\"poNumber\": \"P13189\"}'\n\n# Mark documents consumed — AFTER they've been billed (honors SPORTSINC_DRY_RUN)\npython3 scripts/sportslink.py mark-historical '{\"siDocNumbers\": [12345, 23456]}'\n\n# A2A-safe action for agent-to-agent calls (structured request/response contract)\npython3 scripts/sportslink.py get-for-a2a '{\"customer_ref\": \"DEALER-001\", \"date_range\": {\"start\": \"2024-01-01\", \"end\": \"2024-12-31\"}, \"statuses\": [\"open\"]}'\n```\n\nEvery command prints one JSON object, or `{\"error\": {...}}` with a non-zero\nexit. Needs `SPORTSINC_API_KEY` (if unset, exits `config_error` — stop and tell\nthe user to configure it; never ask for the key in chat).\n\n## Normalised invoice shape\n\n`list`/`get` return `{count, total_count, invoices: [...]}`, each invoice:\n\n```json\n{\n  \"source\": \"sports_inc\",\n  \"po_number\": \"P13189\",          // dealer PO number → the match key to a PO\n  \"si_doc_number\": 12345,          // SI's document id → used to mark-historical\n  \"invoice_number\": \"…\",           // supplierDocNumber (falls back to si_doc_number)\n  \"invoice_date\": \"…\",             // supplierDocDate, else siDocDate\n  \"due_date\": \"…\", \"supplier\": \"…\",\n  \"is_credit\": false,               // credit memo → handle separately, never a bill\n  \"has_lines\": true,                // false for scanned/OCR docs (header totals only)\n  \"total\": 0,                       // docTotal\n  \"charges\": {\"merchandise\", \"freight\", \"freight_allowance\", \"si_upcharge\",\n              \"svc_handle\", \"sales_tax\", \"excise_tax\", \"discount\"},\n  \"lines\": [{\"item\",\"upc\",\"description\",\"size\",\"color\",\"unit\",\n             \"qty_ordered\",\"qty_shipped\",\"qty_backordered\",\n             \"list_price\",\"discount_pct\",\"net_price\",\"extension\"}]\n}\n```\n\nThis is the **same shape a PDF-extracted invoice would have**, so a payables\nworkflow reconciles it without caring that it came from SportsLink.\n\n## Rules that matter\n\n- **Don't pull before ~10:30am ET** — SI's internal processing runs first; earlier\n  reads can be incomplete. Schedule accordingly.\n- **Line data is EDI-only.** Scanned/OCR documents come back with header totals\n  but no `lines` (`has_lines: false`). Pass `ediOnly: true` to fetch only\n  documents with line items; a header-only doc can't be line-verified and should\n  be escalated by the workflow, not blind-billed.\n- **`is_credit: true` is a credit memo** — route it to a human / vendor credit,\n  never create it as a payable.\n- **Exactly-once — the golden rule.** Import first, `mark-historical` **after**\n  the bill is created. This adapter deliberately does **not** use the API's\n  `moveToHistorical=true` GET flag (which marks on read, before billing) — a\n  crash between read and bill would silently drop the invoice. The natural loop:\n  `list active` → bill each in the ERP → `mark-historical` the ones that\n  succeeded; failures/escalations stay active and are retried next run.\n- **Paging** is automatic (`all: true`, the default). Max 1000 docs/call on SI's\n  side; the helper pages to the end (capped at 50 pages as a backstop).\n\n## Where this fits\n\nSource adapter (this) → payables **workflow** (`drivethru-payable-matching`) →\nERP **adapter** (`drivethru-odoo` / `drivethru_mcp`). This skill owns only the\n\"get the invoices + mark them consumed\" half; matching to POs, correcting\npricing, and creating the draft bill live in the workflow. See that skill's\n`references/sportsinc_payables.md` for the end-to-end procedure.\n\n## Agent-to-Agent (A2A) Mode\n\nThe `get-for-a2a` action provides a **contract-driven interface** for inter-agent\ncommunication. Deploy this skill on a dedicated Sports Inc agent and let the\ninternal agent that needs invoices (e.g. an Accounts Payable agent) reach it via\na **delegation connection** in the Knoxville platform.\n\n### Where `SPORTSINC_API_KEY` comes from (credential broker)\n\nThe `SPORTSINC_API_KEY` is bound to the **calling** agent (the one that\nrepresents your company — e.g. Accounts Payable), not to this Sports Inc agent.\nOn that agent's delegation connection to this one, the operator chooses to\n**share** `SPORTSINC_API_KEY` with the connection.\n\nThe value is **pulled on demand**, not pushed. When this agent handles a\ndelegated call (`X-Knox-Caller-Kind: agent`), **the runtime** (not you) fetches\nthe shared `SPORTSINC_API_KEY` for this conversation and places it into the\nskill's **execution environment for this turn only**, before your `exec` runs.\nThe platform verifies this agent is the target of the delegated conversation and\nthat the connection shares the credential, and **logs the access in\n`agent_connection_audit_log`**. `sportslink.py` then reads `SPORTSINC_API_KEY`\nfrom the environment exactly as it does standalone. You do **not** see this value\nin your context — it is deliberately kept out of the model prompt.\n\n**So on a delegated turn, just run the tool.** Your first action for a Sports\nInc request is the `exec` call itself — e.g.\n`python3 scripts/sportslink.py get-for-a2a '{...}'`. Do **not**, before running\nit:\n\n- call `get_my_bundle`, `get_delegated_credentials`, or any tool to look for or\n  \"verify\" the key — it is intentionally invisible to you, so you will always\n  find nothing and wrongly conclude you have no access;\n- spawn a sub-agent (`sessions_spawn`) to do this skill's job — you are the agent\n  that runs it;\n- tell the caller you lack the key or access **before** you have actually run the\n  script and read its output.\n\nIf the script itself reports an `auth_error` (or `config_error`), the caller's\nconnection hasn't shared the credential — surface that error rather than\nguessing, and never print the credential value into the chat reply.\n\n### Request Contract\n\n`get-for-a2a` params (all optional):\n\n```json\n{\n  \"customer_ref\": \"DEALER-001\",\n  \"date_range\": { \"start\": \"2024-01-01\", \"end\": \"2024-12-31\" },\n  \"include_historical\": false\n}\n```\n\n- `include_historical` (default `false`) → active/un-imported invoices only;\n  set `true` to also include historical/consumed docs.\n- `customer_ref` is **advisory only** — the SportsLink API key is per-dealer and\n  the API has no customer filter, so this field does **not** scope the result. It\n  is echoed back in `metadata.customer_ref` for the caller's audit.\n\n### Response Contract\n\nUnlike the other actions (which exit non-zero on error), `get-for-a2a` **always\nexits 0** and reports failure in-band, so an A2A caller reads one envelope shape\neither way.\n\nSuccess:\n\n```json\n{\n  \"success\": true,\n  \"invoices\": [ { \"source\": \"sports_inc\", \"po_number\": \"P13189\", \"si_doc_number\": 12345, \"total\": 1500.00, \"lines\": [] } ],\n  \"metadata\": { \"count\": 42, \"total_count\": 50, \"pages_read\": 1, \"source\": \"sports_inc\", \"customer_ref\": \"DEALER-001\", \"include_historical\": false },\n  \"error\": null\n}\n```\n\nError:\n\n```json\n{\n  \"success\": false,\n  \"invoices\": null,\n  \"metadata\": null,\n  \"error\": {\n    \"type\": \"auth_error|connection_error|api_error|validation_error\",\n    \"message\": \"Human-readable error message\",\n    \"retriable\": true\n  }\n}\n```\n\n### Replying to a delegated *task* — compact markdown, not raw JSON\n\nThe JSON envelope above is the contract for a **synchronous** `send_message`\ncall, where the caller reads the object programmatically. But when the payables\nagent reaches you as an **async delegated task** (it `start_task`s you a request\nand you report back with an outcome/summary), your reply is **free text another\nagent reads**, and that summary field has a hard **~20,000-character limit**. A\nraw JSON dump of several POs' invoices overflows it and is **silently\ntruncated** — which hands the payables agent a half-parsed payload and corrupts\nits billing. (This is exactly what happened once: five POs of pretty-printed\nJSON, cut off mid-object.)\n\nSo on a delegated task, **run `get-for-a2a` / `list` as usual, then hand back a\ncompact markdown breakdown** in your outcome summary — never paste the raw\nJSON. Markdown carries all the same data in a fraction of the characters and the\npayables agent reads it directly (it does not need strict JSON). Include every\nfield it needs, terse, and **do not wrap it in a code fence** (fences add bulk\nand confuse parsing):\n\n- One `## <po_number>` heading per PO.\n- One bullet per SI document: `si_doc_number`, `invoice_number`,\n  `invoice_date`, `due_date`, `is_credit`, `has_lines`, and the money from\n  `charges` + `total` (`merchandise`, `freight`, `si_upcharge`, `total`).\n- Under each document with `has_lines: true`, one terse line per item:\n  `item`, `upc`, `size`, `qty_shipped`, `net_price`, `extension`, `description`.\n  For a `has_lines: false` document write `detail no` and omit item lines.\n\nExample — keep it this tight (normalised field values, no code fence around it\nin your real reply):\n\n```text\n## P09409\n- SI 23962348 | inv# 6164920830 | 2026-02-16 | due 2026-05-10 | credit no | detail yes | merch 51.00 freight 8.58 upcharge 0.48 total 60.06\n  - JP1477 | 197612326076 | S | qtyShip 1 | net 12.75 | ext 12.75 | TF SHRT TIGHT M BLACK\n  - JP1477 | 197612326083 | M | qtyShip 3 | net 12.75 | ext 38.25 | TF SHRT TIGHT M BLACK\n- SI 23972779 | inv# 6164929812 | 2026-02-17 | due 2026-05-10 | credit no | detail yes | merch 180.61 freight 0.00 upcharge 1.45 total 182.06\n  - JJ1179 | 196476717082 | L | qtyShip 1 | net 25.50 | ext 25.50 | GG SL HD MGREYH\n```\n\nIf even the compact form would be too large (many POs, many lines), **never cut\nit silently**: return the POs you can and end with an explicit\n`NOTE: truncated — returned N of M POs, ask again for the rest`, so the caller\nknows to re-request rather than bill from a partial payload.\n\nThe `retriable` flag indicates whether the caller should retry (transient\nconnection errors) or escalate (auth/config errors).\n\nFile v0.4.0:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"sportsinc-sportslink\",\n  \"version\": \"0.4.0\",\n  \"publishedAt\": 1785768878964\n}\n\nFile v0.4.0:references/sportslink_api.md\n\n# SportsLink API — reference (distilled from the 2024 Dealers spec)\n\nThe `scripts/sportslink.py` helper wraps all of this; read here when you need a\nparameter, a field, or the semantics behind one.\n\n## Basics\n\n- **Base URL:** `https://api.sportsinc.com/`\n- **Auth:** API key in the `X-API-KEY` request header. Request one from\n  `mhoerner@hq.sportsinc.com`.\n- **Limits:** max **1000 documents per call** — page. Don't retrieve before\n  **~10:30am ET** (SI internal processing completes first).\n\n## GET `/dealers/documents/` — the invoices\n\nReturns JSON documents from the SportsWeb Invoice Center.\n\n### Query parameters (all optional)\n\n| Param | Type | Notes |\n|---|---|---|\n| `poNumber` | string | **Dealer PO number** — the match key to your ERP PO |\n| `supplierDocNumber` | string | The underlying supplier's document number |\n| `siDocNumber` | int | Sports Inc's document id (used to mark historical) |\n| `siDocDate` / `siDocStartDate` / `siDocEndDate` | date `yyyy-MM-dd` | SI processing date. A single date or `start,end`. Start-only = on/after; end-only = on/before |\n| `supplierDocDate` / `supplierDocStartDate` / `supplierDocEndDate` | date | Supplier document date, same range semantics |\n| `lines` | bool (default false) | Include line-item data — **EDI documents only** |\n| `active` | bool (default false) | Only documents **not** marked historical (the un-imported inbox) |\n| `moveToHistorical` | bool (default false) | Mark the returned docs historical **on read** — ⚠️ do NOT use for billing (marks before you've billed); use the PATCH endpoint after billing instead |\n| `excludeScannedDocuments` | bool (default false) | Only documents with line-item data (EDI); scanned/OCR docs have none |\n| `fields` | string[] | Sparse fieldset — return only these properties |\n| `page` / `pageSize` | int | Paging (`pageSize` default = total doc count) |\n| `orderBy` (default `SIDocDate`) / `orderByDescending` | string / bool | Sorting |\n\nExample: `GET /dealers/documents/?siDocDate=2024-01-31&lines=true&active=true`\n\n### Response envelope\n\n`{ items: [ …document… ], pageNumber, pageSize, totalPages, totalCount,\nhasPreviousPage, hasNextPage, orderBy, orderByDescending }`\n\n### Document (header) fields\n\n`poNumber`, `siDocNumber`, `siDocDate`, `dueDate`, `discountDate`,\n`requestedShipDate`, `shipDate`, `supplierDocNumber`, `supplierDocDate`,\n`supplier`, `isCredit`, plus money: `merchandiseTotal`, `freightAmount`,\n`freightAllowance`, `discountAmount`, `siUpcharge`, `svcHandleCharge`,\n`salesTax`, `exciseTax`, `docTotal`. Also `termsOfPayment`, `termsOfDelivery`,\n`carrier`, `weight`, `trackingNumber`, `methodOfPayment`, `supplierAddress{}`,\n`shippingAddress{}`. Missing/OCR-unavailable properties are simply omitted.\n\n### Line fields (EDI only)\n\n`supplierItemNumber`, `upc`, `quantityShipped`, `quantityOrdered`,\n`quantityBackOrdered`, `unit`, `listPrice`, `discountPercent`, `netPrice`,\n`extension`, `size`, `color`, `description`.\n\n`docTotal` = `merchandiseTotal` + `siUpcharge` + `svcHandleCharge` +\n`freightAmount` + `salesTax` + `exciseTax` − `discountAmount` −\n`freightAllowance` (SI-specific charges included — this is what you'll be\nbilled, and what the ERP bill's `expected_total` should tie to).\n\n## PATCH `/dealers/documents/status` — mark consumed\n\nBody: `{ \"siDocNumbers\": [12345, 23456], \"isActive\": false }`\n- `isActive: false` → historical (moves to the Invoice Center \"Historical\" tab).\n- `isActive: true` → back to active.\n\nResponses: `204 No Content` (success), `400` (bad body), `401` (bad key).\n\n**Use this to mark documents consumed AFTER a bill is created** — the exactly-once\nseam. Prefer it over `moveToHistorical=true` on the GET, which marks on read.\n\n## Failure modes to expect\n\n- `401` — bad/expired key (the helper surfaces `auth_error`).\n- Transient `429`/`5xx`/timeouts — the helper retries with backoff.\n- Missing fields — OCR gaps or supplier-not-provided; simply absent, not null.\n- Scanned docs — no `lines`; `has_lines: false` in the normalised shape.\n\nFile v0.4.0:skill-card.md\n\n## Description: <br>\nSports Inc SportsLink API adapter for retrieving dealer invoice documents from SportsLink, normalizing them into a common invoice shape, and marking imported documents consumed. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[zmtucker](https://clawhub.ai/user/zmtucker) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nAccounts payable agents and operators use this skill to retrieve Sports Inc invoices, normalize document headers and line items for PO matching, and mark only successfully imported documents as consumed. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can access API-key-scoped Sports Inc invoice data. <br>\nMitigation: Install only for agents that should access Sports Inc invoices and share SPORTSINC_API_KEY only with intended delegation connections. <br>\nRisk: customer_ref is advisory and does not limit which invoices the SportsLink API returns. <br>\nMitigation: Treat the SportsLink API key as the effective data scope and rely on downstream payables workflows for customer-specific matching. <br>\nRisk: mark-historical changes document status in SportsLink. <br>\nMitigation: Use dry-run or human review when appropriate and mark documents historical only after the payable has been created successfully. <br>\nRisk: Scanned or OCR documents may lack line-item detail. <br>\nMitigation: Prefer EDI documents for line verification and escalate header-only documents instead of billing them blindly. <br>\n\n\n## Reference(s): <br>\n- [SportsLink API Reference](references/sportslink_api.md) <br>\n- [Sports Inc](https://www.sportsinc.com) <br>\n- [SportsLink API](https://api.sportsinc.com/) <br>\n- [ClawHub Skill Page](https://clawhub.ai/zmtucker/skills/sportsinc-sportslink) <br>\n- [Publisher Profile](https://clawhub.ai/user/zmtucker) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance] <br>\n**Output Format:** [JSON responses for helper actions and compact Markdown summaries for delegated tasks] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Requires SPORTSINC_API_KEY; list and get actions read invoice documents, while mark-historical changes document status after successful import.] <br>\n\n## Skill Version(s): <br>\n0.4.0 (source: frontmatter and release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.2: 5 files, 13936 bytes\n\nFiles: references/sportslink_api.md (4044b), scripts/sportslink.py (16141b), skill-card.md (2460b), SKILL.md (10269b), _meta.json (139b)\n\nFile v0.3.2:SKILL.md\n\n---\nname: sportsinc-sportslink\ndescription: >\n  Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\")\n  from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports\n  Inc is a buying group that does NOT send individual vendor invoices; its\n  SportsLink REST API is where the invoices live. Use when you need to retrieve\n  Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull\n  SportsLink documents\", \"fetch this month's SI invoices to match against POs\".\n  This is the SOURCE adapter only: it authenticates, pages, normalises each SI\n  document into a common invoice shape (po_number, invoice_number/date,\n  lines[], charges, total, is_credit), and marks documents historical once\n  imported. It is customer-agnostic (every Sports Inc dealer uses this same\n  API) and touches no ERP — pair it with a payables workflow (e.g.\n  `drivethru-payable-matching`) to match against POs and create the bill.\nversion: 0.3.2\nemoji: 🏟️\nhomepage: https://www.sportsinc.com\nmetadata:\n  openclaw:\n    requires:\n      # SPORTSINC_API_KEY is deliberately NOT listed here. openclaw gates a\n      # skill OUT of the model's view when a `requires.env` key is absent from\n      # the *boot* environment — but in A2A mode (see \"Agent-to-Agent (A2A)\n      # Mode\" below) this key is brokered per-turn by the platform and is\n      # absent at boot by design, so gating on it would hide this skill from\n      # the very delegated flow it exists to serve. The key stays fully\n      # documented via `primaryEnv`/`envVars`, and the helper self-guards at\n      # runtime (`config_error`/`auth_error`) when it is genuinely missing.\n      # `python3` stays gated because it must be present at boot.\n      bins: [python3]\n    primaryEnv: SPORTSINC_API_KEY\n    envVars:\n      SPORTSINC_API_KEY:\n        required: true\n        description: >\n          SportsLink API key, sent as the `X-API-KEY` header. Request one from\n          mhoerner@hq.sportsinc.com. Treat as a secret; never paste into chat.\n      SPORTSINC_API_URL:\n        required: false\n        description: Base URL, default `https://api.sportsinc.com/`.\n      SPORTSINC_DRY_RUN:\n        required: false\n        description: If truthy, `mark-historical` is simulated (no state change).\n    install:\n      uv:\n        - requests>=2.28\n---\n\n# Sports Inc SportsLink adapter\n\nSports Inc is a buying group: BaconCo (and every other SI dealer) buys through\nSports Inc, and Sports Inc does **not** email individual vendor invoices —\nthey're published in the SportsWeb Invoice Center and exposed through the\n**SportsLink REST API**. This skill is the source adapter for that API. It does\none job: hand a payables workflow a clean, normalised list of invoices, and\nmark them consumed once they've been imported. It never touches Odoo.\n\nThe single helper is `scripts/sportslink.py`:\n\n```bash\n# The un-imported inbox: active documents that carry line items (EDI), normalised\npython3 scripts/sportslink.py list '{\"active\": true, \"lines\": true, \"ediOnly\": true}'\n\n# A specific document (ignores the active-only filter)\npython3 scripts/sportslink.py get '{\"poNumber\": \"P13189\"}'\n\n# Mark documents consumed — AFTER they've been billed (honors SPORTSINC_DRY_RUN)\npython3 scripts/sportslink.py mark-historical '{\"siDocNumbers\": [12345, 23456]}'\n\n# A2A-safe action for agent-to-agent calls (structured request/response contract)\npython3 scripts/sportslink.py get-for-a2a '{\"customer_ref\": \"DEALER-001\", \"date_range\": {\"start\": \"2024-01-01\", \"end\": \"2024-12-31\"}, \"statuses\": [\"open\"]}'\n```\n\nEvery command prints one JSON object, or `{\"error\": {...}}` with a non-zero\nexit. Needs `SPORTSINC_API_KEY` (if unset, exits `config_error` — stop and tell\nthe user to configure it; never ask for the key in chat).\n\n## Normalised invoice shape\n\n`list`/`get` return `{count, total_count, invoices: [...]}`, each invoice:\n\n```json\n{\n  \"source\": \"sports_inc\",\n  \"po_number\": \"P13189\",          // dealer PO number → the match key to a PO\n  \"si_doc_number\": 12345,          // SI's document id → used to mark-historical\n  \"invoice_number\": \"…\",           // supplierDocNumber (falls back to si_doc_number)\n  \"invoice_date\": \"…\",             // supplierDocDate, else siDocDate\n  \"due_date\": \"…\", \"supplier\": \"…\",\n  \"is_credit\": false,               // credit memo → handle separately, never a bill\n  \"has_lines\": true,                // false for scanned/OCR docs (header totals only)\n  \"total\": 0,                       // docTotal\n  \"charges\": {\"merchandise\", \"freight\", \"freight_allowance\", \"si_upcharge\",\n              \"svc_handle\", \"sales_tax\", \"excise_tax\", \"discount\"},\n  \"lines\": [{\"item\",\"upc\",\"description\",\"size\",\"color\",\"unit\",\n             \"qty_ordered\",\"qty_shipped\",\"qty_backordered\",\n             \"list_price\",\"discount_pct\",\"net_price\",\"extension\"}]\n}\n```\n\nThis is the **same shape a PDF-extracted invoice would have**, so a payables\nworkflow reconciles it without caring that it came from SportsLink.\n\n## Rules that matter\n\n- **Don't pull before ~10:30am ET** — SI's internal processing runs first; earlier\n  reads can be incomplete. Schedule accordingly.\n- **Line data is EDI-only.** Scanned/OCR documents come back with header totals\n  but no `lines` (`has_lines: false`). Pass `ediOnly: true` to fetch only\n  documents with line items; a header-only doc can't be line-verified and should\n  be escalated by the workflow, not blind-billed.\n- **`is_credit: true` is a credit memo** — route it to a human / vendor credit,\n  never create it as a payable.\n- **Exactly-once — the golden rule.** Import first, `mark-historical` **after**\n  the bill is created. This adapter deliberately does **not** use the API's\n  `moveToHistorical=true` GET flag (which marks on read, before billing) — a\n  crash between read and bill would silently drop the invoice. The natural loop:\n  `list active` → bill each in the ERP → `mark-historical` the ones that\n  succeeded; failures/escalations stay active and are retried next run.\n- **Paging** is automatic (`all: true`, the default). Max 1000 docs/call on SI's\n  side; the helper pages to the end (capped at 50 pages as a backstop).\n\n## Where this fits\n\nSource adapter (this) → payables **workflow** (`drivethru-payable-matching`) →\nERP **adapter** (`drivethru-odoo` / `drivethru_mcp`). This skill owns only the\n\"get the invoices + mark them consumed\" half; matching to POs, correcting\npricing, and creating the draft bill live in the workflow. See that skill's\n`references/sportsinc_payables.md` for the end-to-end procedure.\n\n## Agent-to-Agent (A2A) Mode\n\nThe `get-for-a2a` action provides a **contract-driven interface** for inter-agent\ncommunication. Deploy this skill on a dedicated Sports Inc agent and let the\ninternal agent that needs invoices (e.g. an Accounts Payable agent) reach it via\na **delegation connection** in the Knoxville platform.\n\n### Where `SPORTSINC_API_KEY` comes from (credential broker)\n\nThe `SPORTSINC_API_KEY` is bound to the **calling** agent (the one that\nrepresents your company — e.g. Accounts Payable), not to this Sports Inc agent.\nOn that agent's delegation connection to this one, the operator chooses to\n**share** `SPORTSINC_API_KEY` with the connection.\n\nThe value is **pulled on demand**, not pushed. When this agent handles a\ndelegated call (`X-Knox-Caller-Kind: agent`), **the runtime** (not you) fetches\nthe shared `SPORTSINC_API_KEY` for this conversation and places it into the\nskill's **execution environment for this turn only**, before your `exec` runs.\nThe platform verifies this agent is the target of the delegated conversation and\nthat the connection shares the credential, and **logs the access in\n`agent_connection_audit_log`**. `sportslink.py` then reads `SPORTSINC_API_KEY`\nfrom the environment exactly as it does standalone. You do **not** see this value\nin your context — it is deliberately kept out of the model prompt.\n\n**So on a delegated turn, just run the tool.** Your first action for a Sports\nInc request is the `exec` call itself — e.g.\n`python3 scripts/sportslink.py get-for-a2a '{...}'`. Do **not**, before running\nit:\n\n- call `get_my_bundle`, `get_delegated_credentials`, or any tool to look for or\n  \"verify\" the key — it is intentionally invisible to you, so you will always\n  find nothing and wrongly conclude you have no access;\n- spawn a sub-agent (`sessions_spawn`) to do this skill's job — you are the agent\n  that runs it;\n- tell the caller you lack the key or access **before** you have actually run the\n  script and read its output.\n\nIf the script itself reports an `auth_error` (or `config_error`), the caller's\nconnection hasn't shared the credential — surface that error rather than\nguessing, and never print the credential value into the chat reply.\n\n### Request Contract\n\n`get-for-a2a` params (all optional):\n\n```json\n{\n  \"customer_ref\": \"DEALER-001\",\n  \"date_range\": { \"start\": \"2024-01-01\", \"end\": \"2024-12-31\" },\n  \"include_historical\": false\n}\n```\n\n- `include_historical` (default `false`) → active/un-imported invoices only;\n  set `true` to also include historical/consumed docs.\n- `customer_ref` is **advisory only** — the SportsLink API key is per-dealer and\n  the API has no customer filter, so this field does **not** scope the result. It\n  is echoed back in `metadata.customer_ref` for the caller's audit.\n\n### Response Contract\n\nUnlike the other actions (which exit non-zero on error), `get-for-a2a` **always\nexits 0** and reports failure in-band, so an A2A caller reads one envelope shape\neither way.\n\nSuccess:\n\n```json\n{\n  \"success\": true,\n  \"invoices\": [ { \"source\": \"sports_inc\", \"po_number\": \"P13189\", \"si_doc_number\": 12345, \"total\": 1500.00, \"lines\": [] } ],\n  \"metadata\": { \"count\": 42, \"total_count\": 50, \"pages_read\": 1, \"source\": \"sports_inc\", \"customer_ref\": \"DEALER-001\", \"include_historical\": false },\n  \"error\": null\n}\n```\n\nError:\n\n```json\n{\n  \"success\": false,\n  \"invoices\": null,\n  \"metadata\": null,\n  \"error\": {\n    \"type\": \"auth_error|connection_error|api_error|validation_error\",\n    \"message\": \"Human-readable error message\",\n    \"retriable\": true\n  }\n}\n```\n\nThe `retriable` flag indicates whether the caller should retry (transient\nconnection errors) or escalate (auth/config errors).\n\nFile v0.3.2:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"sportsinc-sportslink\",\n  \"version\": \"0.3.2\",\n  \"publishedAt\": 1784990851103\n}\n\nFile v0.3.2:references/sportslink_api.md\n\n# SportsLink API — reference (distilled from the 2024 Dealers spec)\n\nThe `scripts/sportslink.py` helper wraps all of this; read here when you need a\nparameter, a field, or the semantics behind one.\n\n## Basics\n\n- **Base URL:** `https://api.sportsinc.com/`\n- **Auth:** API key in the `X-API-KEY` request header. Request one from\n  `mhoerner@hq.sportsinc.com`.\n- **Limits:** max **1000 documents per call** — page. Don't retrieve before\n  **~10:30am ET** (SI internal processing completes first).\n\n## GET `/dealers/documents/` — the invoices\n\nReturns JSON documents from the SportsWeb Invoice Center.\n\n### Query parameters (all optional)\n\n| Param | Type | Notes |\n|---|---|---|\n| `poNumber` | string | **Dealer PO number** — the match key to your ERP PO |\n| `supplierDocNumber` | string | The underlying supplier's document number |\n| `siDocNumber` | int | Sports Inc's document id (used to mark historical) |\n| `siDocDate` / `siDocStartDate` / `siDocEndDate` | date `yyyy-MM-dd` | SI processing date. A single date or `start,end`. Start-only = on/after; end-only = on/before |\n| `supplierDocDate` / `supplierDocStartDate` / `supplierDocEndDate` | date | Supplier document date, same range semantics |\n| `lines` | bool (default false) | Include line-item data — **EDI documents only** |\n| `active` | bool (default false) | Only documents **not** marked historical (the un-imported inbox) |\n| `moveToHistorical` | bool (default false) | Mark the returned docs historical **on read** — ⚠️ do NOT use for billing (marks before you've billed); use the PATCH endpoint after billing instead |\n| `excludeScannedDocuments` | bool (default false) | Only documents with line-item data (EDI); scanned/OCR docs have none |\n| `fields` | string[] | Sparse fieldset — return only these properties |\n| `page` / `pageSize` | int | Paging (`pageSize` default = total doc count) |\n| `orderBy` (default `SIDocDate`) / `orderByDescending` | string / bool | Sorting |\n\nExample: `GET /dealers/documents/?siDocDate=2024-01-31&lines=true&active=true`\n\n### Response envelope\n\n`{ items: [ …document… ], pageNumber, pageSize, totalPages, totalCount,\nhasPreviousPage, hasNextPage, orderBy, orderByDescending }`\n\n### Document (header) fields\n\n`poNumber`, `siDocNumber`, `siDocDate`, `dueDate`, `discountDate`,\n`requestedShipDate`, `shipDate`, `supplierDocNumber`, `supplierDocDate`,\n`supplier`, `isCredit`, plus money: `merchandiseTotal`, `freightAmount`,\n`freightAllowance`, `discountAmount`, `siUpcharge`, `svcHandleCharge`,\n`salesTax`, `exciseTax`, `docTotal`. Also `termsOfPayment`, `termsOfDelivery`,\n`carrier`, `weight`, `trackingNumber`, `methodOfPayment`, `supplierAddress{}`,\n`shippingAddress{}`. Missing/OCR-unavailable properties are simply omitted.\n\n### Line fields (EDI only)\n\n`supplierItemNumber`, `upc`, `quantityShipped`, `quantityOrdered`,\n`quantityBackOrdered`, `unit`, `listPrice`, `discountPercent`, `netPrice`,\n`extension`, `size`, `color`, `description`.\n\n`docTotal` = `merchandiseTotal` + `siUpcharge` + `svcHandleCharge` +\n`freightAmount` + `salesTax` + `exciseTax` − `discountAmount` −\n`freightAllowance` (SI-specific charges included — this is what you'll be\nbilled, and what the ERP bill's `expected_total` should tie to).\n\n## PATCH `/dealers/documents/status` — mark consumed\n\nBody: `{ \"siDocNumbers\": [12345, 23456], \"isActive\": false }`\n- `isActive: false` → historical (moves to the Invoice Center \"Historical\" tab).\n- `isActive: true` → back to active.\n\nResponses: `204 No Content` (success), `400` (bad body), `401` (bad key).\n\n**Use this to mark documents consumed AFTER a bill is created** — the exactly-once\nseam. Prefer it over `moveToHistorical=true` on the GET, which marks on read.\n\n## Failure modes to expect\n\n- `401` — bad/expired key (the helper surfaces `auth_error`).\n- Transient `429`/`5xx`/timeouts — the helper retries with backoff.\n- Missing fields — OCR gaps or supplier-not-provided; simply absent, not null.\n- Scanned docs — no `lines`; `has_lines: false` in the normalised shape.\n\nFile v0.3.2:skill-card.md\n\n## Description: <br>\nSports Inc SportsLink API adapter for retrieving dealer invoice documents, normalizing them for payables workflows, and marking successfully imported documents as consumed. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[zmtucker](https://clawhub.ai/user/zmtucker) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nAccounts payable agents and operations teams use this skill to fetch Sports Inc invoice documents, normalize invoice fields for downstream matching, and mark only successfully imported documents historical. It is intended as a source adapter paired with a payables workflow or ERP adapter. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill uses a SportsLink API key to access dealer invoice data. <br>\nMitigation: Install it only for agents that should access the dealer's Sports Inc invoices and share SPORTSINC_API_KEY only through the intended credential mechanism. <br>\nRisk: Including historical documents can broaden retrieval beyond the active unimported invoice inbox. <br>\nMitigation: Keep include_historical disabled unless historical or consumed documents are explicitly needed. <br>\nRisk: mark-historical changes document status and can hide invoices from the active workflow if used too early. <br>\nMitigation: Run mark-historical only after the downstream bill or import succeeds; use SPORTSINC_DRY_RUN when validating the flow. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/zmtucker/skills/sportsinc-sportslink) <br>\n- [Sports Inc homepage](https://www.sportsinc.com) <br>\n- [SportsLink API reference](artifact/references/sportslink_api.md) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [JSON, Shell commands, Guidance] <br>\n**Output Format:** [JSON objects from a Python CLI, with Markdown usage guidance] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Requires python3, requests, and SPORTSINC_API_KEY; SPORTSINC_API_URL and SPORTSINC_DRY_RUN are optional.] <br>\n\n## Skill Version(s): <br>\n0.3.2 (source: release metadata and SKILL.md frontmatter) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.1: 5 files, 13622 bytes\n\nFiles: references/sportslink_api.md (4044b), scripts/sportslink.py (16141b), skill-card.md (2364b), SKILL.md (9561b), _meta.json (139b)\n\nFile v0.3.1:SKILL.md\n\n---\nname: sportsinc-sportslink\ndescription: >\n  Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\")\n  from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports\n  Inc is a buying group that does NOT send individual vendor invoices; its\n  SportsLink REST API is where the invoices live. Use when you need to retrieve\n  Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull\n  SportsLink documents\", \"fetch this month's SI invoices to match against POs\".\n  This is the SOURCE adapter only: it authenticates, pages, normalises each SI\n  document into a common invoice shape (po_number, invoice_number/date,\n  lines[], charges, total, is_credit), and marks documents historical once\n  imported. It is customer-agnostic (every Sports Inc dealer uses this same\n  API) and touches no ERP — pair it with a payables workflow (e.g.\n  `drivethru-payable-matching`) to match against POs and create the bill.\nversion: 0.3.1\nemoji: 🏟️\nhomepage: https://www.sportsinc.com\nmetadata:\n  openclaw:\n    requires:\n      # SPORTSINC_API_KEY is deliberately NOT listed here. openclaw gates a\n      # skill OUT of the model's view when a `requires.env` key is absent from\n      # the *boot* environment — but in A2A mode (see \"Agent-to-Agent (A2A)\n      # Mode\" below) this key is brokered per-turn by the platform and is\n      # absent at boot by design, so gating on it would hide this skill from\n      # the very delegated flow it exists to serve. The key stays fully\n      # documented via `primaryEnv`/`envVars`, and the helper self-guards at\n      # runtime (`config_error`/`auth_error`) when it is genuinely missing.\n      # `python3` stays gated because it must be present at boot.\n      bins: [python3]\n    primaryEnv: SPORTSINC_API_KEY\n    envVars:\n      SPORTSINC_API_KEY:\n        required: true\n        description: >\n          SportsLink API key, sent as the `X-API-KEY` header. Request one from\n          mhoerner@hq.sportsinc.com. Treat as a secret; never paste into chat.\n      SPORTSINC_API_URL:\n        required: false\n        description: Base URL, default `https://api.sportsinc.com/`.\n      SPORTSINC_DRY_RUN:\n        required: false\n        description: If truthy, `mark-historical` is simulated (no state change).\n    install:\n      uv:\n        - requests>=2.28\n---\n\n# Sports Inc SportsLink adapter\n\nSports Inc is a buying group: BaconCo (and every other SI dealer) buys through\nSports Inc, and Sports Inc does **not** email individual vendor invoices —\nthey're published in the SportsWeb Invoice Center and exposed through the\n**SportsLink REST API**. This skill is the source adapter for that API. It does\none job: hand a payables workflow a clean, normalised list of invoices, and\nmark them consumed once they've been imported. It never touches Odoo.\n\nThe single helper is `scripts/sportslink.py`:\n\n```bash\n# The un-imported inbox: active documents that carry line items (EDI), normalised\npython3 scripts/sportslink.py list '{\"active\": true, \"lines\": true, \"ediOnly\": true}'\n\n# A specific document (ignores the active-only filter)\npython3 scripts/sportslink.py get '{\"poNumber\": \"P13189\"}'\n\n# Mark documents consumed — AFTER they've been billed (honors SPORTSINC_DRY_RUN)\npython3 scripts/sportslink.py mark-historical '{\"siDocNumbers\": [12345, 23456]}'\n\n# A2A-safe action for agent-to-agent calls (structured request/response contract)\npython3 scripts/sportslink.py get-for-a2a '{\"customer_ref\": \"DEALER-001\", \"date_range\": {\"start\": \"2024-01-01\", \"end\": \"2024-12-31\"}, \"statuses\": [\"open\"]}'\n```\n\nEvery command prints one JSON object, or `{\"error\": {...}}` with a non-zero\nexit. Needs `SPORTSINC_API_KEY` (if unset, exits `config_error` — stop and tell\nthe user to configure it; never ask for the key in chat).\n\n## Normalised invoice shape\n\n`list`/`get` return `{count, total_count, invoices: [...]}`, each invoice:\n\n```json\n{\n  \"source\": \"sports_inc\",\n  \"po_number\": \"P13189\",          // dealer PO number → the match key to a PO\n  \"si_doc_number\": 12345,          // SI's document id → used to mark-historical\n  \"invoice_number\": \"…\",           // supplierDocNumber (falls back to si_doc_number)\n  \"invoice_date\": \"…\",             // supplierDocDate, else siDocDate\n  \"due_date\": \"…\", \"supplier\": \"…\",\n  \"is_credit\": false,               // credit memo → handle separately, never a bill\n  \"has_lines\": true,                // false for scanned/OCR docs (header totals only)\n  \"total\": 0,                       // docTotal\n  \"charges\": {\"merchandise\", \"freight\", \"freight_allowance\", \"si_upcharge\",\n              \"svc_handle\", \"sales_tax\", \"excise_tax\", \"discount\"},\n  \"lines\": [{\"item\",\"upc\",\"description\",\"size\",\"color\",\"unit\",\n             \"qty_ordered\",\"qty_shipped\",\"qty_backordered\",\n             \"list_price\",\"discount_pct\",\"net_price\",\"extension\"}]\n}\n```\n\nThis is the **same shape a PDF-extracted invoice would have**, so a payables\nworkflow reconciles it without caring that it came from SportsLink.\n\n## Rules that matter\n\n- **Don't pull before ~10:30am ET** — SI's internal processing runs first; earlier\n  reads can be incomplete. Schedule accordingly.\n- **Line data is EDI-only.** Scanned/OCR documents come back with header totals\n  but no `lines` (`has_lines: false`). Pass `ediOnly: true` to fetch only\n  documents with line items; a header-only doc can't be line-verified and should\n  be escalated by the workflow, not blind-billed.\n- **`is_credit: true` is a credit memo** — route it to a human / vendor credit,\n  never create it as a payable.\n- **Exactly-once — the golden rule.** Import first, `mark-historical` **after**\n  the bill is created. This adapter deliberately does **not** use the API's\n  `moveToHistorical=true` GET flag (which marks on read, before billing) — a\n  crash between read and bill would silently drop the invoice. The natural loop:\n  `list active` → bill each in the ERP → `mark-historical` the ones that\n  succeeded; failures/escalations stay active and are retried next run.\n- **Paging** is automatic (`all: true`, the default). Max 1000 docs/call on SI's\n  side; the helper pages to the end (capped at 50 pages as a backstop).\n\n## Where this fits\n\nSource adapter (this) → payables **workflow** (`drivethru-payable-matching`) →\nERP **adapter** (`drivethru-odoo` / `drivethru_mcp`). This skill owns only the\n\"get the invoices + mark them consumed\" half; matching to POs, correcting\npricing, and creating the draft bill live in the workflow. See that skill's\n`references/sportsinc_payables.md` for the end-to-end procedure.\n\n## Agent-to-Agent (A2A) Mode\n\nThe `get-for-a2a` action provides a **contract-driven interface** for inter-agent\ncommunication. Deploy this skill on a dedicated Sports Inc agent and let the\ninternal agent that needs invoices (e.g. an Accounts Payable agent) reach it via\na **delegation connection** in the Knoxville platform.\n\n### Where `SPORTSINC_API_KEY` comes from (credential broker)\n\nThe `SPORTSINC_API_KEY` is bound to the **calling** agent (the one that\nrepresents your company — e.g. Accounts Payable), not to this Sports Inc agent.\nOn that agent's delegation connection to this one, the operator chooses to\n**share** `SPORTSINC_API_KEY` with the connection.\n\nThe value is **pulled on demand**, not pushed. When this agent handles a\ndelegated call (`X-Knox-Caller-Kind: agent`), its runtime calls the platform MCP\ntool **`get_delegated_credentials({ conversation_id })`** for the conversation it\nis answering. The platform verifies this agent is the target of that delegated\nconversation and that the connection shares the credential, **logs the access in\n`agent_connection_audit_log`**, and returns a `{ env_key: value }` map. The\nruntime exposes those values as env vars for the turn, so `sportslink.py` reads\n`SPORTSINC_API_KEY` from the environment exactly as it does standalone.\n\nIf the credential isn't present, the caller's connection hasn't shared it (or the\nruntime didn't fetch it) — surface a clear `auth_error` rather than guessing.\nNever print the credential value into the chat reply.\n\n### Request Contract\n\n`get-for-a2a` params (all optional):\n\n```json\n{\n  \"customer_ref\": \"DEALER-001\",\n  \"date_range\": { \"start\": \"2024-01-01\", \"end\": \"2024-12-31\" },\n  \"include_historical\": false\n}\n```\n\n- `include_historical` (default `false`) → active/un-imported invoices only;\n  set `true` to also include historical/consumed docs.\n- `customer_ref` is **advisory only** — the SportsLink API key is per-dealer and\n  the API has no customer filter, so this field does **not** scope the result. It\n  is echoed back in `metadata.customer_ref` for the caller's audit.\n\n### Response Contract\n\nUnlike the other actions (which exit non-zero on error), `get-for-a2a` **always\nexits 0** and reports failure in-band, so an A2A caller reads one envelope shape\neither way.\n\nSuccess:\n\n```json\n{\n  \"success\": true,\n  \"invoices\": [ { \"source\": \"sports_inc\", \"po_number\": \"P13189\", \"si_doc_number\": 12345, \"total\": 1500.00, \"lines\": [] } ],\n  \"metadata\": { \"count\": 42, \"total_count\": 50, \"pages_read\": 1, \"source\": \"sports_inc\", \"customer_ref\": \"DEALER-001\", \"include_historical\": false },\n  \"error\": null\n}\n```\n\nError:\n\n```json\n{\n  \"success\": false,\n  \"invoices\": null,\n  \"metadata\": null,\n  \"error\": {\n    \"type\": \"auth_error|connection_error|api_error|validation_error\",\n    \"message\": \"Human-readable error message\",\n    \"retriable\": true\n  }\n}\n```\n\nThe `retriable` flag indicates whether the caller should retry (transient\nconnection errors) or escalate (auth/config errors).\n\nFile v0.3.1:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"sportsinc-sportslink\",\n  \"version\": \"0.3.1\",\n  \"publishedAt\": 1784832519213\n}\n\nFile v0.3.1:references/sportslink_api.md\n\n# SportsLink API — reference (distilled from the 2024 Dealers spec)\n\nThe `scripts/sportslink.py` helper wraps all of this; read here when you need a\nparameter, a field, or the semantics behind one.\n\n## Basics\n\n- **Base URL:** `https://api.sportsinc.com/`\n- **Auth:** API key in the `X-API-KEY` request header. Request one from\n  `mhoerner@hq.sportsinc.com`.\n- **Limits:** max **1000 documents per call** — page. Don't retrieve before\n  **~10:30am ET** (SI internal processing completes first).\n\n## GET `/dealers/documents/` — the invoices\n\nReturns JSON documents from the SportsWeb Invoice Center.\n\n### Query parameters (all optional)\n\n| Param | Type | Notes |\n|---|---|---|\n| `poNumber` | string | **Dealer PO number** — the match key to your ERP PO |\n| `supplierDocNumber` | string | The underlying supplier's document number |\n| `siDocNumber` | int | Sports Inc's document id (used to mark historical) |\n| `siDocDate` / `siDocStartDate` / `siDocEndDate` | date `yyyy-MM-dd` | SI processing date. A single date or `start,end`. Start-only = on/after; end-only = on/before |\n| `supplierDocDate` / `supplierDocStartDate` / `supplierDocEndDate` | date | Supplier document date, same range semantics |\n| `lines` | bool (default false) | Include line-item data — **EDI documents only** |\n| `active` | bool (default false) | Only documents **not** marked historical (the un-imported inbox) |\n| `moveToHistorical` | bool (default false) | Mark the returned docs historical **on read** — ⚠️ do NOT use for billing (marks before you've billed); use the PATCH endpoint after billing instead |\n| `excludeScannedDocuments` | bool (default false) | Only documents with line-item data (EDI); scanned/OCR docs have none |\n| `fields` | string[] | Sparse fieldset — return only these properties |\n| `page` / `pageSize` | int | Paging (`pageSize` default = total doc count) |\n| `orderBy` (default `SIDocDate`) / `orderByDescending` | string / bool | Sorting |\n\nExample: `GET /dealers/documents/?siDocDate=2024-01-31&lines=true&active=true`\n\n### Response envelope\n\n`{ items: [ …document… ], pageNumber, pageSize, totalPages, totalCount,\nhasPreviousPage, hasNextPage, orderBy, orderByDescending }`\n\n### Document (header) fields\n\n`poNumber`, `siDocNumber`, `siDocDate`, `dueDate`, `discountDate`,\n`requestedShipDate`, `shipDate`, `supplierDocNumber`, `supplierDocDate`,\n`supplier`, `isCredit`, plus money: `merchandiseTotal`, `freightAmount`,\n`freightAllowance`, `discountAmount`, `siUpcharge`, `svcHandleCharge`,\n`salesTax`, `exciseTax`, `docTotal`. Also `termsOfPayment`, `termsOfDelivery`,\n`carrier`, `weight`, `trackingNumber`, `methodOfPayment`, `supplierAddress{}`,\n`shippingAddress{}`. Missing/OCR-unavailable properties are simply omitted.\n\n### Line fields (EDI only)\n\n`supplierItemNumber`, `upc`, `quantityShipped`, `quantityOrdered`,\n`quantityBackOrdered`, `unit`, `listPrice`, `discountPercent`, `netPrice`,\n`extension`, `size`, `color`, `description`.\n\n`docTotal` = `merchandiseTotal` + `siUpcharge` + `svcHandleCharge` +\n`freightAmount` + `salesTax` + `exciseTax` − `discountAmount` −\n`freightAllowance` (SI-specific charges included — this is what you'll be\nbilled, and what the ERP bill's `expected_total` should tie to).\n\n## PATCH `/dealers/documents/status` — mark consumed\n\nBody: `{ \"siDocNumbers\": [12345, 23456], \"isActive\": false }`\n- `isActive: false` → historical (moves to the Invoice Center \"Historical\" tab).\n- `isActive: true` → back to active.\n\nResponses: `204 No Content` (success), `400` (bad body), `401` (bad key).\n\n**Use this to mark documents consumed AFTER a bill is created** — the exactly-once\nseam. Prefer it over `moveToHistorical=true` on the GET, which marks on read.\n\n## Failure modes to expect\n\n- `401` — bad/expired key (the helper surfaces `auth_error`).\n- Transient `429`/`5xx`/timeouts — the helper retries with backoff.\n- Missing fields — OCR gaps or supplier-not-provided; simply absent, not null.\n- Scanned docs — no `lines`; `has_lines: false` in the normalised shape.\n\nFile v0.3.1:skill-card.md\n\n## Description: <br>\nSports Inc SportsLink API adapter pulls a dealer's invoice documents from the Sports Inc SportsWeb Invoice Center, normalizes them for payables matching, and marks documents consumed after import. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[zmtucker](https://clawhub.ai/user/zmtucker) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and accounts payable agents use this skill to retrieve Sports Inc invoice documents, normalize them into a common invoice shape, and hand them to a payables workflow for PO matching and bill creation. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill requires access to a Sports Inc API key that can retrieve dealer invoice data. <br>\nMitigation: Confirm the agent should have this credential, share it only through the platform credential mechanism, and do not paste the key into chat. <br>\nRisk: The mark-historical action changes invoice state by marking selected documents consumed. <br>\nMitigation: Run mark-historical only after successful import and bill creation, and use SPORTSINC_DRY_RUN when testing. <br>\nRisk: Changing SPORTSINC_API_URL could send requests to an unintended endpoint. <br>\nMitigation: Keep the default trusted Sports Inc endpoint unless deliberately operating a compatible proxy. <br>\n\n\n## Reference(s): <br>\n- [SportsLink API reference](references/sportslink_api.md) <br>\n- [Sports Inc](https://www.sportsinc.com) <br>\n- [SportsLink API endpoint](https://api.sportsinc.com/) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [JSON, Shell commands, Configuration guidance] <br>\n**Output Format:** [JSON objects printed by command-line helper actions, with Markdown usage examples in the skill documentation] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Requires SPORTSINC_API_KEY; supports list, get, mark-historical, and get-for-a2a actions.] <br>\n\n## Skill Version(s): <br>\n0.3.1 (source: frontmatter and server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.0: 5 files, 13391 bytes\n\nFiles: references/sportslink_api.md (4044b), scripts/sportslink.py (16141b), skill-card.md (2522b), SKILL.md (8917b), _meta.json (139b)\n\nFile v0.3.0:SKILL.md\n\n---\nname: sportsinc-sportslink\ndescription: >\n  Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\")\n  from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports\n  Inc is a buying group that does NOT send individual vendor invoices; its\n  SportsLink REST API is where the invoices live. Use when you need to retrieve\n  Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull\n  SportsLink documents\", \"fetch this month's SI invoices to match against POs\".\n  This is the SOURCE adapter only: it authenticates, pages, normalises each SI\n  document into a common invoice shape (po_number, invoice_number/date,\n  lines[], charges, total, is_credit), and marks documents historical once\n  imported. It is customer-agnostic (every Sports Inc dealer uses this same\n  API) and touches no ERP — pair it with a payables workflow (e.g.\n  `drivethru-payable-matching`) to match against POs and create the bill.\nversion: 0.3.0\nemoji: 🏟️\nhomepage: https://www.sportsinc.com\nmetadata:\n  openclaw:\n    requires:\n      env: [SPORTSINC_API_KEY]\n      bins: [python3]\n    primaryEnv: SPORTSINC_API_KEY\n    envVars:\n      SPORTSINC_API_KEY:\n        required: true\n        description: >\n          SportsLink API key, sent as the `X-API-KEY` header. Request one from\n          mhoerner@hq.sportsinc.com. Treat as a secret; never paste into chat.\n      SPORTSINC_API_URL:\n        required: false\n        description: Base URL, default `https://api.sportsinc.com/`.\n      SPORTSINC_DRY_RUN:\n        required: false\n        description: If truthy, `mark-historical` is simulated (no state change).\n    install:\n      uv:\n        - requests>=2.28\n---\n\n# Sports Inc SportsLink adapter\n\nSports Inc is a buying group: BaconCo (and every other SI dealer) buys through\nSports Inc, and Sports Inc does **not** email individual vendor invoices —\nthey're published in the SportsWeb Invoice Center and exposed through the\n**SportsLink REST API**. This skill is the source adapter for that API. It does\none job: hand a payables workflow a clean, normalised list of invoices, and\nmark them consumed once they've been imported. It never touches Odoo.\n\nThe single helper is `scripts/sportslink.py`:\n\n```bash\n# The un-imported inbox: active documents that carry line items (EDI), normalised\npython3 scripts/sportslink.py list '{\"active\": true, \"lines\": true, \"ediOnly\": true}'\n\n# A specific document (ignores the active-only filter)\npython3 scripts/sportslink.py get '{\"poNumber\": \"P13189\"}'\n\n# Mark documents consumed — AFTER they've been billed (honors SPORTSINC_DRY_RUN)\npython3 scripts/sportslink.py mark-historical '{\"siDocNumbers\": [12345, 23456]}'\n\n# A2A-safe action for agent-to-agent calls (structured request/response contract)\npython3 scripts/sportslink.py get-for-a2a '{\"customer_ref\": \"DEALER-001\", \"date_range\": {\"start\": \"2024-01-01\", \"end\": \"2024-12-31\"}, \"statuses\": [\"open\"]}'\n```\n\nEvery command prints one JSON object, or `{\"error\": {...}}` with a non-zero\nexit. Needs `SPORTSINC_API_KEY` (if unset, exits `config_error` — stop and tell\nthe user to configure it; never ask for the key in chat).\n\n## Normalised invoice shape\n\n`list`/`get` return `{count, total_count, invoices: [...]}`, each invoice:\n\n```json\n{\n  \"source\": \"sports_inc\",\n  \"po_number\": \"P13189\",          // dealer PO number → the match key to a PO\n  \"si_doc_number\": 12345,          // SI's document id → used to mark-historical\n  \"invoice_number\": \"…\",           // supplierDocNumber (falls back to si_doc_number)\n  \"invoice_date\": \"…\",             // supplierDocDate, else siDocDate\n  \"due_date\": \"…\", \"supplier\": \"…\",\n  \"is_credit\": false,               // credit memo → handle separately, never a bill\n  \"has_lines\": true,                // false for scanned/OCR docs (header totals only)\n  \"total\": 0,                       // docTotal\n  \"charges\": {\"merchandise\", \"freight\", \"freight_allowance\", \"si_upcharge\",\n              \"svc_handle\", \"sales_tax\", \"excise_tax\", \"discount\"},\n  \"lines\": [{\"item\",\"upc\",\"description\",\"size\",\"color\",\"unit\",\n             \"qty_ordered\",\"qty_shipped\",\"qty_backordered\",\n             \"list_price\",\"discount_pct\",\"net_price\",\"extension\"}]\n}\n```\n\nThis is the **same shape a PDF-extracted invoice would have**, so a payables\nworkflow reconciles it without caring that it came from SportsLink.\n\n## Rules that matter\n\n- **Don't pull before ~10:30am ET** — SI's internal processing runs first; earlier\n  reads can be incomplete. Schedule accordingly.\n- **Line data is EDI-only.** Scanned/OCR documents come back with header totals\n  but no `lines` (`has_lines: false`). Pass `ediOnly: true` to fetch only\n  documents with line items; a header-only doc can't be line-verified and should\n  be escalated by the workflow, not blind-billed.\n- **`is_credit: true` is a credit memo** — route it to a human / vendor credit,\n  never create it as a payable.\n- **Exactly-once — the golden rule.** Import first, `mark-historical` **after**\n  the bill is created. This adapter deliberately does **not** use the API's\n  `moveToHistorical=true` GET flag (which marks on read, before billing) — a\n  crash between read and bill would silently drop the invoice. The natural loop:\n  `list active` → bill each in the ERP → `mark-historical` the ones that\n  succeeded; failures/escalations stay active and are retried next run.\n- **Paging** is automatic (`all: true`, the default). Max 1000 docs/call on SI's\n  side; the helper pages to the end (capped at 50 pages as a backstop).\n\n## Where this fits\n\nSource adapter (this) → payables **workflow** (`drivethru-payable-matching`) →\nERP **adapter** (`drivethru-odoo` / `drivethru_mcp`). This skill owns only the\n\"get the invoices + mark them consumed\" half; matching to POs, correcting\npricing, and creating the draft bill live in the workflow. See that skill's\n`references/sportsinc_payables.md` for the end-to-end procedure.\n\n## Agent-to-Agent (A2A) Mode\n\nThe `get-for-a2a` action provides a **contract-driven interface** for inter-agent\ncommunication. Deploy this skill on a dedicated Sports Inc agent and let the\ninternal agent that needs invoices (e.g. an Accounts Payable agent) reach it via\na **delegation connection** in the Knoxville platform.\n\n### Where `SPORTSINC_API_KEY` comes from (credential broker)\n\nThe `SPORTSINC_API_KEY` is bound to the **calling** agent (the one that\nrepresents your company — e.g. Accounts Payable), not to this Sports Inc agent.\nOn that agent's delegation connection to this one, the operator chooses to\n**share** `SPORTSINC_API_KEY` with the connection.\n\nThe value is **pulled on demand**, not pushed. When this agent handles a\ndelegated call (`X-Knox-Caller-Kind: agent`), its runtime calls the platform MCP\ntool **`get_delegated_credentials({ conversation_id })`** for the conversation it\nis answering. The platform verifies this agent is the target of that delegated\nconversation and that the connection shares the credential, **logs the access in\n`agent_connection_audit_log`**, and returns a `{ env_key: value }` map. The\nruntime exposes those values as env vars for the turn, so `sportslink.py` reads\n`SPORTSINC_API_KEY` from the environment exactly as it does standalone.\n\nIf the credential isn't present, the caller's connection hasn't shared it (or the\nruntime didn't fetch it) — surface a clear `auth_error` rather than guessing.\nNever print the credential value into the chat reply.\n\n### Request Contract\n\n`get-for-a2a` params (all optional):\n\n```json\n{\n  \"customer_ref\": \"DEALER-001\",\n  \"date_range\": { \"start\": \"2024-01-01\", \"end\": \"2024-12-31\" },\n  \"include_historical\": false\n}\n```\n\n- `include_historical` (default `false`) → active/un-imported invoices only;\n  set `true` to also include historical/consumed docs.\n- `customer_ref` is **advisory only** — the SportsLink API key is per-dealer and\n  the API has no customer filter, so this field does **not** scope the result. It\n  is echoed back in `metadata.customer_ref` for the caller's audit.\n\n### Response Contract\n\nUnlike the other actions (which exit non-zero on error), `get-for-a2a` **always\nexits 0** and reports failure in-band, so an A2A caller reads one envelope shape\neither way.\n\nSuccess:\n\n```json\n{\n  \"success\": true,\n  \"invoices\": [ { \"source\": \"sports_inc\", \"po_number\": \"P13189\", \"si_doc_number\": 12345, \"total\": 1500.00, \"lines\": [] } ],\n  \"metadata\": { \"count\": 42, \"total_count\": 50, \"pages_read\": 1, \"source\": \"sports_inc\", \"customer_ref\": \"DEALER-001\", \"include_historical\": false },\n  \"error\": null\n}\n```\n\nError:\n\n```json\n{\n  \"success\": false,\n  \"invoices\": null,\n  \"metadata\": null,\n  \"error\": {\n    \"type\": \"auth_error|connection_error|api_error|validation_error\",\n    \"message\": \"Human-readable error message\",\n    \"retriable\": true\n  }\n}\n```\n\nThe `retriable` flag indicates whether the caller should retry (transient\nconnection errors) or escalate (auth/config errors).\n\nFile v0.3.0:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"sportsinc-sportslink\",\n  \"version\": \"0.3.0\",\n  \"publishedAt\": 1784820806462\n}\n\nFile v0.3.0:references/sportslink_api.md\n\n# SportsLink API — reference (distilled from the 2024 Dealers spec)\n\nThe `scripts/sportslink.py` helper wraps all of this; read here when you need a\nparameter, a field, or the semantics behind one.\n\n## Basics\n\n- **Base URL:** `https://api.sportsinc.com/`\n- **Auth:** API key in the `X-API-KEY` request header. Request one from\n  `mhoerner@hq.sportsinc.com`.\n- **Limits:** max **1000 documents per call** — page. Don't retrieve before\n  **~10:30am ET** (SI internal processing completes first).\n\n## GET `/dealers/documents/` — the invoices\n\nReturns JSON documents from the SportsWeb Invoice Center.\n\n### Query parameters (all optional)\n\n| Param | Type | Notes |\n|---|---|---|\n| `poNumber` | string | **Dealer PO number** — the match key to your ERP PO |\n| `supplierDocNumber` | string | The underlying supplier's document number |\n| `siDocNumber` | int | Sports Inc's document id (used to mark historical) |\n| `siDocDate` / `siDocStartDate` / `siDocEndDate` | date `yyyy-MM-dd` | SI processing date. A single date or `start,end`. Start-only = on/after; end-only = on/before |\n| `supplierDocDate` / `supplierDocStartDate` / `supplierDocEndDate` | date | Supplier document date, same range semantics |\n| `lines` | bool (default false) | Include line-item data — **EDI documents only** |\n| `active` | bool (default false) | Only documents **not** marked historical (the un-imported inbox) |\n| `moveToHistorical` | bool (default false) | Mark the returned docs historical **on read** — ⚠️ do NOT use for billing (marks before you've billed); use the PATCH endpoint after billing instead |\n| `excludeScannedDocuments` | bool (default false) | Only documents with line-item data (EDI); scanned/OCR docs have none |\n| `fields` | string[] | Sparse fieldset — return only these properties |\n| `page` / `pageSize` | int | Paging (`pageSize` default = total doc count) |\n| `orderBy` (default `SIDocDate`) / `orderByDescending` | string / bool | Sorting |\n\nExample: `GET /dealers/documents/?siDocDate=2024-01-31&lines=true&active=true`\n\n### Response envelope\n\n`{ items: [ …document… ], pageNumber, pageSize, totalPages, totalCount,\nhasPreviousPage, hasNextPage, orderBy, orderByDescending }`\n\n### Document (header) fields\n\n`poNumber`, `siDocNumber`, `siDocDate`, `dueDate`, `discountDate`,\n`requestedShipDate`, `shipDate`, `supplierDocNumber`, `supplierDocDate`,\n`supplier`, `isCredit`, plus money: `merchandiseTotal`, `freightAmount`,\n`freightAllowance`, `discountAmount`, `siUpcharge`, `svcHandleCharge`,\n`salesTax`, `exciseTax`, `docTotal`. Also `termsOfPayment`, `termsOfDelivery`,\n`carrier`, `weight`, `trackingNumber`, `methodOfPayment`, `supplierAddress{}`,\n`shippingAddress{}`. Missing/OCR-unavailable properties are simply omitted.\n\n### Line fields (EDI only)\n\n`supplier\n\nArchive v0.2.0: 5 files, 13475 bytes\n\nFiles: references/sportslink_api.md (4044b), scripts/sportslink.py (16141b), skill-card.md (2841b), SKILL.md (8792b), _meta.json (139b)\n\nArchive v0.1.0: 5 files, 11197 bytes\n\nFiles: references/sportslink_api.md (4044b), scripts/sportslink.py (13040b), skill-card.md (2589b), SKILL.md (5693b), _meta.json (139b)","readmeExcerpt":"Skill: sportsinc-sportslink Owner: zmtucker Summary: Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\") from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports Inc is a buying group that does NOT send individual vendor invoices; its SportsLink REST API is where the invoices live. Use when you need to retrieve Sports Inc invoices for payables — \"get the Sports Inc invoices\",","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"# The un-imported inbox: every active document, normalised. Scanned documents\n# come back with no lines and are named in `needs_line_recovery` — see the\n# retrieval procedure below. Do NOT pass `ediOnly: true` for a billing run: it\n# filters those documents out of the result entirely, so they are never billed\n# and simply age.\npython3 scripts/sportslink.py list '{\"active\": true, \"lines\": true}'\n\n# A specific document (ignores the active-only filter)\npython3 scripts/sportslink.py get '{\"poNumber\": \"P13189\"}'\n\n# Mark documents consumed — AFTER they've been billed (honors SPORTSINC_DRY_RUN)\npython3 scripts/sportslink.py mark-historical '{\"siDocNumbers\": [12345, 23456]}'\n\n# A2A-safe action for agent-to-agent calls (structured request/response contract)\npython3 scripts/sportslink.py get-for-a2a '{\"customer_ref\": \"DEALER-001\", \"date_range\": {\"start\": \"2024-01-01\", \"end\": \"2024-12-31\"}, \"statuses\": [\"open\"]}'\n\n# Header-only (scanned) document: get its PDF, read it yourself, then get checked\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 23962348}'\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 23962348, \"lines\": [...]}'"},{"language":"json","snippet":"\"needs_line_recovery\": [24682750, 24684277],\n\"credits\": [24690002],\n\"next_step\": \"2 document(s) have no line detail from the API …\""},{"language":"bash","snippet":"# 1. Fetch it. Logs in to the portal, downloads the PDF, and OCRs the scanned\n#    vendor invoice into text. Active tab first, Archived automatically after.\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 24684277}'\n\n# 2. Read the returned `text` and extract the line items yourself.\n#    See references/pdf_extraction.md — which pages to skip, the field mapping,\n#    and the rules (transcribe don't compute, never infer a quantity).\n\n# 3. Hand them back to be checked against the API's own merchandise total.\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 24684277, \"lines\": [...]}'"},{"language":"json","snippet":"{\n  \"source\": \"sports_inc\",\n  \"po_number\": \"P13189\",          // dealer PO number → the match key to a PO\n  \"si_doc_number\": 12345,          // SI's document id → used to mark-historical\n  \"invoice_number\": \"…\",           // supplierDocNumber (falls back to si_doc_number)\n  \"invoice_date\": \"…\",             // supplierDocDate, else siDocDate\n  \"due_date\": \"…\", \"supplier\": \"…\",\n  \"is_credit\": false,               // credit memo → handle separately, never a bill\n  \"has_lines\": true,                // false for scanned/OCR docs (header totals only)\n  \"placeholder_lines\": 0,           // empty rows the API returned and we dropped\n  \"placeholder_note\": null,         // what they said, e.g. \"SEE VENDOR INVOICE FOR DETAIL.\"\n                                    // has_lines false ⇒ this document appears in\n                                    // `needs_line_recovery`; see the retrieval\n                                    // procedure above before billing it\n  \"lines_source\": \"pdf\",            // present only when lines were recovered from a PDF\n  \"total\": 0,                       // docTotal\n  \"charges\": {\"merchandise\", \"freight\", \"freight_allowance\", \"si_upcharge\",\n              \"svc_handle\", \"sales_tax\", \"excise_tax\", \"discount\"},\n  \"lines\": [{\"item\",\"upc\",\"description\",\"size\",\"color\",\"unit\",\n             \"qty_ordered\",\"qty_shipped\",\"qty_backordered\",\n             \"list_price\",\"discount_pct\",\"net_price\",\"extension\"}]\n}"},{"language":"text","snippet":"fetch-invoice-doc  →  you read the PDF  →  reconcile-lines\n   (Python: I/O)      (the only fuzzy step)   (Python: arithmetic)"},{"language":"text","snippet":"www.sportsinc.us  →  click DEALER LOGIN  →  sportsweb.us.auth0.com/u/login\n  →  swv3.sportsinc.com/home        ← the home screen, and the only search box\n  →  type the PO, click Search      → swv2h.sportsinc.com/Member/InvoiceCenter/…\n  →  tick the matching row(s)  →  Downloads  →  \"PDF File\""}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: sportsinc-sportslink\ndescription: >\n  Sports Inc SportsLink API adapter — pull a dealer's invoices (\"documents\")\n  from the Sports Inc SportsWeb Invoice Center and mark them consumed. Sports\n  Inc is a buying group that does NOT send individual vendor invoices; its\n  SportsLink REST API is where the invoices live. Use when you need to retrieve\n  Sports Inc invoices for payables — \"get the Sports Inc invoices\", \"pull\n  SportsLink documents\", \"fetch this month's SI invoices to match against POs\".\n  This is the SOURCE adapter only: it authenticates, pages, normalises each SI\n  document into a common invoice shape (po_number, invoice_number/date,\n  lines[], charges, total, is_credit), and marks documents historical once\n  imported. Some documents are scanned rather than EDI, so the API returns their\n  header totals with NO line items (`has_lines: false`) — every retrieval names\n  those in `needs_line_recovery`, and they are not billable as returned. For\n  each, this skill logs in to the SportsWeb portal, downloads the invoice PDF,\n  OCRs the scanned vendor invoice into text for the agent to read, and then\n  checks the extracted lines against the API's own merchandise total before any\n  of it is billable. It is customer-agnostic (every Sports Inc\n  dealer uses this same API) and touches no ERP — pair it with a payables\n  workflow (e.g. `drivethru-payable-matching`) to match against POs and create\n  the bill.\nversion: 0.7.1\nemoji: 🏟️\nhomepage: https://www.sportsinc.com\nmetadata:\n  openclaw:\n    requires:\n      # SPORTSINC_API_KEY is deliberately NOT listed here. openclaw gates a\n      # skill OUT of the model's view when a `requires.env` key is absent from\n      # the *boot* environment — but in A2A mode (see \"Agent-to-Agent (A2A)\n      # Mode\" below) this key is brokered per-turn by the platform and is\n      # absent at boot by design, so gating on it would hide this skill from\n      # the very delegated flow it exists to serve. The key stays fully\n      # documented via `primaryEnv`/`envVars`, and the helper self-guards at\n      # runtime (`config_error`/`auth_error`) when it is genuinely missing.\n      # `python3` stays gated because it must be present at boot.\n      bins: [python3]\n    primaryEnv: SPORTSINC_API_KEY\n    envVars:\n      SPORTSINC_API_KEY:\n        required: true\n        description: >\n          SportsLink API key, sent as the `X-API-KEY` header. Request one from\n          mhoerner@hq.sportsinc.com. Treat as a secret; never paste into chat.\n      SPORTSINC_API_URL:\n        required: false\n        description: Base URL, default `https://api.sportsinc.com/`.\n      SPORTSINC_DRY_RUN:\n        required: false\n        description: If truthy, `mark-historical` is simulated (no state change).\n      SPORTSINC_WEB_USERNAME:\n        required: false\n        description: >\n          SportsWeb portal login, used only by `fetch-invoice-doc` to pull a\n          scanned document's PDF. This is the dealer's own portal user and is\n          SEP"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"sportsinc-sportslink\",\n  \"version\": \"0.7.1\",\n  \"publishedAt\": 1786111175182\n}"},{"path":"references/pdf_extraction.md","content":"# Reading a Sports Inc invoice PDF — extraction guide\n\nYou are here because a SportsLink document came back `has_lines: false`. Sports\nInc scanned it rather than receiving it as EDI, so the API has the header money\nbut no line items.\n\nThe *interpretation* is *your* job. The Python around you does everything\ndeterministic first: it fetches the PDF, classifies its pages, extracts the\nscanned ones, **OCRs them into text**, and then audits the lines you hand back.\n\nSo you are normally reading text, not looking at a picture. That ordering is\ndeliberate — text is a fraction of the context cost, and it can be diffed,\ngrepped and logged. The images stay on disk for when the text is not good\nenough, and there is no extraction *model* anywhere in the pipeline: OCR\ntranscribes, you interpret, arithmetic checks.\n\n## The loop\n\n```bash\n# 1. Get the document and make it readable.\npython3 scripts/sportslink.py fetch-invoice-doc '{\"si_doc_number\": 24682750}'\n\n# 2. You read it. (This document.)\n\n# 3. Hand the lines back to be checked against the API's header money.\npython3 scripts/sportslink.py reconcile-lines '{\"si_doc_number\": 24682750, \"lines\": [...]}'\n```\n\nStep 3 returns `status: \"verified\"` — a normalised invoice the payables workflow\nconsumes exactly like an EDI one — or `status: \"needs_review\"`. **Only a\n`verified` invoice may be billed.**\n\n## What a Sports Inc download actually contains\n\nNot one invoice. A stack of documents, each of which is two parts:\n\n| | Page | Content |\n|---|---|---|\n| **SI cover** | landscape, native text | Sports Inc's own invoice. **No line detail** — it says, in as many words, `SEE VENDOR INVOICE FOR DETAIL.` Carries the SI document number, the PO number, and the totals. |\n| **Vendor invoice** | portrait, usually a **300dpi scan** | The actual supplier invoice. **This is where the line items are.** |\n\nOne PDF routinely holds several of these pairs — one per SI document on the PO.\n\n**Ignore the SI cover pages.** They are useless for extraction; if they held line\ndetail, the API would have had it too. Their only jobs are to divide the stack\ninto documents and to tell you which SI document number each vendor invoice\nbelongs to.\n\n`fetch-invoice-doc` does that division for you:\n\n```json\n\"documents\": [\n  {\"si_cover_page\": 1, \"detail_pages\": [2], \"detail_is_scanned\": true,\n   \"si_doc_number_candidates\": [24682750], \"supplier_doc_candidates\": [\"SI3503366\"],\n   \"matches_requested\": true},\n  {\"si_cover_page\": 3, \"detail_pages\": [4], \"detail_is_scanned\": true,\n   \"si_doc_number_candidates\": [24684277], \"supplier_doc_candidates\": [\"SI3503509\"],\n   \"matches_requested\": false}\n]\n```\n\n**Extract the document with `matches_requested: true`, and reconcile one document\nat a time.** Its lines tie to *its* SI document's totals, not to the stack's.\nThe others in the same PDF are separate SI documents with their own\n`si_doc_number`; handle each with its own `fetch-invoice-doc` /\n`reconcile-lines` pair. If nothing matches, `notes` will say so — stop and "},{"path":"references/sportslink_api.md","content":"# SportsLink API — reference (distilled from the 2024 Dealers spec)\n\nThe `scripts/sportslink.py` helper wraps all of this; read here when you need a\nparameter, a field, or the semantics behind one.\n\n## Basics\n\n- **Base URL:** `https://api.sportsinc.com/`\n- **Auth:** API key in the `X-API-KEY` request header. Request one from\n  `mhoerner@hq.sportsinc.com`.\n- **Limits:** max **1000 documents per call** — page. Don't retrieve before\n  **~10:30am ET** (SI internal processing completes first).\n\n## GET `/dealers/documents/` — the invoices\n\nReturns JSON documents from the SportsWeb Invoice Center.\n\n### Query parameters (all optional)\n\n| Param | Type | Notes |\n|---|---|---|\n| `poNumber` | string | **Dealer PO number** — the match key to your ERP PO |\n| `supplierDocNumber` | string | The underlying supplier's document number |\n| `siDocNumber` | int | Sports Inc's document id (used to mark historical) |\n| `siDocDate` / `siDocStartDate` / `siDocEndDate` | date `yyyy-MM-dd` | SI processing date. A single date or `start,end`. Start-only = on/after; end-only = on/before |\n| `supplierDocDate` / `supplierDocStartDate` / `supplierDocEndDate` | date | Supplier document date, same range semantics |\n| `lines` | bool (default false) | Include line-item data — **EDI documents only** |\n| `active` | bool (default false) | Only documents **not** marked historical (the un-imported inbox) |\n| `moveToHistorical` | bool (default false) | Mark the returned docs historical **on read** — ⚠️ do NOT use for billing (marks before you've billed); use the PATCH endpoint after billing instead |\n| `excludeScannedDocuments` | bool (default false) | Only documents with line-item data (EDI); scanned/OCR docs have none |\n| `fields` | string[] | Sparse fieldset — return only these properties |\n| `page` / `pageSize` | int | Paging (`pageSize` default = total doc count) |\n| `orderBy` (default `SIDocDate`) / `orderByDescending` | string / bool | Sorting |\n\nExample: `GET /dealers/documents/?siDocDate=2024-01-31&lines=true&active=true`\n\n### Response envelope\n\n`{ items: [ …document… ], pageNumber, pageSize, totalPages, totalCount,\nhasPreviousPage, hasNextPage, orderBy, orderByDescending }`\n\n### Document (header) fields\n\n`poNumber`, `siDocNumber`, `siDocDate`, `dueDate`, `discountDate`,\n`requestedShipDate`, `shipDate`, `supplierDocNumber`, `supplierDocDate`,\n`supplier`, `isCredit`, plus money: `merchandiseTotal`, `freightAmount`,\n`freightAllowance`, `discountAmount`, `siUpcharge`, `svcHandleCharge`,\n`salesTax`, `exciseTax`, `docTotal`. Also `termsOfPayment`, `termsOfDelivery`,\n`carrier`, `weight`, `trackingNumber`, `methodOfPayment`, `supplierAddress{}`,\n`shippingAddress{}`. Missing/OCR-unavailable properties are simply omitted.\n\n### Line fields (EDI only)\n\n`supplierItemNumber`, `upc`, `quantityShipped`, `quantityOrdered`,\n`quantityBackOrdered`, `unit`, `listPrice`, `discountPercent`, `netPrice`,\n`extension`, `size`, `color`, `description`.\n\n`docTotal` = `merchandiseTotal` + `siUpcharge` + `svcHa"},{"path":"references/sportsweb_flow_notes.md","content":"# SportsWeb portal — invoice PDF flow\n\nThe reverse-engineered flow behind `scripts/sportsweb_browser.py`, captured from\na live walkthrough of the portal.\n\n**Status: confirmed end to end on the live portal.** A `capture-portal` run with\n`probe_download` logged in, searched, parsed both P13554 rows, ticked one, and\npulled back a 121,619-byte PDF in 17.9 seconds — see \"What the live run proved\"\nbelow.\n\n## Hosts\n\n| | |\n|---|---|\n| Public site | `https://www.sportsinc.us/` — Wix marketing site. **The entry point**: its DEALER LOGIN button must be *clicked*. |\n| Login entry | `https://swv3.sportsinc.com/login-redirect` — the button's href. **Returns 403 on a direct hit**; only works as a click-through. |\n| Identity provider | `https://sportsweb.us.auth0.com/u/login?state=…` (Auth0 Universal Login) |\n| **Home screen** | `https://swv3.sportsinc.com/home` — where login lands, and the **only** page with a search box |\n| Invoice Center | `https://swv2h.sportsinc.com/Member/InvoiceCenter/Default.aspx` — where the search navigates to |\n\n**Home and the Invoice Center are different hosts.** That is not cosmetic: a\nsession check pointed at `swv2h` can never find the search box, so it always\nreports \"not logged in\" and sends every run through a full login it did not\nneed. `SPORTSINC_WEB_HOME_URL` overrides the home; `SPORTSINC_WEB_BASE_URL` the\nInvoice Center host.\n\nThe Invoice Center is **ASP.NET WebForms**; the home screen around it is a Vue\napp. Both appear in the flow.\n\n## Waiting — never on `networkidle`\n\nThe home screen is a Vue app that holds connections open, so `networkidle` may\nsimply never arrive. Waiting on it is indistinguishable from a hang, and it is\nwhat made the first live run appear to freeze. Every wait is for a concrete\nelement, timeouts default to 25s (`SPORTSINC_WEB_TIMEOUT_MS`), and every step is\nrecorded with elapsed milliseconds in the `trace` that `capture-portal` returns —\nso a slow run reports where it is instead of going quiet. A selftest guards\nagainst `networkidle` being reintroduced.\n\n## Selector strategy\n\nGenerated ids are avoided where they are truly generated, but WebForms ids have\na useful property: `ctl00_ContentPlaceHolder1_grdInvoices` is\n`<framework prefix>_<developer's control name>`. The **suffix** is authored and\nstable; the prefix is the framework's. So controls are matched on the suffix —\n`table[id$='grdInvoices']`, `input[id$='_chkItem']`,\n`a[id$='lbDisplayDownload']` — which survives a page restructure that moves the\nprefix. Auth0's classes are per-build hashes (`c72825458`) and are never used;\nits ids (`#username`, `#password`) are.\n\nA row is matched on its **SI Doc No.** cell, never by position. Downloading the\nwrong invoice is the one failure reconciliation cannot catch: another document's\nlines are internally consistent and simply tie to a different header.\n\n---\n\n## Step 1 — Login ✅ confirmed live\n\nLoad `https://www.sportsinc.us/` and **click** the DEALER LOGIN button\n(`a[href*='login-redirect']` — its Wix classe"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2610,"uniquenessScore":38,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T06:25:36.608Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T06:25:36.608Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T08:46:38.681Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}