Normalization & handwriting · Client API

Smart
Scorecards.

Normalize photographed CADDIE.100 scorecards, correct camera perspective, and extract supported handwriting without converting uncertain evidence into guessed values.

Choose an integration surface

One processor. Three ways to use it.

POST /normalizePrivate server request for perspective correction, design identity, canonical images, and registered semantic geometry.
POST /processPrivate server request for normalization, handwriting extraction, golf interpretation, and explicit review decisions.
Scorecards JSPrebuilt browser capture UI using an origin-restricted publishable key and the bounded scorecards:capture scope.
Credential boundaryHTTP processing uses private scorecards:process credentials. Browser capture uses publishable keys and never exposes the private endpoints.

The HTTP endpoints are synchronous and intended for trusted backends. Scorecards JS owns the browser capture experience and returns a deliberately smaller, browser-safe result. Both paths use the same authoritative processing contracts.

Printed identity and registered geometry

Let the page identify its own design.

Version 4 groups each complete four-marker page independently, normalizes that page from the full-resolution photograph, and only then reads its required S identity from the standardized crop. The validated identity selects immutable design geometry for the final canonical image. Camera rotation, crop, skew, and perspective are corrected per page.

The checksummed label beside the lower-left marker uses self-routing chunks. Strip S from a design chunk such as SM9H1, SM9H1GG, SM9H10GG, SM18F, or SM18B to obtain its required design code. The GG and M18 identities select exact front- or back-nine page geometry; clients do not infer crop percentages from the code. The open-ended M18 pages do not encode a course. Other designs may include an optional C course chunk such as CNJ282; E identifies the checksum chunk. Because each chunk declares its type, their order does not affect decoding. Normalization returns any course code but does not yet apply course data.

The generated design catalog contains SM18B SM18F SM9H1 SM9H10 SM9H10GG SM9H1GG . The production API accepts only identities in rollout scorecard-production-rollout-phase-9u@1: SM9H1 SM9H10GG SM9H1GG SM18F SM18B . Controlled previews remain available to local verification but are not production-routable. Catalog revision scorecard-design-catalog@3.

POST /api/v1/scorecards/normalize

Upload a file or provide a public URL.

Use a private licensed server credential with scorecards:process. A CADDIE.100 administrator enables the organization entitlement; an organization owner or admin then assigns the scope to a private key in the client dashboard. The same scope authorizes both normalization and processing. Publishable browser keys cannot receive it. See the credential security guide for the two-level permission model.

Send the private key through x-api-key or Authorization: Bearer from a trusted backend. Public Scorecards endpoints do not accept employee-session fallback. The internal playground is an employee diagnostic surface, not a client integration URL; it exercises the same production application handlers without changing which designs clients can use.

Only the image is required. courseId, teeBox, and identifier are optional metadata echoed back when supplied; normalization and handwriting processing do not depend on them.

Multipart accepts one JPEG, PNG, or WebP image up to 4,000,000 bytes inside a complete request of at most 4,250,000 bytes. If the file is larger, host it at a public HTTPS URL and use JSON ingestion.

URL ingestion permits up to 15 MiB and 40,000,000 decoded pixels. The service revalidates every redirect and address, allows at most three redirects, and applies one 15-second deadline to the complete download.

curl -X POST "https://caddie100.com/api/v1/scorecards/normalize" \
  -H "x-api-key: c100_live_YOUR_PRIVATE_KEY" \
  -F "identifier=your-round-42" \
  -F "image=@scorecard.jpg;type=image/jpeg"
curl -X POST "https://caddie100.com/api/v1/scorecards/normalize" \
  -H "x-api-key: c100_live_YOUR_PRIVATE_KEY" \
  -H "content-type: application/json" \
  -d '{
    "identifier": "your-round-42",
    "image": { "url": "https://cdn.example.com/scorecard.jpg" }
  }'

Omit caddie-scorecard-normalization-version for production version 4, which discovers and normalizes up to four independent pages from one photograph. Send 2 only for the frozen square-marker M9 response or 3 for the prior one-capture topology-neutral response. Version 4 returns every complete page even when other visible markers form only an incomplete page. Identity reading uses a full-resolution-derived page crop; a crop below the qualified source-detail floor is reported as a page issue and is never sent to the identity provider.

Separate browser project

Scorecards JS now has dedicated documentation.

The HTTP normalization and processing contracts remain on this page. Installation, browser-key setup, widget configuration, capture sources, callbacks, results, lifecycle, host security policy, and current SDK limitations now live in a focused Scorecards JS guide.

Scorecards JS documentationStart with installation and credentials, continue to the first capture, or review events and result handling.

Client launch checklist

