Skip to content
Stribog

Compliance

All writing

PCI DSS Self-Assessment: When the SAQ Path Closes

Which self-assessment questionnaire PCI DSS lets you use is an architecture test and a Visa volume test, and neither verdict is the merchant's to issue.

Stribog22 min read

Search the term and you get lists: each form, one paragraph, ending on "pick the one that matches your setup." Nobody picks. Two parties decide, on unrelated criteria, and the answer moves when a transaction count creeps up or an appliance lands in the wrong segment.

Who Decides Whether You May Self-Assess at All

Start with the disclaimer on the Council's own blog — the sentence the roundups skip. PCI SSC "does not define compliance requirements for any organization or set compliance validation responsibilities"; those requirements "are set by brands, acquirers, payment facilitators, etc., which are often referred to as compliance enforcing entities." The forms are instruments. Both the obligation to validate and the permission to self-assess come from elsewhere.

Naming that elsewhere precisely matters, because three PCI SSC documents use three phrases for it — "compliance enforcing entities" in the 30 January 2025 blog post, "the entity to which the SAQ will be submitted" in the October 2024 SAQ bulletin, and in FAQ 1473 the one naming the role:

Compliance-accepting entities (typically, payment brands and acquirers) are responsible for determining the PCI DSS validation and reporting methods of their merchants and service providers, including how compliance is to be evidenced–for example, whether via a Report on Compliance (ROC) or a Self-Assessment Questionnaire (SAQ).
PCI SSC FAQ 1473

The question is never which form reads most manageable, but which entity accepts your validation. Two consequences follow. First, a self-assessment starts with a conversation: the October 2024 bulletin says merchants "should confirm they meet all eligibility criteria for a particular SAQ before commencing their self-assessment", and are "encouraged to first contact the entity to which the SAQ will be submitted". Second, eligibility criteria do not travel. Asked whether they can guide requirement applicability in a Report on Compliance, FAQ 1331 answers that SAQs "should not be used as a 'guide'" for that "unless explicitly reviewed, discussed and agreed upon with the merchant's compliance accepting entity" — discouraged, with a route to agreement, not prohibited. Scope and evidence are a separate discipline; this is the gate before it.

The SAQ Family Is a Map of Architectures, Not a Difficulty Ladder

The forms are not tiers, and reading them as easy-to-hard is the root of most wrong answers. Each carries eligibility criteria describing a payment architecture, and FAQ 1443 states the constraint without wiggle room: "only system types defined in the eligibility criteria may be used in that environment", and environments "containing any other types of systems would not be eligible". Eligibility is a property of what you built, read off an inventory and a segmentation diagram.

One sourcing note, because the version is load-bearing. The October 2024 bulletin states that the v4.0 SAQs "will remain active until PCI DSS v4.0 is retired on 31 December 2024" — so the v4.0.1 forms are the current ones, and they sit behind the Council's document-library licence gate. The retired v4.0 PDFs of April 2022 are still publicly served and are what is quoted below — an indication of the architectures the criteria describe, not the form to submit. The bulletin names eligibility-criteria clarifications only in SAQs A, A-EP and C-VT, but that list is expressly non-exhaustive — it opens "including:" and closes by directing readers to "the Document Changes table at the beginning of each SAQ for specific details". Confirm every form below against its own v4.0.1 Document Changes table with your compliance-accepting entity.

Read off those v4.0 forms, the architectures are narrow. SAQ B: account data only via imprint machines or standalone dial-out terminals, stored on no computer system. SAQ C: an Internet-connected payment application system — a point-of-sale, typically — storing no electronic account data and connected to no other system in the merchant environment, a segmentation rule in eligibility clothing. SAQ P2PE: account data only via a validated PCI-listed P2PE solution, with merchants that "do not have access to clear-text account data on any computer system", every control in the P2PE Instruction Manual implemented. All three state they are not applicable to e-commerce channels or service providers. SAQ D for Merchants is the residual, for merchants that "do not meet the criteria for any other SAQ type".

SAQ B-IP is worth quoting, because its exclusion is what operators trip over. It covers merchants taking account data only via "standalone, PCI-listed approved PIN Transaction Security (PTS) point-of-interaction (POI) devices with an IP connection to the payment processor" — then: "merchants using SCRs or SCRPs are not eligible for this SAQ", SCR and SCRP being Secure Card Reader classifications. Being on the PTS list does not make a device a B-IP device; its classification does. Eligibility can hinge on a procurement choice.

