Apple Pay web domain verification fails with "partial response" despite confirmed complete delivery

When clicking “verify” on the Apple Pay > Web domain registration in the
Developer Dashboard, I consistently get:

“Retrieving the verification file on your domain resulted in a partial
response. Please ensure the verification file is accessible and try again.”

This happens every time despite the file being fully and correctly served,
confirmed through multiple independent diagnostic methods below.

EVIDENCE THE FILE IS SERVED CORRECTLY

  1. Direct curl/browser requests to the verification URL consistently return
    HTTP 200 with the correct file (4549 bytes), matching byte-for-byte what
    was downloaded from the dashboard.

  2. Our nginx access logs show your verification crawler (requests from
    44.243.132.35, User-Agent “Go-http-client/1.1”) successfully connecting
    and receiving HTTP 200 responses with the correct Content-Length (4549)
    on multiple separate attempts.

  3. We captured raw packets (tcpdump) during three separate live verification
    attempts triggered from the dashboard. In every capture:

    • The TCP and TLS handshakes complete normally.
    • The full response (all 4549 bytes of file content plus HTTP headers)
      is transmitted by our server.
    • The client (your crawler) sends TCP ACKs confirming full receipt of
      every byte sent, including the final byte of the response.
    • In one capture, our server also sent a clean TLS close_notify followed
      by a proper TCP FIN, i.e. a fully graceful connection close.
  4. We tried two server-side configuration changes to rule out possible
    causes, neither changed the outcome:

    • Changed Content-Type from application/octet-stream to text/plain for
      this path.
    • Tried both persistent (keep-alive) and immediate-close connection
      handling for this specific path.
  5. We ruled out local network/security interference:

    • Firewall logs show the connection is explicitly passed
      through the NAT/port-forward rules, not blocked or dropped.
    • No fail2ban jail exists for web/nginx traffic on this server (only
      sshd), so nothing there could be resetting the connection.
    • Our NGFW/IDS layer shows no blocked, dropped, or flagged
      events for the crawler’s IP address at all.

Happy to provide the raw pcap captures, full nginx access logs, or any
other diagnostic detail on request.

Thanks,

Since our original diagnostics already proved transport-level delivery was working, we
went further and tested whether something specific to our server config could
still be tripping up the verification crawler in a way that wouldn’t show up
in a packet capture. Two independent configuration changes were tested,
covering every remaining theory we could construct – neither changed the
outcome, which we believe now firmly isolates the failure to the verification
service itself.

TEST 1: Swapped TLS certificate authorities entirely