Prove the permission boundary before live traffic.

  1. Confirm the client organization is active and has an owner or administrator.
  2. Confirm the organization policy includes scorecards:process.
  3. Create a narrowly scoped c100_test_ private key and store it in the integration backend's secret manager.
  4. Verify no-key, publishable-key, and browser-origin requests fail; then run one authorized normalization.
  5. Create a separate c100_live_ key only after test verification and store it independently.
  6. Assign owners for usage review, rate-limit changes, rotation, and emergency revocation.

Plaintext credentials are displayed only once. Never place a private key in a printable, browser bundle, browser storage, repository, support ticket, analytics event, or log.

Photograph the registered capture surface

Keep the whole page or spread—and every marker—in frame.

Use the topology named by the printed design and keep the phone approximately parallel to the paper. Ordinary rotation is corrected after upload, but steep perspective, clipped boundaries, deep fold shadows, and glare reduce usable marker evidence. The table below lists catalog-derived marker counts; it does not make a preview topology production-eligible.

TopologyMarkersFramingFold treatmentRetake when
single-page4 requiredPhotograph the complete page boundary with all corner whitespace visible. Keep every marker and the adjacent S identity label unobstructed.No fold is expected inside the capture surface; flatten curled paper without covering a marker.Retake when any edge or marker is clipped, the S label is unreadable, or glare/shadow crosses a marker or writable table.
horizontal-spread6 requiredPhotograph the complete left/right spread in one image. Center the camera over the spread and include all four outer markers plus both fold-safe markers.Keep the vertical fold visible but gently opened. Do not press, crop, or shadow the fold-safe markers beside the crease.Retake when either page edge is missing, a fold marker is hidden, the crease creates severe perspective, or glare/shadow obscures either page.
vertical-spread6 requiredPhotograph the complete top/bottom spread in one image. Center the camera between both halves and include all four outer markers plus both fold-safe markers.Keep the horizontal fold visible but gently opened. Preserve clearance around the markers above and below the crease.Retake when either half is cropped, a fold marker is hidden, rotation prevents the whole spread fitting cleanly, or glare/shadow obscures a marker.

Before submitting, confirm that marker blacks are not washed out by glare, marker whites are not lost in shadow, the unique index marker and its adjacent S label are legible, and no hand, clip, scorecard cover, or page edge overlaps a required marker. If in doubt, retake from slightly farther away. With response version 4, a photo containing one complete page and additional incomplete marker evidence returns the complete page with PARTIAL status and structured issues instead of discarding the usable result. Two complete booklet pages in one photograph are normalized separately, so fold or page warp does not force one transform across both pages.

POST /api/v1/scorecards/process · Authenticated client API

Extract values without hiding uncertainty.

The processing endpoint accepts the same file and public-HTTPS inputs as normalization. It additionally requires an Idempotency-Key containing 8–128 visible ASCII characters. Reuse the same key with the exact same metadata, image, and processing version to recover the stored result without another model call or usage unit; using that key with different input returns 409.

Processing version 3 is the default client contract. The service discovers up to four complete pages and keeps successful pages when another page cannot be processed. One page can succeed while another is returned as NORMALIZED_ONLY with a structured issue.

Public processing version 3 supports F9, B9 panels through production-registered processors: M9H1/front-nine-page, M9H1GG/front-nine-page, M9H10GG/back-nine-page, M18F/front-nine-page, and M18B/back-nine-page. It returns extracted fields, review reasons, supported supplemental form data, and scoring-ready canonical records. Crop geometry, marker evidence, model/provider details, cost and token usage, internal revisions, and debug artifacts are omitted from client responses. The M18 front page adds permissive Round Details transcription; the back page adds Round Summary and Penalty transcription. Blank or unreadable fields remain null. Other discovered designs remain explicit partial results until a production processor is registered. Pages are not automatically merged into one round because two sheets in one photo are ambiguous without a shared printed instance token.

The service handles image preparation, page normalization, extraction retries, and bounded provider work internally. Clients receive only the resulting scorecard data and review guidance. One completed logical photo request records one scorecards:process unit.

curl -X POST "https://caddie100.com/api/v1/scorecards/process" \
  -H "x-api-key: c100_live_YOUR_PRIVATE_KEY" \
  -H "Idempotency-Key: your-round-42-attempt-1" \
  -H "caddie-scorecard-processing-version: 3" \
  -F "identifier=your-round-42" \
  -F "image=@scorecard.jpg;type=image/jpeg"
curl -X POST "https://caddie100.com/api/v1/scorecards/process" \
  -H "x-api-key: c100_live_YOUR_PRIVATE_KEY" \
  -H "Idempotency-Key: your-round-42-attempt-1" \
  -H "caddie-scorecard-processing-version: 3" \
  -H "content-type: application/json" \
  -d '{
    "identifier": "your-round-42",
    "image": { "url": "https://cdn.example.com/scorecard.jpg" }
  }'