The criteria are not a device budget. FAQ 1443 says the intent is not "to prohibit more than one of the permitted system types being on the same network zone, as long as the permitted systems are all isolated from other types of systems". A *kind* of system appearing breaks eligibility; a count rising does not — which makes default-deny network policy an eligibility control.

Gate one is settled by what the brand says you are, gate two by what your payment path is made of, and neither is answered by reading the forms in order of length. The e-commerce node is deliberately one box: the A and A-EP criteria changed in v4.0.1 and the current forms are not publicly served, so the honest output of a decision tree here is "go and read the current form with your acquirer".

If You Are a Service Provider, There Is Exactly One SAQ

Settle the role before any architecture question; it is the gate that closes fastest. FAQ 1133 leaves nothing open: "SAQ D for Service Providers is the ONLY SAQ for SAQ-eligible service providers. All other SAQs are for merchant use only." The v4.0 form (April 2022) says the same of itself.

The clause that matters more sits immediately before it: the form "applies to all service providers defined by a payment brand as being eligible to complete a self-assessment questionnaire". SAQ-eligible is a status a brand confers, not a shape an architecture earns. A platform can be trivial — one embedded form, no account data on any system it runs — and still be told to produce a ROC.

So "the platform only embeds the processor's form, therefore SAQ A" is not an argument a service provider has. It has two destinations: SAQ D for Service Providers, if a brand has said it may self-assess at all, or an assessor. No lighter form exists to chase, so the effort belongs in making the SAQ D evidence cheap, as with owning your SOC 2 evidence.

The determination is not self-certified, and it is worth asking rather than answering: a product that never sees a primary account number but serves the page collecting one is where assumption and answer diverge. Record the role, who agreed it and the date, beside the level. Concentration risk in a single vendor is the same question pointed the other way.

The Iframe Boundary: SAQ A Eligibility Is Now a Scripts Problem

Two changes to SAQ A sit on the record from different dates, and running them together makes the Council look self-contradictory. The October 2024 bulletin lists "Adding a requirement to SAQ A and removing a requirement from SAQ C". Then on 30 January 2025 PCI SSC announced SAQ A r1: removal of Requirements 6.4.3 and 11.6.1 "for payment page security" and 12.3.1 "for a Targeted Risk Analysis", and in their place an eligibility criterion that merchants "confirm their site is not susceptible to attacks from scripts that could affect the merchant's e-commerce system(s)".

Read that as PCI DSS dropping payment-page script controls and you have made the most expensive error this subject offers. The same post says the opposite: v4.0.1 Requirements 6.4.3, 11.6.1 and 12.3.1 "become effective as of 31 March 2025", and the SAQ A modifications "do not remove or diminish the underlying requirements within PCI DSS." What moved is where the claim gets made — out of a reporting line, into an eligibility condition. A failed requirement produces a finding; a failed criterion removes the form.

Which pages the criterion reaches is a question for FAQ 1588, not for inference. It "only applies to e-commerce merchants with a webpage that includes a TPSP's/payment processor's embedded payment page/form (for example, one or more inline frame(s) (iframes))", and does "not apply" where the page "redirects customers from the merchant's webpage to a TPSP/payment processor". Embedding and redirecting are not two styles of one integration; under SAQ A r1 they are different eligibility positions, and a front-end team can move you between them in a sprint.

FAQ 1588 gives two ways to satisfy it, with different engineering shapes. Either use "techniques such as, but not limited to, those detailed in PCI DSS Requirements 6.4.3 and 11.6.1 to protect the merchant's webpage from scripts targeting account data", deployable "by the merchant or a third party" — or obtain confirmation from the processor supplying the form that its solution "includes techniques that protect the merchant's payment page from script attacks." The second is a procurement artefact, not a control. Either way one question stays open — what is on the page today — and a static inventory answers it for the markup you ship, repeatably enough to schedule.

python
#!/usr/bin/env python3
"""payment_page_inventory.py — the script and frame origins your page serves.

Reads the checkout page's delivered markup and classifies every <script src>
and <iframe src> origin as merchant-owned, declared-processor, or unknown. A
source with no web origin (data:, javascript:, blob:) can be in neither declared
set, so it counts as unknown; an inline <script> block carries no src at all and
is counted on its own. Exits 1 on any unknown origin or any inline script, so it
belongs in a schedule rather than in a one-off audit.

BLIND SPOT, stated because the SAQ A r1 criterion turns on it: this sees only
what the server sends. A script injected at runtime by a tag manager, a consent
platform or a compromised first-party bundle never appears here. Pair it with a
headless capture of the loaded resource list and with CSP reporting.
"""

