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.
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"
}| Field | Type | Meaning |
|---|---|---|
server_time | string | ISO 8601 server timestamp |
server_epoch_ms | number | the same instant as Unix epoch milliseconds; use this for arithmetic |
stratum | number | 2 for the database clock, 3 for the edge runtime fallback |
source | string | postgres-clock-timestamp or edge-runtime-fallback |
processing_ms | number | server-side handling time for this request, to two decimals |
protocol | string | always NTP-over-HTTPS |
version | string | payload 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"}'| Field | Type | Required | Notes |
|---|---|---|---|
client_epoch_ms | number | one of the two | epoch milliseconds; preferred, no parsing loss |
client_time | string | one of the two | ISO 8601, used only when client_epoch_ms is absent |
mode | string | no | default 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.
| Status | Body | Cause |
|---|---|---|
| 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 |
| 429 | rate limit response | over 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.
| Mode | tolerance_ms | In words |
|---|---|---|
default | 5000 | five seconds, loose enough for mobile networks |
strict | 500 | half 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.