Every processed page returns field decisions with literal, normalized value, field-valid bestGuess, reason codes, and either ACCEPTED or NEEDS_REVIEW. A review field remains editable but is not empty when the service has a meaningful guess. A photo whose pages all process but contain review fields returns COMPLETE_WITH_REVIEW; PARTIAL is reserved for missing or normalized-only page results. The response also includes canonical round records that can be corrected and submitted to Scoring. Internal crops, transforms, marker evidence, provider attempts, prompts, model identifiers, usage/cost data, and debug artifacts are never returned to client API keys.

See Smart Scorecards scoring logic for the complete handwritten numeric, club, FIR, GIR, direction, and review-decision vocabulary.

{
  "schema": "caddie100-scorecard-processing@3",
  "requestId": "scn_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "courseId": "example-course",
  "teeBox": "Blue",
  "identifier": "your-round-42",
  "status": "COMPLETE_WITH_REVIEW",
  "pages": [
    {
      "pageInstanceId": "page-1",
      "status": "NEEDS_REVIEW",
      "design": {
        "designId": "M9H1GG",
        "panel": "F9"
      },
      "processing": {
        "status": "NEEDS_REVIEW",
        "fields": [
          {
            "row": "1",
            "field": "score",
            "decision": "ACCEPTED",
            "value": 4,
            "literal": "4",
            "bestGuess": 4,
            "reasonCodes": []
          }
        ],
        "summary": {
          "totalCells": 70,
          "acceptedCells": 65,
          "reviewCells": 5,
          "reviewRate": 0.0714
        }
      },
      "warnings": []
    }
  ],
  "roundDrafts": [
    {
      "draftId": "draft_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      "selectionPolicy": "best-available",
      "pageInstanceIds": [
        "page-1"
      ],
      "record": {
        "schema": "caddie100-scorecard-round-record@1",
        "holes": [
          {
            "holeNumber": 1,
            "score": 4
          }
        ]
      }
    }
  ],
  "summary": {
    "discoveredPages": 1,
    "processedPages": 1,
    "normalizedOnlyPages": 0,
    "acceptedCells": 65,
    "reviewCells": 5
  },
  "issues": [],
  "warnings": []
}

The initial six-photo evaluation auto-accepted 85.95% of cells and flagged 14.05% for review. That 14.05% is a review rate, not a 14.05% success rate. Review cells are withheld after retry and still require the client or a later workflow to resolve them. The 99.45% measurement is precision among auto-accepted cells, not the probability that an entire card is perfect. Card-level exact-match performance is not yet qualified. The fixtures represent only two physical cards and one writer, so clients should retain review handling while broader physical-card and writer sampling continues as post-release quality assurance.

Synchronous v2, v3, and v4 contracts

Receive verified temporary artifacts.

Version 4 is the default production strip-marker contract. It returns a pages array, separate artifacts for each normalized page, and unmatched-marker or unreadable-identity issues without failing complete pages. Version 2 remains the frozen square-marker M9 compatibility response, while version 3 retains the prior one-capture topology-neutral evidence contract. Every successful response returns the selected version in caddie-scorecard-normalization-version. source.persisted is always false.