from __future__ import annotations

import json
import sys
import urllib.request
from datetime import datetime, timezone
from html.parser import HTMLParser
from urllib.parse import urljoin, urlsplit

# Declared here and in the determination record, nowhere else. Anything absent
# from both sets is unknown by definition — which is the whole point.
MERCHANT_ORIGINS = {"https://shop.example.com", "https://static.example.com"}
PROCESSOR_ORIGINS = {"https://pay.processor.example"}

TIMEOUT_SECONDS = 20


class SourceCollector(HTMLParser):
    """Collect script and iframe sources, inline script blocks included."""

    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.found: list[tuple[str, str]] = []
        self.inline_scripts = 0

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag not in ("script", "iframe"):
            return
        source = next((value for name, value in attrs if name == "src" and value), None)
        if source is not None:
            self.found.append((tag, source))
        elif tag == "script":
            # No src: an inline block, on the page the criterion is about.
            self.inline_scripts += 1


def origin_of(reference: str, base: str) -> str | None:
    parts = urlsplit(urljoin(base, reference))
    if parts.scheme not in ("http", "https") or not parts.netloc:
        return None
    return f"{parts.scheme}://{parts.netloc}"


def classify(origin: str) -> str:
    if origin in MERCHANT_ORIGINS:
        return "merchant"
    if origin in PROCESSOR_ORIGINS:
        return "declared-processor"
    return "unknown"


def fetch(page_url: str) -> str:
    request = urllib.request.Request(page_url, headers={"User-Agent": "saq-inventory/1"})
    with urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) as response:
        charset = response.headers.get_content_charset() or "utf-8"
        return response.read().decode(charset, "replace")


def inventory(markup: str, page_url: str) -> tuple[dict[str, dict[str, object]], int]:
    collector = SourceCollector()
    collector.feed(markup)
    elements: dict[str, set[str]] = {}
    for tag, reference in collector.found:
        origin = origin_of(reference, page_url)
        if origin is None:
            # data:, javascript: and blob: sources have no origin, so they can
            # never be in either declared set. Record them; do not skip them.
            scheme = urlsplit(reference).scheme or "no-scheme"
            origin = f"{scheme}: (no web origin)"
        # One origin often serves more than one element type. Record every one:
        # recording only the first makes the JSON wrong as evidence.
        elements.setdefault(origin, set()).add(tag)
    rows = {
        origin: {"elements": sorted(tags), "verdict": classify(origin)}
        for origin, tags in elements.items()
    }
    return rows, collector.inline_scripts


def main(page_url: str) -> int:
    rows, inline_scripts = inventory(fetch(page_url), page_url)
    unknown = sorted(o for o, row in rows.items() if row["verdict"] == "unknown")
    print(
        json.dumps(
            {
                "page": page_url,
                "observed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
                "scope": "statically served markup only",
                "origins": rows,
                "inline_scripts": inline_scripts,
                "unknown": unknown,
            },
            indent=2,
            sort_keys=True,
        )
    )
    return 1 if unknown or inline_scripts else 0


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("usage: payment_page_inventory.py https://shop.example.com/checkout")
    sys.exit(main(sys.argv[1]))