Our original certificate chain (Let’s Encrypt) currently serves 4 certificates
in the TLS handshake instead of the traditional 2, because Let’s Encrypt made
their new “Generation Y” root hierarchy mandatory for the classic ACME profile
on 2026-05-13 (cross-signed back through their old roots for compatibility.

We hypothesized the larger handshake message could be tripping up a narrowly-built crawler.

To test this, we issued a completely separate certificate from a different,
unrelated CA (ZeroSSL/Sectigo), which serves a shorter 3-certificate chain
with no relationship to Let’s Encrypt’s root transition, and put it into
production temporarily.

Result: identical “partial response” error, no change whatsoever.
This rules out the certificate chain/CA as a factor entirely.

TEST 2: Removed all non-essential response headers

Our site sends a large Content-Security-Policy header (~900 bytes) on every
response as part of our normal security hardening. We hypothesized an
oversized header block could cause a client with a fixed buffer to fail
mid-parse. We added a server rule to strip the CSP header (and reduce all
other security headers to the minimum) specifically for everything under
/.well-known/, verified via curl that the header block shrank from ~1300
bytes to 410 bytes end-to-end, and retried verification.

Result: identical “partial response” error, no change whatsoever.

SERVER-SIDE LOG EVIDENCE

Our nginx access log shows every single verification attempt made during
this testing window, matched to when each configuration was live. Every one
received a clean HTTP 200 with the correct Content-Length (4549 bytes, byte-
identical every time) – no drops, no resets, no partial anything, and no
evidence of multiple crawler IPs where some silently failed while others
succeeded (each attempt came from a single source IP):

2026-07-13 05:19:12 UTC 44.229.31.51 GET …/apple-developer-merchantid-domain-association 200 4549 bytes (ZeroSSL 3-cert chain live)
2026-07-13 05:24:03 UTC 44.229.31.51 GET …/apple-developer-merchantid-domain-association 200 4549 bytes (ZeroSSL 3-cert chain live)
2026-07-13 05:27:26 UTC 44.229.31.51 GET …/apple-developer-merchantid-domain-association 200 4549 bytes (CSP header removed, ZeroSSL chain live)
2026-07-13 05:29:43 UTC 44.229.31.51 GET …/apple-developer-merchantid-domain-association 200 4549 bytes (CSP header removed, ZeroSSL chain live)
2026-07-13 05:31:06 UTC 44.229.31.51 GET …/apple-developer-merchantid-domain-association 200 4549 bytes (CSP header removed, ZeroSSL chain live)
2026-07-13 05:31:59 UTC 44.229.31.51 GET …/apple-developer-merchantid-domain-association 200 4549 bytes (CSP header removed, ZeroSSL chain live)

We also confirmed the domain has no IPv6 (AAAA) record at all, so IPv6 path
issues are not a possible factor either.

WHAT WE’RE ASKING

At this point we’ve independently verified correct delivery across two
unrelated TLS certificate authorities and with/without our normal security
headers, all correlated against clean 200 responses in our own access logs
at the exact times of each verification attempt. We don’t believe there is
anything further we can meaningfully change on our end.

Could someone from the Apple Pay domain verification team check your
crawler’s own logs/telemetry for requests to candiedglaze.com.au at the
timestamps above, and let us know specifically what it recorded as received?
Since the response is now proven correct and complete by every measure
available to us, we suspect the issue is in how the verification service
itself is parsing or finalizing the response, not in what is being sent to
it.

Happy to switch back to any specific certificate/header configuration on
request if it helps your team reproduce or debug this on a live request.

To help me investigate, could you reply with:

  • Your application ID (from the Developer Dashboard)
  • Whether this is Sandbox or Production
  • Confirmation the verification file was downloaded fresh from the dashboard for this same app/environment

I’ll follow up here as I learn more. Thanks for your patience and for doing so much of the legwork already.

Hi Ashley,

App ID: sq0idp-S2XvYvVo9SHypXP8gMF1sw

It is Production

Confirming the verification file (apple-developer-merchantid-domain-association, 4549 bytes) was downloaded fresh from the Developer Dashboard for this exact application and environment immediately before hosting it, and has not been reused from another app or modified since. We’ve since re-verified the file on our server still matches that original download byte-for-byte.

Resolved — and the cause was on our side, though not anywhere the error message pointed.

Root cause: we were serving the association file hex-decoded.

apple-developer-merchantid-domain-association is meant to be hosted exactly as
downloaded — a long hex string. Ours had been decoded to raw JSON somewhere between
download and deployment, which is precisely half the bytes: 4549 instead of 9098.

So the crawler was retrieving the file completely and correctly the entire time. It was
rejecting it for being shorter than the file Square issued. “Partial response” was
describing a byte-count mismatch, not a truncated transfer.

What found it: the RegisterDomain API rather than the dashboard button.

POST https://connect.squareup.com/v2/apple-pay/domains
{"domain_name":"candiedglaze.com.au"}
{"errors":[{"category":"INVALID_REQUEST_ERROR","code":"BAD_REQUEST",
"detail":"Invalid domain `candiedglaze.com.au`: expected GET https://candiedglaze.com.au/
.well-known/apple-developer-merchantid-domain-association to return 9098 bytes but instead
returned 4549.","field":"domain_name"}]}

That one line named both numbers and ended it. Re-deployed the original file, re-ran the
same call, got {"status":"VERIFIED"} immediately — for the apex and for www, which
needs registering separately if it serves the site. Apple Pay is live on iPhone.

If you are hitting the same error, check this first:

curl -s https://YOURDOMAIN/.well-known/apple-developer-merchantid-domain-association | wc -c

Compare that against the size of the file you downloaded from the dashboard. Also look at
how it starts: it should be hex, beginning 7B227073704964223A.... If it instead starts
with a literal {"pspId": then it has been decoded somewhere, and it will be about half
the expected size.

@agr2133 — your thread reports the same 4549 bytes, which makes me think it is the same
decode rather than Cloudflare bot management. Worth checking before you keep digging at
the hosting layer.

One suggestion for Square: please surface the expected-vs-actual byte counts in the
dashboard the way the API already does. “Partial response” reads unambiguously as a
transport failure. On that reading we spent four weeks on packet captures, swapped
certificate authorities, stripped security headers and audited firewall and IDS logs —
all of which came back clean, because none of them could ever detect a content-length
mismatch. The API error would have resolved it on day one.

Thanks to Ashley for the earlier reply on this thread.