url
Temporary signed HTTPS bearer URL. Treat it as sensitive.
contentType
image/png for the normalized image or image/jpeg for the marker diagnostic.
sizeBytes
Verified object size in bytes.
sha256
SHA-256 digest of the returned artifact bytes.
width / height
Pixel dimensions for this specific artifact.
expiresAt
Exact time at which signed download access ends.
retentionUntil
Earliest lifecycle-deletion eligibility, not a guaranteed deletion time.
{
  "schema": "caddie100-scorecard-normalization@2",
  "requestId": "scn_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "courseId": "example-course",
  "teeBox": "Blue",
  "identifier": "example-round-42",
  "source": {
    "method": "multipart",
    "persisted": false
  },
  "template": {
    "designId": "M9H1",
    "slug": "greens-one-sheet",
    "version": 4,
    "panel": "F9",
    "layoutRevision": "greens-one-sheet-scorecard@4",
    "fiducialFamily": "caddie-2x9-h10-page-family@1",
    "source": "fiducials"
  },
  "printedMetadata": {
    "text": "SM9H1-CNJ282-EF88",
    "courseToken": "NJ282"
  },
  "normalization": {
    "rotationDegrees": 0,
    "sourceWidth": 3072,
    "sourceHeight": 4080,
    "sourceCorners": {
      "topLeft": {
        "x": 612.25,
        "y": 498.5
      },
      "topRight": {
        "x": 2510.75,
        "y": 522.5
      },
      "bottomRight": {
        "x": 2491.5,
        "y": 3608.25
      },
      "bottomLeft": {
        "x": 590.5,
        "y": 3584.75
      }
    },
    "outputWidth": 1275,
    "outputHeight": 2063
  },
  "regions": {
    "coordinateSpace": {
      "origin": "top-left",
      "unit": "normalized-0-to-1",
      "canonicalPixels": {
        "width": 1275,
        "height": 2063
      }
    },
    "panel": "F9",
    "markerText": {
      "x1": 0.085882,
      "y1": 0.96,
      "x2": 0.552941,
      "y2": 0.992
    },
    "holes": [
      {
        "holeNumber": 1,
        "row": {
          "x1": 0.089412,
          "y1": 0.14742,
          "x2": 0.910588,
          "y2": 0.204025
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.14742,
            "x2": 0.171529,
            "y2": 0.204025
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.14742,
            "x2": 0.263912,
            "y2": 0.204025
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.14742,
            "x2": 0.356294,
            "y2": 0.204025
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.14742,
            "x2": 0.448676,
            "y2": 0.204025
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.14742,
            "x2": 0.571853,
            "y2": 0.204025
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.14742,
            "x2": 0.664235,
            "y2": 0.204025
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.14742,
            "x2": 0.787412,
            "y2": 0.204025
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.14742,
            "x2": 0.910588,
            "y2": 0.204025
          }
        }
      },
      {
        "holeNumber": 2,
        "row": {
          "x1": 0.089412,
          "y1": 0.204025,
          "x2": 0.910588,
          "y2": 0.26063
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.204025,
            "x2": 0.171529,
            "y2": 0.26063
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.204025,
            "x2": 0.263912,
            "y2": 0.26063
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.204025,
            "x2": 0.356294,
            "y2": 0.26063
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.204025,
            "x2": 0.448676,
            "y2": 0.26063
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.204025,
            "x2": 0.571853,
            "y2": 0.26063
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.204025,
            "x2": 0.664235,
            "y2": 0.26063
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.204025,
            "x2": 0.787412,
            "y2": 0.26063
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.204025,
            "x2": 0.910588,
            "y2": 0.26063
          }
        }
      },
      {
        "holeNumber": 3,
        "row": {
          "x1": 0.089412,
          "y1": 0.26063,
          "x2": 0.910588,
          "y2": 0.317235
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.26063,
            "x2": 0.171529,
            "y2": 0.317235
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.26063,
            "x2": 0.263912,
            "y2": 0.317235
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.26063,
            "x2": 0.356294,
            "y2": 0.317235
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.26063,
            "x2": 0.448676,
            "y2": 0.317235
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.26063,
            "x2": 0.571853,
            "y2": 0.317235
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.26063,
            "x2": 0.664235,
            "y2": 0.317235
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.26063,
            "x2": 0.787412,
            "y2": 0.317235
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.26063,
            "x2": 0.910588,
            "y2": 0.317235
          }
        }
      },
      {
        "holeNumber": 4,
        "row": {
          "x1": 0.089412,
          "y1": 0.317235,
          "x2": 0.910588,
          "y2": 0.37384
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.317235,
            "x2": 0.171529,
            "y2": 0.37384
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.317235,
            "x2": 0.263912,
            "y2": 0.37384
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.317235,
            "x2": 0.356294,
            "y2": 0.37384
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.317235,
            "x2": 0.448676,
            "y2": 0.37384
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.317235,
            "x2": 0.571853,
            "y2": 0.37384
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.317235,
            "x2": 0.664235,
            "y2": 0.37384
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.317235,
            "x2": 0.787412,
            "y2": 0.37384
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.317235,
            "x2": 0.910588,
            "y2": 0.37384
          }
        }
      },
      {
        "holeNumber": 5,
        "row": {
          "x1": 0.089412,
          "y1": 0.37384,
          "x2": 0.910588,
          "y2": 0.430445
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.37384,
            "x2": 0.171529,
            "y2": 0.430445
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.37384,
            "x2": 0.263912,
            "y2": 0.430445
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.37384,
            "x2": 0.356294,
            "y2": 0.430445
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.37384,
            "x2": 0.448676,
            "y2": 0.430445
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.37384,
            "x2": 0.571853,
            "y2": 0.430445
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.37384,
            "x2": 0.664235,
            "y2": 0.430445
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.37384,
            "x2": 0.787412,
            "y2": 0.430445
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.37384,
            "x2": 0.910588,
            "y2": 0.430445
          }
        }
      },
      {
        "holeNumber": 6,
        "row": {
          "x1": 0.089412,
          "y1": 0.430445,
          "x2": 0.910588,
          "y2": 0.48705
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.430445,
            "x2": 0.171529,
            "y2": 0.48705
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.430445,
            "x2": 0.263912,
            "y2": 0.48705
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.430445,
            "x2": 0.356294,
            "y2": 0.48705
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.430445,
            "x2": 0.448676,
            "y2": 0.48705
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.430445,
            "x2": 0.571853,
            "y2": 0.48705
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.430445,
            "x2": 0.664235,
            "y2": 0.48705
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.430445,
            "x2": 0.787412,
            "y2": 0.48705
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.430445,
            "x2": 0.910588,
            "y2": 0.48705
          }
        }
      },
      {
        "holeNumber": 7,
        "row": {
          "x1": 0.089412,
          "y1": 0.48705,
          "x2": 0.910588,
          "y2": 0.543654
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.48705,
            "x2": 0.171529,
            "y2": 0.543654
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.48705,
            "x2": 0.263912,
            "y2": 0.543654
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.48705,
            "x2": 0.356294,
            "y2": 0.543654
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.48705,
            "x2": 0.448676,
            "y2": 0.543654
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.48705,
            "x2": 0.571853,
            "y2": 0.543654
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.48705,
            "x2": 0.664235,
            "y2": 0.543654
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.48705,
            "x2": 0.787412,
            "y2": 0.543654
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.48705,
            "x2": 0.910588,
            "y2": 0.543654
          }
        }
      },
      {
        "holeNumber": 8,
        "row": {
          "x1": 0.089412,
          "y1": 0.543654,
          "x2": 0.910588,
          "y2": 0.600259
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.543654,
            "x2": 0.171529,
            "y2": 0.600259
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.543654,
            "x2": 0.263912,
            "y2": 0.600259
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.543654,
            "x2": 0.356294,
            "y2": 0.600259
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.543654,
            "x2": 0.448676,
            "y2": 0.600259
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.543654,
            "x2": 0.571853,
            "y2": 0.600259
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.543654,
            "x2": 0.664235,
            "y2": 0.600259
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.543654,
            "x2": 0.787412,
            "y2": 0.600259
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.543654,
            "x2": 0.910588,
            "y2": 0.600259
          }
        }
      },
      {
        "holeNumber": 9,
        "row": {
          "x1": 0.089412,
          "y1": 0.600259,
          "x2": 0.910588,
          "y2": 0.656864
        },
        "cells": {
          "hole": {
            "x1": 0.089412,
            "y1": 0.600259,
            "x2": 0.171529,
            "y2": 0.656864
          },
          "score": {
            "x1": 0.171529,
            "y1": 0.600259,
            "x2": 0.263912,
            "y2": 0.656864
          },
          "putts": {
            "x1": 0.263912,
            "y1": 0.600259,
            "x2": 0.356294,
            "y2": 0.656864
          },
          "teeFir": {
            "x1": 0.356294,
            "y1": 0.600259,
            "x2": 0.448676,
            "y2": 0.656864
          },
          "teeClub": {
            "x1": 0.448676,
            "y1": 0.600259,
            "x2": 0.571853,
            "y2": 0.656864
          },
          "approachGir": {
            "x1": 0.571853,
            "y1": 0.600259,
            "x2": 0.664235,
            "y2": 0.656864
          },
          "approachClub": {
            "x1": 0.664235,
            "y1": 0.600259,
            "x2": 0.787412,
            "y2": 0.656864
          },
          "approachDistance": {
            "x1": 0.787412,
            "y1": 0.600259,
            "x2": 0.910588,
            "y2": 0.656864
          }
        }
      }
    ],
    "totals": {
      "x1": 0.089412,
      "y1": 0.656864,
      "x2": 0.910588,
      "y2": 0.713469
    }
  },
  "artifacts": {
    "normalizedImage": {
      "url": "https://delivery.example.com/example/normalized.png?Expires=1789396200&Signature=redacted&Key-Pair-Id=EXAMPLE",
      "contentType": "image/png",
      "sizeBytes": 2310842,
      "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "width": 1275,
      "height": 2063,
      "expiresAt": "2026-09-14T14:30:00.000Z",
      "retentionUntil": "2026-09-15T00:00:00.000Z"
    },
    "debugImage": {
      "url": "https://delivery.example.com/example/debug.jpg?Expires=1789396200&Signature=redacted&Key-Pair-Id=EXAMPLE",
      "contentType": "image/jpeg",
      "sizeBytes": 481993,
      "sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      "width": 1656,
      "height": 2200,
      "expiresAt": "2026-09-14T14:30:00.000Z",
      "retentionUntil": "2026-09-15T00:00:00.000Z"
    }
  },
  "warnings": []
}
{
  "schema": "caddie100-scorecard-normalization@3",
  "requestId": "scn_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "courseId": "example-course",
  "teeBox": "Blue",
  "identifier": "example-round-42",
  "source": {
    "method": "multipart",
    "persisted": false
  },
  "template": {
    "designId": "M9H1",
    "slug": "greens-one-sheet",
    "version": 4,
    "panel": "F9",
    "layoutRevision": "greens-one-sheet-scorecard@4",
    "fiducialFamily": "caddie-2x9-h10-page-family@1",
    "source": "fiducials",
    "captureId": "front-nine-page",
    "topology": "single-page",
    "definitionHash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
    "revisionManifestHash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
  },
  "printedMetadata": {
    "text": "SM9H1-CNJ282-EF88",
    "courseToken": "NJ282",
    "designChunk": "SM9H1",
    "checksum": "F88",
    "correctionCount": 0
  },
  "normalization": {
    "rotationDegrees": 0,
    "sourceWidth": 3072,
    "sourceHeight": 4080,
    "sourceCorners": {
      "topLeft": {
        "x": 612.25,
        "y": 498.5
      },
      "topRight": {
        "x": 2510.75,
        "y": 522.5
      },
      "bottomRight": {
        "x": 2491.5,
        "y": 3608.25
      },
      "bottomLeft": {
        "x": 590.5,
        "y": 3584.75
      }
    },
    "outputWidth": 1275,
    "outputHeight": 2063,
    "sourceToCanonical": [
      0.6715,
      0.0047,
      -412.8,
      -0.0052,
      0.6679,
      -329.1,
      0,
      0,
      1
    ]
  },
  "regions": {
    "coordinateSpace": {
      "origin": "top-left",
      "unit": "normalized-0-to-1",
      "canonicalPixels": {
        "width": 1275,
        "height": 2063
      }
    },
    "captureId": "front-nine-page",
    "topology": "single-page",
    "pages": [
      {
        "pageId": "front-nine",
        "frame": {
          "x1": 0,
          "y1": 0,
          "x2": 1,
          "y2": 1
        }
      }
    ],
    "semanticRegions": []
  },
  "artifacts": {
    "normalizedImage": {
      "url": "https://delivery.example.com/example/normalized.png?Expires=1789396200&Signature=redacted&Key-Pair-Id=EXAMPLE",
      "contentType": "image/png",
      "sizeBytes": 2310842,
      "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "width": 1275,
      "height": 2063,
      "expiresAt": "2026-09-14T14:30:00.000Z",
      "retentionUntil": "2026-09-15T00:00:00.000Z"
    },
    "debugImage": {
      "url": "https://delivery.example.com/example/debug.jpg?Expires=1789396200&Signature=redacted&Key-Pair-Id=EXAMPLE",
      "contentType": "image/jpeg",
      "sizeBytes": 481993,
      "sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      "width": 1656,
      "height": 2200,
      "expiresAt": "2026-09-14T14:30:00.000Z",
      "retentionUntil": "2026-09-15T00:00:00.000Z"
    }
  },
  "warnings": [],
  "quality": {
    "confidence": 0.986,
    "band": "high",
    "detectedCandidates": 4,
    "decodedFiducials": 4,
    "matchedFiducials": 4,
    "reprojectionRmsPixels": 0.31,
    "reprojectionMaxPixels": 0.52,
    "markerEvidence": [
      {
        "name": "bottomLeft",
        "kind": "index-front",
        "sourceCenter": {
          "x": 590.5,
          "y": 3584.75
        },
        "bitErrors": 0,
        "contrast": 142.8,
        "pixelsPerModule": 11.6,
        "reprojectionErrorPixels": 0.21
      }
    ]
  }
}
{
  "schema": "caddie100-scorecard-normalization@4",
  "requestId": "scn_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "courseId": "example-course",
  "teeBox": "Blue",
  "identifier": "example-round-42",
  "status": "COMPLETE",
  "source": {
    "method": "multipart",
    "persisted": false
  },
  "pages": [
    {
      "pageInstanceId": "page-1",
      "status": "NORMALIZED",
      "template": {
        "designId": "M9H1",
        "slug": "greens-one-sheet",
        "version": 4,
        "panel": "F9",
        "layoutRevision": "greens-one-sheet-scorecard@4",
        "fiducialFamily": "caddie-2x9-h10-page-family@1",
        "source": "fiducials",
        "captureId": "front-nine-page",
        "topology": "single-page",
        "definitionHash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
        "revisionManifestHash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
      },
      "printedMetadata": {
        "text": "SM9H1-CNJ282-EF88",
        "courseToken": "NJ282",
        "designChunk": "SM9H1",
        "checksum": "F88",
        "correctionCount": 0
      },
      "normalization": {
        "rotationDegrees": 0,
        "sourceWidth": 3072,
        "sourceHeight": 4080,
        "sourceCorners": {
          "topLeft": {
            "x": 612.25,
            "y": 498.5
          },
          "topRight": {
            "x": 2510.75,
            "y": 522.5
          },
          "bottomRight": {
            "x": 2491.5,
            "y": 3608.25
          },
          "bottomLeft": {
            "x": 590.5,
            "y": 3584.75
          }
        },
        "outputWidth": 1275,
        "outputHeight": 2063,
        "sourceToCanonical": [
          0.6715,
          0.0047,
          -412.8,
          -0.0052,
          0.6679,
          -329.1,
          0,
          0,
          1
        ]
      },
      "regions": {
        "coordinateSpace": {
          "origin": "top-left",
          "unit": "normalized-0-to-1",
          "canonicalPixels": {
            "width": 1275,
            "height": 2063
          }
        },
        "captureId": "front-nine-page",
        "topology": "single-page",
        "pages": [
          {
            "pageId": "front-nine",
            "frame": {
              "x1": 0,
              "y1": 0,
              "x2": 1,
              "y2": 1
            }
          }
        ],
        "semanticRegions": []
      },
      "quality": {
        "confidence": 0.986,
        "band": "high",
        "detectedCandidates": 4,
        "decodedFiducials": 4,
        "matchedFiducials": 4,
        "reprojectionRmsPixels": 0.31,
        "reprojectionMaxPixels": 0.52,
        "markerEvidence": [
          {
            "name": "bottomLeft",
            "kind": "index-front",
            "sourceCenter": {
              "x": 590.5,
              "y": 3584.75
            },
            "bitErrors": 0,
            "contrast": 142.8,
            "pixelsPerModule": 11.6,
            "reprojectionErrorPixels": 0.21
          }
        ]
      },
      "artifacts": {
        "normalizedImage": {
          "url": "https://delivery.example.com/example/normalized.png?Expires=1789396200&Signature=redacted&Key-Pair-Id=EXAMPLE",
          "contentType": "image/png",
          "sizeBytes": 2310842,
          "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
          "width": 1275,
          "height": 2063,
          "expiresAt": "2026-09-14T14:30:00.000Z",
          "retentionUntil": "2026-09-15T00:00:00.000Z"
        },
        "debugImage": {
          "url": "https://delivery.example.com/example/debug.jpg?Expires=1789396200&Signature=redacted&Key-Pair-Id=EXAMPLE",
          "contentType": "image/jpeg",
          "sizeBytes": 481993,
          "sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
          "width": 1656,
          "height": 2200,
          "expiresAt": "2026-09-14T14:30:00.000Z",
          "retentionUntil": "2026-09-15T00:00:00.000Z"
        }
      },
      "warnings": []
    }
  ],
  "discovery": {
    "detectedCandidates": 4,
    "decodedFiducials": 4,
    "retainedFiducials": 4,
    "matchedFiducials": 4,
    "unmatchedFiducials": 0
  },
  "issues": [],
  "warnings": []
}