Standard library only, so the check has no supply chain of its own. The exit code is the point: an unknown origin on a payment page is a finding, not a log line. An inline block and a `data:` source fail it too — neither can be in a declared set. Substitute your own origins; the two sets are the declaration everything else is measured against.
nginx
# EXCERPT. Substitute your origin and your processor's, and nothing else.
location = /checkout {
    # nginx has no line continuation. Inside a quoted string the lexer
    # unescapes only a short set of pairs, so a backslash before a real newline
    # is neither a join nor a drop: both the backslash and the newline stay in
    # the value and the header goes out malformed. Build the value with
    # successive set directives instead, which also keeps one definition for
    # both headers.
    set $csp "default-src 'none'";
    set $csp "${csp}; script-src 'self' https://pay.processor.example";
    set $csp "${csp}; frame-src https://pay.processor.example";
    set $csp "${csp}; connect-src 'self' https://pay.processor.example";
    set $csp "${csp}; img-src 'self' data:; style-src 'self'; font-src 'self'";
    set $csp "${csp}; base-uri 'none'; object-src 'none'; frame-ancestors 'none'";
    set $csp "${csp}; form-action 'self' https://pay.processor.example";
    # report-uri is deprecated in favour of the Reporting API and still has the
    # broadest browser support. Send both if your endpoint speaks
    # Reporting-Endpoints.
    set $csp "${csp}; report-uri /csp-report";

    add_header Content-Security-Policy-Report-Only $csp always;

    # Promote to enforcing only once the report stream is quiet for a cycle.
    # Same $csp, so "quiet" means quiet against the policy you will enforce.
    # add_header Content-Security-Policy $csp always;
}
Excerpt — the payment-page location only, with the processor origin left as a placeholder; add these directives to the checkout location you already have rather than pasting a second one, since two `location = /checkout` blocks is a configuration error. Note what `add_header` does on the way in: an `add_header` anywhere in a block discards every `add_header` inherited from the enclosing `server`/`http` level, so a checkout location that previously had none will silently lose HSTS, `X-Content-Type-Options` and anything else set higher up — repeat those directives here. Report-only and enforcing carry the same directive set deliberately: a report-only policy stricter than the one you intend to enforce buries the script findings under violations you would have allowed anyway, and the report stream never goes quiet. Deploy the report-only line first for a full billing cycle; its reports are the runtime inventory the static scan cannot build, and they arrive from real customer browsers rather than from your CI.

Volume Is the Other Gate, and Visa's Own Pages Disagree on Whose Volume Counts

Everything in this section is Visa's, and only Visa's. Mastercard, American Express, Discover and JCB each publish their own levels and thresholds, so a merchant accepting several brands can sit at a different level with each. Treat 6 million as a Visa figure, not an industry one.

Visa's regional compliance-validation page sets four merchant levels, and the channel counted changes as you descend them. Level 1 is "Merchants processing over 6 million Visa transactions annually (all channels) or Global merchants identified as Level 1 by any Visa region" — that second half is not decoration: a multi-region merchant identified as Level 1 anywhere is not doing arithmetic on its own volume, though a footnote carves out an escape: "[e]xceptions may apply to global merchants if no common infrastructure exists or if Visa data is not aggregated across borders; in such cases the merchant validates according to regional levels." Level 1 validates by an "Annual Report on Compliance (ROC) by Qualified Security Assessor (QSA)", a quarterly ASV scan and an AOC. Level 2 is 1 million to 6 million annually, all channels, on an annual SAQ; Level 3, 20,000 to 1 million Visa e-commerce transactions annually, likewise. Level 4 is "less than 20,000 Visa e-commerce transactions annually and all other merchants processing up to 1 million Visa transactions annually", where the SAQ is only "recommended" and "Compliance validation requirements set by acquirer".

Two rough edges are not yours to smooth. A card-present merchant at exactly 1 million reads as both Level 2 and Level 4 in Visa's rows as written; and Visa writes the service-provider boundary as "over 300,000" for Level 1 and "less than 300,000" for Level 2, leaving the boundary unassigned. Neither closes with an invented "at or above".

Now the aggregation rule — whose volume counts, as distinct from which channel — where most self-placements quietly go wrong. The same page assigns levels "based on Visa transaction volume over a 12-month period", aggregated "from a merchant Doing Business As (‘DBA')". Visa's corporate account information security page says something else: level identification "is based on the corporate entity's total volume of Visa transactions ... in one country or with one acquirer per year", with volume from "independently-owned and operated merchant locations (e.g., franchisee, licensee)" excludable "if it is not processed by the corporate entity." Both pages are first-party Visa and hand a franchise group different answers. Ask the acquirer in writing; record the answer with a date.

Service providers get two levels, and the top one is not purely a volume test either: "VisaNet processors or any service provider that stores, processes and/or transmits over 300,000 transactions per year" validates by an annual ROC by a QSA — so a VisaNet processor is Level 1 at any volume. The English row does not say *Visa* transactions; the Spanish sibling table on that page reads "más de 300,000 transacciones Visa por año." Under 300,000 is Level 2 on an annual SAQ, which can only be SAQ D for Service Providers. A Level 2 provider also "May choose to validate as a Level 1 service provider to be included in Visa's List of PCI DSS Compliant Service Providers" — which is how the SAQ path usually closes for a platform: commercially, in a procurement questionnaire. And one input is discretionary: a merchant that "has suffered a hack that resulted in an account data compromise may be escalated to a higher validation level." May.

