Clock sync over HTTPS

The unauthenticated shield-ntp endpoint: server time, client drift measurement, the two tolerance modes and where each one is actually enforced.

Last reviewed September 16, 2026Fresh

Overview

shield-ntp returns the server's authoritative timestamp over HTTPS so a client can measure its own clock offset and correct for it. Challenge freshness, token expiry and replay windows all compare a client timestamp against server time, so a device whose clock is minutes out fails operations that are otherwise valid.

Who can use it. Anyone. The endpoint is unauthenticated: clock sync is a prerequisite for authenticated work, so no API key or session is needed. It is on the public API allowlist, so it answers at https://api.passkeybridge.io/v1/shield-ntp.

Rate limit. 120 requests per minute per client IP, counted in memory inside one edge isolate. It is a burst guard rather than an exact quota: a request served by a different isolate is counted in a different bucket. Over the limit answers 429.

GET returns server time. POST takes your timestamp and returns the drift with a correction recommendation. Any other method answers 405 Method not allowed.

Server time comes from the database's clock_timestamp(), which runs on an NTP-disciplined host and is reported as stratum 2. When that read fails, the function falls back to the edge runtime clock and reports stratum 3 with source: "edge-runtime-fallback". The stratum number is a label describing which clock answered; PasskeyBridge does not measure its distance to a reference clock.

Server time with GET

curl https://api.passkeybridge.io/v1/shield-ntp
{
  "server_time": "2026-09-16T12:00:00.123Z",
  "server_epoch_ms": 1789646400123,
  "stratum": 2,
  "source": "postgres-clock-timestamp",
  "processing_ms": 4.21,
  "protocol": "NTP-over-HTTPS",
  "version": "1.0.0"
}
FieldTypeMeaning
server_timestringISO 8601 server timestamp
server_epoch_msnumberthe same instant as Unix epoch milliseconds; use this for arithmetic
stratumnumber2 for the database clock, 3 for the edge runtime fallback
sourcestringpostgres-clock-timestamp or edge-runtime-fallback
processing_msnumberserver-side handling time for this request, to two decimals
protocolstringalways NTP-over-HTTPS
versionstringpayload version, currently 1.0.0

Your offset is server_epoch_ms - Date.now(). This is a one-way measurement: it includes network transit, so it is an upper bound on how far your clock is behind and a lower bound on how far it is ahead. For a symmetric estimate, take two samples and discard the slower round trip.

Drift measurement with POST

POST compares your timestamp against server time and tells you what to apply.

curl -X POST https://api.passkeybridge.io/v1/shield-ntp -H "Content-Type: application/json" -d '{"client_epoch_ms":1789646408323,"mode":"strict"}'
FieldTypeRequiredNotes
client_epoch_msnumberone of the twoepoch milliseconds; preferred, no parsing loss
client_timestringone of the twoISO 8601, used only when client_epoch_ms is absent
modestringnodefault or strict; anything else is treated as default
{
  "server_time": "2026-09-16T12:00:00.123Z",
  "server_epoch_ms": 1789646400123,
  "client_epoch_ms": 1789646408323,
  "drift_ms": 8200,
  "abs_drift_ms": 8200,
  "direction": "ahead",
  "tolerance_ms": 500,
  "mode": "strict",
  "within_tolerance": false,
  "stratum": 2,
  "source": "postgres-clock-timestamp",
  "processing_ms": 5.02,
  "recommendation": "Client clock is 8200ms ahead. Apply offset of -8200ms to subsequent requests."
}

drift_ms is signed: positive means the client is ahead of the server. direction is ahead, behind or synced.

StatusBodyCause
400{"error":"Provide client_time (ISO 8601) or client_epoch_ms"}neither field present
400{"error":"Invalid client timestamp"}client_time did not parse
400{"error":"Invalid JSON body"}body is not JSON
405{"error":"Method not allowed"}method other than GET or POST
413{"error":"Payload too large"}body over the small-JSON cap
429rate limit responseover 120 requests a minute from this address
500{"error":"Internal server error"}unhandled failure

Tolerance modes

mode selects which tolerance the response judges your drift against. It changes tolerance_ms, within_tolerance and the recommendation text, and nothing else: this endpoint never rejects a request for drift.

Modetolerance_msIn words
default5000five seconds, loose enough for mobile networks
strict500half a second

Where these tolerances actually bind is worth being exact about. The strict tolerance is enforced by spatial binding (shield-spatial-bind), which rejects a capture whose client timestamp is outside it. Replay protection elsewhere uses a five-minute window. DPoP proofs use their own windows inside shield-dpop rather than these values, as described in DPoP proof of possession. Signal ingest does not check clock drift at all.

So treat strict as a pre-flight check before a spatial binding, and default as a general health check for a device clock.

Applying the offset in a client

There is no clock sync in the wallet SDK. @passkeybridge/vc-wallet-sdk contains no NTP code and no endpoint returns a clock_drift error, so nothing corrects your clock for you. If your device clocks drift, sample this endpoint and apply the offset yourself.

let offsetMs = 0;

async function syncClock(): Promise<number> {
  const res = await fetch("https://api.passkeybridge.io/v1/shield-ntp");
  const t = await res.json();
  offsetMs = t.server_epoch_ms - Date.now();
  return offsetMs;
}

/** Server-aligned now(), for any timestamp you send us. */
function serverNow(): number {
  return Date.now() + offsetMs;
}

A practical shape: sync once at start-up, re-sync when a timestamped operation fails, and re-sync after the device wakes from sleep, which is when a clock jump is most likely. Keep the offset in memory rather than persisting it, since a persisted offset outlives the skew it was measured against.

Do not add the offset twice. Apply it where you generate a timestamp to send us, and leave your local display clock alone.

Related from the blog