Temporary delivery

Download anything you want to keep.

The submitted source image is processed in memory and is never persisted. Only the normalized PNG and marker-diagnostic JPEG are stored. Their signed URLs remain accessible for no more than 24 hours. Raw provider responses are not retained by the production route.

Objects become eligible for lifecycle deletion after 1 day, rounded to the next UTC midnight under S3 day-based lifecycle rules. Physical deletion is asynchronous, so retentionUntil is not a promise that the object exists until that time or disappears exactly then.

Processing idempotency records—including the response needed for safe replay—have a 24-hour logical lifetime in DynamoDB. DynamoDB TTL deletion is asynchronous. Handwriting images are sent to OpenAI's Responses API with store: false, which disables response application-state storage; OpenAI's default abuse-monitoring logs may still retain customer content for up to 30 days unless the API project has approved Modified Abuse Monitoring or Zero Data Retention controls.

Signed URLs are bearer credentials: do not log, publish, or send them to analytics. The client owns permanent retention and should download and store any desired copy before expiresAt. This service does not refresh or re-sign URLs and does not maintain long-term scorecard storage.

No silent guesses

Unsafe or incomplete evidence fails explicitly.

Errors use application/problem+json with stable status and code fields. Marker or perspective uncertainty returns 422 rather than a guessed crop. Authentication and rate-limit errors use the shared API problem contract.