The gate has one input you control — the count — and three you do not: the aggregation rule, the brand's thresholds, and whether a compromise gets escalated. Building the count so it is reproducible next year is the whole of the operator's side of this.

Determining Your Level in a File You Can Hand Over

PCI DSS requires neither artefact below. Both are a design this article proposes, for the reason any consequential claim gets written down: the determination is made once, by people who change teams, and relied on for a year by people who were not in the room.

yaml
# validation-level-determination.yaml
# NOT a PCI DSS artefact — the standard requires no such file. It exists so that
# next year's answer is a re-derivation and not somebody's memory.
#
# TEMPLATE. Every value below is illustrative: angle brackets mark what you
# supply, and the counts and dates are round sample figures, not measurements.
determination:
  effective: 2026-09-26
  review_by: 2027-09-26

  entity_role:
    assessed_as: merchant # merchant | service-provider
    decided_by: "<your acquirer>, ticket <acquirer-ticket-ref>"
    decided_on: "<date of that confirmation>"
    note: >-
      Asked, not assumed. The checkout page is merchant-operated; the
      processor's payment form is embedded in it as an iframe.

  per_brand:
    - brand: visa
      window: "<rolling 12-month window, e.g. 2025-09-01/2026-08-31>"
      # Sample figures. Yours come from the tripwire below, not from this file.
      # A single-channel e-commerce merchant, so the two counts are equal by
      # construction. A merchant that also takes card-present volume records the
      # two counts apart AND carries one eligibility entry per channel below:
      # one form does not cover two architectures.
      counts:
        all_channels: 780000
        ecommerce: 780000
      # Two separate things, named separately on purpose. The aggregation rule
      # is WHOSE volume counts; the counted channel is WHICH transactions do.
      # Both are the acquirer's to confirm, and conflating them is how a
      # self-placement lands a level off.
      aggregation_rule: corporate-entity-one-acquirer
      aggregation_rule_source: >-
        Recorded, not inferred. Visa publishes two readings — DBA aggregation on
        the regional compliance-validation page, the corporate entity in one
        country or with one acquirer on the corporate account information
        security page. The acquirer ticket above confirms which applies here and
        that franchisee volume is excluded.
      excluded_volume:
        independently_operated_storefronts: 250000
        basis: >-
          Franchisee-run storefronts on their own merchant accounts, so not
          processed by the corporate entity. Excluded under the aggregation rule
          the acquirer confirmed above, not under the other published reading.
      counted_channel: ecommerce
      threshold_applied: "Level 3: 20,000 to 1,000,000 Visa e-commerce transactions annually"
      level: 3
      required_artefact: annual SAQ + AOC, quarterly ASV scan
    - brand: other-brands-accepted
      status: not-determined
      note: >-
        Open item, tracked. Each brand publishes its own levels and thresholds;
        the Visa level above generalises to none of them.

  eligibility:
    saq: A r1
    basis: >-
      Embedded payment form, so the SAQ A r1 script eligibility criterion
      applies (PCI SSC FAQ 1588). Satisfied by the second of the two routes that
      FAQ allows: written confirmation from the processor that its embedded-form
      solution includes script protections when implemented per its instructions.
    processor_confirmation: "evidence/processor-script-confirmation-<date>.pdf"
    static_inventory: "evidence/payment-page-inventory-<date>.json"
    runtime_inventory: "evidence/csp-reports-<month>.jsonl"
    script_criterion_check: >-
      The checkout page's script and frame origins match the two declared sets
      above — no unknown origin, no inline script — reviewed on <date> against
      static_inventory and runtime_inventory (FAQ 1588). No card-present
      channel, so no POI-device or other system-type taxonomy question arises
      here; a mixed-channel record adds that check under the form whose criteria
      name those system types (FAQ 1443).

  confirmed_by:
    compliance_accepting_entity: "<your acquirer>"
    reference: "<acquirer-ticket-ref>"
    on: "<date of that confirmation>"
A blank template with illustrative values, not a record of anything. Deliberately a single-channel e-commerce merchant: eligibility is a per-architecture property, so a mixed-channel entity needs an eligibility entry per channel rather than one `saq:` line. The fields that earn their place are the ones nobody remembers twelve months later: which aggregation rule applied, which channel was counted, who confirmed both, what volume was excluded and on what basis, and which of FAQ 1588's two routes satisfied the script criterion. The open item on the other brands is deliberately in the file rather than out of it.

That record fixes last year's answer and does nothing about the count moving: monthly, while the validation cycle is annual — precisely the interval in which a business grows into a Report on Compliance without noticing.

bash
#!/usr/bin/env bash
# validation-level-tripwire.sh — recompute the rolling 12-month per-brand
# transaction count from settlement exports and fail while a threshold crossing
# is still months away rather than after an acquirer letter arrives.
#
# Not a PCI DSS artefact either. It exists so the crossing is detected by the
# operator, on a schedule the operator sets.
set -euo pipefail

EXPORT_DIR="${1:?usage: $0 <settlement-export-dir> [thresholds.json]}"
THRESHOLDS="${2:-thresholds.json}"
WARN_AT_PERCENT="${WARN_AT_PERCENT:-85}"

# GNU date; use gdate on macOS. A rolling window, because a fiscal-year total
# answers a different question than the one the brand asked.
since="$(date -u -d '12 months ago' +%Y-%m-%d)"

# One NDJSON file per month, one object per settled transaction, carrying at
# least {"brand": "...", "channel": "...", "settled_at": "YYYY-MM-DD"}.
#
# Measuring nothing is the likeliest operating mistake with this script: a wrong
# directory, exports rotated elsewhere, a volume that did not mount, .jsonl where
# .ndjson was expected. Each of those produces an empty input, an empty result
# and — without the two guards below — a green run. Zero measured transactions is
# a failed measurement, not a clean one, so count the inputs and fail on zero.
export_files="$(find "$EXPORT_DIR" -name '*.ndjson' -type f | wc -l | tr -d ' ')"
if [ "$export_files" -eq 0 ]; then
  echo "TRIPWIRE: no *.ndjson settlement exports under $EXPORT_DIR; measurement did not run." >&2
  exit 1
fi

in_window="$(
  find "$EXPORT_DIR" -name '*.ndjson' -type f -print0 \
    | xargs -0 cat \
    | jq -s --arg since "$since" '[ .[] | select(.settled_at >= $since) ]'
)"
records="$(jq 'length' <<<"$in_window")"
if [ "$records" -eq 0 ]; then
  echo "TRIPWIRE: $export_files export file(s) read, 0 settled records on or after $since; measurement did not run." >&2
  exit 1
fi

counts="$(
  jq 'group_by(.brand)
      | map({
          brand: .[0].brand,
          all_channels: length,
          ecommerce: ([ .[] | select(.channel == "ecommerce") ] | length)
        })' <<<"$in_window"
)"

# thresholds.json records only the boundary the acquirer confirmed applies to
# you, the channel it is counted on, and what crossing it actually costs, per
# brand. Arm the boundary that changes the deliverable. For the Level 3 merchant
# in the record above, crossing 1,000,000 e-commerce moves it to Level 2, which
# validates on the same set Level 3 does — a different level, the same artefact —
# so the boundary worth an alert is 6,000,000 all channels, where the artefact
# becomes a QSA's ROC. Transcribe that consequence from the brand's own table:
# getting the field wrong is how a tripwire fires and gets ignored.
#   [{"brand":"visa","counted_channel":"all_channels","boundary":6000000,
#     "next_level":"1","artefact":"annual ROC by QSA + AOC, quarterly ASV scan"}]
jq --slurpfile thresholds "$THRESHOLDS" --argjson warn "$WARN_AT_PERCENT" \
   --argjson files "$export_files" --argjson records "$records" '
      ( [ .[].brand ] ) as $observed_brands
      | ( [ $thresholds[0][].brand ] ) as $configured_brands
      | [ .[] as $c
          | ($thresholds[0][] | select(.brand == $c.brand)) as $t
          | {
              brand: $c.brand,
              counted_channel: $t.counted_channel,
              observed: $c[$t.counted_channel],
              boundary: $t.boundary,
              percent: (($c[$t.counted_channel] / $t.boundary) * 100 | floor),
              next_level: $t.next_level,
              artefact: $t.artefact
            }
        ] as $rows
      # A brand you settle but never configured is a finding in its own right:
      # an undetermined level, not a clean run. Surface it rather than drop it.
      # export_files and records_counted travel with the verdict: an artefact
      # that does not say what it counted cannot be audited a year later.
      | { measured_at: (now | todate),
          export_files: $files, records_counted: $records,
          rows: $rows,
          unconfigured_brands: ($observed_brands - $configured_brands),
          breached: [ $rows[] | select(.percent >= $warn) ] }
    ' <<<"$counts" > ./validation-level-tripwire.json