CodeHTTPRetryClient action
invalid-content-length400NoSend a valid Content-Length or let the HTTP client calculate it.
invalid-image-url400NoUse a public HTTPS hostname on the default port without credentials or a fragment.
invalid-json400NoCorrect the JSON syntax.
invalid-multipart400NoRebuild the multipart body and boundary.
invalid-request-body400ConditionalRetry only if transport interruption caused the unreadable stream.
invalid-scorecard-request400NoCorrect missing, ambiguous, or invalid fields.
image-decode-failed400NoSupply a complete JPEG, PNG, or WebP image.
image-fetch-failed502ConditionalConfirm the source is reachable for 15 seconds, then retry.
image-too-large413NoReduce the file size or decoded dimensions; use URL ingestion only for a multipart-size overage.
registration-markers-incomplete422After correctionNo complete page was recoverable. Retake with all four markers for at least one page visible, sharp, and unobstructed.
scorecard-capture-disabled422LaterThis capture or topology is not in the production support registry.
scorecard-capture-ambiguous422After correctionRetake the complete page or spread so its marker topology identifies one capture surface.
scorecard-artifact-signing-unavailable503YesRetry with backoff; no successful result was metered.
scorecard-artifact-storage-unavailable503YesRetry with backoff; partial derived objects are cleaned up.
scorecard-artifact-verification-failed503YesRetry with backoff; unverified derived objects are cleaned up.
scorecard-design-detection-unavailable503YesRetry with backoff; the printed design reader was unavailable.
scorecard-design-disabled422LaterThis registered design is not in the production support registry.
scorecard-design-contradictory422After correctionRetake the complete printed design; its label and visible marker topology disagree.
scorecard-design-unknown422NoUse a registered CADDIE.100 Smart Scorecard design.
scorecard-design-unreadable422After correctionRetake the photo with the printed S identity label beside the unique marker visible and sharp.
scorecard-identity-contradictory422After correctionCapture one complete scorecard surface without another scorecard or identity label in frame.
scorecard-index-marker-missing422After correctionRetake the photo with the unique identity marker and adjacent label visible.
scorecard-normalization-unavailable503YesRetry with backoff.
scorecard-normalization-version-incompatible406After correctionRequest version 3 with caddie-scorecard-normalization-version for a topology-neutral capture.
scorecard-perspective-invalid422After correctionRetake the photo flatter and with all four corners clearly separated.
scorecard-request-too-large413NoReduce the request or use URL ingestion for a large source file.
unsupported-content-type415NoUse application/json or multipart/form-data.
unsupported-image-type415NoConvert the source to JPEG, PNG, or WebP.
unsupported-normalization-version406After correctionRequest normalization response version 2, 3, or 4.
usage-meter-unavailable503YesRetry with backoff; derived artifacts from the failed attempt are cleaned up.