if ! jq -e '.unconfigured_brands | length == 0' ./validation-level-tripwire.json > /dev/null; then
  echo "TRIPWIRE: settled volume on brands with no confirmed boundary on file." >&2
  jq -r '.unconfigured_brands[]' ./validation-level-tripwire.json >&2
  exit 1
fi

if ! jq -e '.rows | length > 0' ./validation-level-tripwire.json > /dev/null; then
  echo "TRIPWIRE: $records record(s) counted but no configured brand matched; nothing was measured against a boundary." >&2
  exit 1
fi

if ! jq -e '.breached | length == 0' ./validation-level-tripwire.json > /dev/null; then
  echo "TRIPWIRE: a per-brand count has reached ${WARN_AT_PERCENT}% of a confirmed boundary." >&2
  jq '.breached' ./validation-level-tripwire.json >&2
  exit 1
fi

echo "tripwire clear: $records settled record(s) from $export_files export file(s); every count is under ${WARN_AT_PERCENT}% of its confirmed boundary."
The zero-measurement guards are the load-bearing part: an empty export directory is the one failure mode that would otherwise print `tripwire clear`, and a green line from a run that counted nothing is worse than no tripwire at all. The thresholds file deliberately holds only the boundary, counted channel and consequence your acquirer confirmed, not a transcription of every published table — a copy of Visa's tables in your repository is a copy that goes stale silently. Warning at 85% is a lead time, not a magic number: at ordinary growth rates the remaining 15% is months of notice, enough to budget an assessment rather than discover one, and the right percentage comes from your own growth rate.

What gets submitted is brand mechanics, not Council guidance, and some of it is old: Visa's service-provider requirements sheet dated 15 July 2015, still hosted on usa.visa.com, has Level 1 providers not directly connected to Visa submit "an executed Attestation of Compliance (AOC), signed by both the Service Provider and the Qualified Security Assessor (QSA)", Visa reserving "the right to request the full Report on Compliance (ROC)".

How the Eligibility Claim Fails in Practice

Five collapses, each with the check that catches it cheaply — ordinary change outrunning the cycle that describes it.

  1. One extra system type in the zone. A jump host, print server or monitoring appliance lands in the POI segment and FAQ 1443 applies at once. *Check:* a scheduled diff of that zone's inventory against the system types your form names, failing on any addition.
  2. The device was PTS-approved but classified wrong. Procurement bought a reader classified SCR or SCRP, which the retired v4.0 B-IP form excludes by name — confirm the v4.0.1 form still does. *Check:* record the classification at purchase, not just the approval.
  3. The role was never settled. A platform self-identifies as a merchant, files a merchant SAQ, and a customer's security review asks why a service provider filed one. *Check:* the determination record's role field, with the entity's reference beside it.
  4. A redirect quietly became an embed. A front-end team swaps a redirect checkout for an inline frame to cut drop-off, moving the page into the SAQ A r1 criterion's scope per FAQ 1588. *Check:* the static inventory plus CSP reports, failing the deploy when the frame source set changes.
  5. The count crossed, or its aggregation rule was never agreed. Growth crossed a boundary the acquirer had named, or franchise volume was excluded on whichever Visa page got read first. *Check:* the monthly tripwire, plus written acquirer confirmation of the rule.

Exit Ramps: Keeping the Boundary Cheap to Re-Prove

Some architectural moves reduce the validation burden; some only relocate it. Moving from an embedded payment form to a redirect or a full outsource takes the page out of the SAQ A r1 criterion's scope, per FAQ 1588 — a real reduction. An embedded form keeps the conversion benefit and imports a criterion you re-prove every deploy. Both are defensible; only one is free.

Validated P2PE and brand incentive programmes are the other genuine reduction, with a verb worth reading closely. Visa's Technology Innovation Program "rewards eligible merchants by eliminating the requirement to verify compliance with the PCI DSS when at least 75% of yearly transactions originate through EMV chip-enabled terminals, a validated point-to-point encryption solution or integrated industry-standard tokenization solution meeting EMVCo Tokenization Specification." Eliminating the requirement to *verify*, not to comply — and only while you keep clearing 75%.

The optionality question underneath all of it: does eligibility depend on one processor's proprietary integration? If the criterion is satisfied by that processor's written confirmation, switching re-opens it — the exit cost includes re-proving eligibility. Keep the evidence in formats you own — a JSON inventory, CSP reports in a store you operate, a determination record beside the infrastructure it describes — and re-proving after a change is three files, shipped into a SIEM you operate to keep the history queryable.