Processing-only failures

CodeHTTPRetryClient action
idempotency-key-conflict409NoUse the original input or choose a new key for a different logical request.
invalid-idempotency-key400After correctionSend 8–128 visible ASCII characters.
scorecard-extraction-failed502ConditionalRetry the same key; retake a clearer photo first when image quality caused the failure.
scorecard-idempotency-unavailable503YesRetry with the same key after backoff.
scorecard-processing-configuration-invalid503NoContact CADDIE.100 support; the server configuration is invalid.
scorecard-processing-deadline-exceeded504ConditionalRetry the same key only when instructed; the service prevents an unsafe provider replay.
scorecard-processing-in-progress409YesWait for Retry-After, then resend the same key and exact input.
scorecard-processing-outcome-unknown409NoDo not create a new key automatically; request human review or contact support.
scorecard-processing-panel-unsupported422After correctionUse an F9 panel for handwriting extraction or send this panel to /normalize for geometry only.
scorecard-processing-unavailable503YesRetry the same key with backoff.
scorecard-processing-version-incompatible406After correctionRequest processing response version 2 for M18F or M18B; other supported designs may use version 1 or 2.
scorecard-response-too-large503NoContact CADDIE.100 support; the safe replay record could not be stored.

Both endpoints are synchronous and polling-free. They do not create jobs, issue webhooks, or retain submitted originals. Processing adds stable idempotency, extraction, deadline, disabled-service, and unknown-outcome failures. Retry a metering failure with the same key. Never automatically retry an unknown provider outcome with a new key.

Compatibility history

Changelog.

2026-09-23 · Smart Minimal 18 client processingAdded public normalization and page-first processing for SM18F and SM18B, including their Round Details and Round Summary/Penalty components. The release uses explicit owner authorization; broader physical and handwriting-provider sampling remains post-release quality assurance.
2026-09-17 · Greens Guide client processingAdded public authenticated normalization and processing-v2 routing for SM9H1GG and SM9H10GG. Internal playground URLs remain employee-only diagnostics; design support is not employee-gated.
2026-09-16 · Photo-level processing v2Added server-side discovery, independent per-page normalization and design routing, partial-success handwriting results, a shared photo budget, and one-unit photo-level metering while retaining processing v1 as the default compatibility response.
2026-09-16 · Multi-page normalization v4Added independent page discovery for up to four pages, per-page artifacts, and partial-success metadata so complete pages remain usable when another page is clipped or otherwise incomplete.
2026-09-15 · Versioned topology contract and production rolloutFroze normalization @2 as the default M9 response, added negotiated @3 for topology-neutral evidence, and restricted production routing to hash-pinned stable identities with an accepted release basis. Preview and internal identities now fail closed.
2026-09-14 · Printed design detectionAdded self-routing S, C, and E chunks, required M9H1/M9H10 design codes, checksum-assisted OCR recovery, and the optional five-character course-layout code without applying course data.
2026-09-14 · Processing v1 private betaAdded caddie100-scorecard-processing@1 with durable idempotency, categorical review decisions, bounded processing, and one-unit logical-result metering.
2026-09-13 · Normalization v2Activated caddie100-scorecard-normalization@2 with signed artifact descriptors, layout greens-one-sheet-scorecard@4, and fiducial family caddie-4x4-h8@1.

Future public schema, recognized layout, template-registry, or fiducial-family changes will receive a dated entry here.