The Long Game: an Eligibility Claim With a Date on It

Eligibility gets treated as a once-a-year question because that is how often somebody asks. It is a standing assertion about a live system, and systems change weekly. The version that survives a decade carries a date, an aggregation rule someone confirmed, an inventory that re-derives itself, and a tripwire that speaks before the acquirer does.

That posture outlasts the specifics here. Thresholds get revised, FAQs are edited in place without changing their URLs, forms renumbered — SAQ A r1 exists because the Council moved requirements into an eligibility criterion mid-version. The obligation keeps its shape: somebody else decides, on criteria they publish and occasionally contradict, and the operator proves which side of the line the system sat on, on a given date, from artefacts it still holds.

§FAQ/Common questions

Frequently asked

Which self-assessment questionnaire can a service provider use for PCI DSS?

Only one. PCI SSC FAQ 1133 states that "SAQ D for Service Providers is the ONLY SAQ for SAQ-eligible service providers. All other SAQs are for merchant use only." The v4.0 form itself (April 2022) repeats that it "is the ONLY SAQ option for service providers" — and, importantly, that it "applies to all service providers defined by a payment brand as being eligible to complete a self-assessment questionnaire." So a service provider does not choose between forms: it either has been defined as eligible to self-assess, in which case there is exactly one form, or it is validating through an assessor. Architectural simplicity does not open a lighter option, because SAQ A is a merchant form.

Do the SAQ A changes mean PCI DSS Requirements 6.4.3 and 11.6.1 no longer apply?

No, and PCI SSC says so explicitly. Its 30 January 2025 post announcing SAQ A r1 describes removing Requirements 6.4.3 and 11.6.1 for payment page security and 12.3.1 for the supporting Targeted Risk Analysis, and adding an eligibility criterion that merchants "confirm their site is not susceptible to attacks from scripts that could affect the merchant's e-commerce system(s)." The same post states that v4.0.1 Requirements 6.4.3, 11.6.1 and 12.3.1 "become effective as of 31 March 2025", and that the SAQ A modifications "do not remove or diminish the underlying requirements within PCI DSS." Only the reporting path moved — into an eligibility condition, where failing it removes the form rather than producing a finding.

Does a card data breach automatically make a merchant Visa Level 1?

Not automatically. Visa's regional compliance-validation page says that "Any merchant that has suffered a hack that resulted in an account data compromise may be escalated to a higher validation level." May be escalated, to a higher level — not shall be escalated, and not necessarily to Level 1. The decision sits with the brand and in practice arrives through the acquirer. Plan for the possibility rather than asserting the rule: the useful preparation is knowing which level you would be escalated to and what artefact that level requires, which is the same determination record a routine year needs.

How is transaction volume counted for a franchise group or a business with several DBAs?

Visa publishes two different answers and does not reconcile them. Its regional compliance-validation page says transaction volume "is based on the aggregate number of Visa transactions (inclusive of credit, debit and prepaid) from a merchant Doing Business As ('DBA')" over a 12-month period. Its corporate account information security page says level identification "is based on the corporate entity's total volume of Visa transactions ... meeting the transaction thresholds in one country or with one acquirer per year", and that volume from independently-owned and operated locations such as franchisees "may be excluded if it is not processed by the corporate entity." Both are first-party Visa. Treat the aggregation rule as a question to put to your acquirer in writing, record which rule was confirmed and when, and do not compute a level unilaterally on whichever reading favours you.

Can SAQ eligibility criteria be used to decide which PCI DSS requirements apply in a Report on Compliance?

Not without agreement. PCI SSC FAQ 1331 answers that SAQs "should not be used as a 'guide' for determining the applicability of PCI DSS requirements unless explicitly reviewed, discussed and agreed upon with the merchant's compliance accepting entity (e.g., payment brands and acquirers)." That is discouragement with a route to agreement, not a prohibition — and the same answer adds that merchants "should always consult with the organizations responsible for managing compliance programs to confirm their PCI DSS validation and reporting requirements." So a ROC's applicability set is a conversation with the assessor and the compliance-accepting entity, and borrowing an SAQ's shorter list into it is a decision to get in writing first.

self assessment questionnaire pci dsspci dss self-assessment questionnairesaq eligibility criteriasaq d for service providerspci dss validation levels service providervisa level 1 merchant report on compliance

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.