API reference
Timestamp a file’s hash, check a receipt, and fetch the public signing keys.
Momento signs a file’s SHA-256 hash and returns a timestamp receipt. Keep that receipt with your file so you can check it later. Your file stays on your device; only its hash is sent to Momento.
Before you start
Use the interactive OpenAPI reference to explore request schemas and try endpoints, or download the schema for your API client.
https://momento.mthatguy.workers.devNo API key is required. Requests and responses use JSON. For POST requests, set Content-Type: application/json. You can call the API from a browser; cross-origin requests are allowed.
| Method | Endpoint | What it does |
|---|---|---|
POST | /api/v2/stamp | Create a signed receipt for a hash |
POST | /api/v2/verify | Check a receipt against a hash |
GET | /api/v2/keys | Get the public signing keys |
GET | /api/health | Check whether the API responds |
GET | /api/ready | Check signing configuration and key self-test |
Create a timestamp
POST /api/v2/stampCompute your file’s SHA-256 hash locally, then send it to this endpoint. Momento adds the current server time and signs the receipt.
Request body
| Field | Type | Description |
|---|---|---|
hash | string · required | SHA-256 digest: exactly 64 lowercase hexadecimal characters (0–9, a–f). |
Send only hash. Extra fields, including a timestamp, are rejected. The body may be up to 1,024 bytes.
Request example
These examples timestamp the SHA-256 hash of an empty file. Replace it with your own file’s hash.
curl --fail-with-body https://momento.mthatguy.workers.dev/api/v2/stamp \
-H 'Content-Type: application/json' \
--data '{"hash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}' \
--output receipt.jsonThis saves the response to receipt.json for the verification example below.
On Windows Command Prompt, single quotes are not unwrapped: use double quotes around --data and escape the inner quotes (--data "{\"hash\":\"…\"}"). PowerShell and Bash run the command as shown.
Response
201 Created — the complete receipt. The ID and signature below are placeholders; use the values returned by your request.
{
"payload": {
"version": 2,
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"issuedAt": "2026-09-24T12:34:56.789Z",
"receiptId": "<base64url-encoded 16 random bytes>",
"keyId": "momento-v2-ml-dsa65"
},
"signature": "<base64url-encoded ML-DSA-65 signature>"
}| Field | Type | Description |
|---|---|---|
payload.version | number | Receipt format version. Currently 2. |
payload.hash | string | The SHA-256 hash you submitted. |
payload.issuedAt | string | Server time in UTC, in ISO 8601 format. |
payload.receiptId | string | Random ID. Repeated requests for the same hash create separate receipts. |
payload.keyId | string | Identifies the public key to use for verification. |
signature | string | ML-DSA-65 signature, encoded as base64url. |
The response uses Cache-Control: no-store.
Verify a receipt
POST /api/v2/verifyHash the file you want to check, then send that hash with its saved receipt. Momento checks the signature and confirms that the receipt belongs to the hash you supplied.
Request body
| Field | Type | Description |
|---|---|---|
hash | string · required | Your file’s SHA-256 digest, as 64 lowercase hexadecimal characters. |
receipt | object · required | The complete response from the stamp endpoint, including payload and signature. |
The body may be up to 8,192 bytes. Choose one receipt format and send only its documented fields.
Request example
Use the receipt.json saved above. This example uses jq to build the request body.
jq --arg hash 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' \
'{hash: $hash, receipt: .}' receipt.json |
curl --fail-with-body https://momento.mthatguy.workers.dev/api/v2/verify \
-H 'Content-Type: application/json' \
--data-binary @-Replace the example hash with a fresh hash of the file you are checking.
Response
{
"valid": true,
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"issuedAt": "2026-09-24T12:34:56.789Z",
"receiptId": "<receipt ID>",
"keyId": "momento-v2-ml-dsa65"
}The returned fields come from the verified receipt.
Check valid, not just the HTTP status
Both successful and failed verification return HTTP 200. Only accept the receipt when valid === true.
Get public keys
GET /api/v2/keysFetch the public keys used to verify receipts. Match the receipt’s payload.keyId to an entry in keys.
curl --fail-with-body https://momento.mthatguy.workers.dev/api/v2/keys{
"algorithm": "ML-DSA-65",
"keys": {
"momento-v2-ml-dsa65": "<base64url-encoded raw ML-DSA-65 public key>"
}
}The key shown above is a placeholder. Use the actual key returned by the endpoint, or a trusted copy you saved earlier for offline verification. The response also includes metadata indexed by key ID: status (active, retired, or compromised), createdAt (or null when unknown), a lowercase SHA-256 fingerprint of raw ML-DSA-65 key bytes, and fingerprintAlgorithm: sha256-ml-dsa65-raw. All older Ed25519 keys must be discarded; v1 receipts are rejected. Future ML-DSA-65 rotation can retain uncompromised v2 verification keys. See Protocol v2.
Check service health
GET /api/healthCheck whether the API is responding. This does not check signing configuration or rate-limit availability.
curl --fail-with-body https://momento.mthatguy.workers.dev/api/health{ "ok": true }Check signing readiness
GET /api/readyReturns 200 with { "ready": true } when the signing and verification keys pass an ML-DSA-65 self-test and the rate-limit bindings are configured. Otherwise returns 503 with { "ready": false }. Self-tests are cached up to 30 seconds per Worker isolate. This check does not consume request quota, test the rate-limit backend, or certify the server clock. Responses use Cache-Control: no-store.
The status page performs this live check. It does not report historical uptime.
Errors
Request errors use an error field. They are separate from a receipt that fails verification.
API responses include a generated X-Request-ID for support. Application request logs omit submitted hashes, request bodies, client IPs, and query strings.
{ "error": "invalid_hash" }| Status | Error | What to do |
|---|---|---|
400 | invalid_json | Send valid UTF-8 JSON. |
400 | invalid_hash | Use a 64-character lowercase SHA-256 digest. For stamp requests, send only hash. |
400 | invalid_request | Use one of the documented verification body formats without extra fields. |
400 | invalid_receipt_encoding | Encode the complete receipt as valid Base64 JSON. |
404 | not_found | Check the endpoint path and HTTP method. |
413 | request_too_large | Keep stamp bodies within 1,024 bytes and verify bodies within 8,192 bytes. |
415 | unsupported_media_type | Set Content-Type: application/json. |
429 | rate_limited | Wait before retrying. The response includes Retry-After: 60. |
503 | rate_limit_unavailable | Try again later; the service could not check its rate limits. |
503 | signing_not_configured, signing_key_mismatch | The service’s signing setup needs attention. |
500 | signing_failed | The service could not sign the receipt. Try again later. |
Rate limits
| Endpoint | Per client IP | Per Cloudflare location |
|---|---|---|
POST /api/v2/stamp | 30 requests/minute | 300 requests/minute |
POST /api/v2/verify | 120 requests/minute | 1,200 requests/minute |
If you receive 429, wait for the Retry-After interval before trying again. People sharing an IP address share the per-client limit. The application does not rate-limit the two GET endpoints.
These are abuse controls, not guaranteed quotas: Cloudflare’s counters are local to each location and eventually consistent. The hosting plan also has a daily request ceiling. See the rate limits guide for counting rules, retry behavior, and how to stay within them.
Verify offline
You can verify a saved receipt without calling this API. Use a trusted copy of the public key and the original file.
- Validate the receipt’s structure and version.
- Select the public key identified by
payload.keyId. - Decode the base64url signature and verify it with ML-DSA-65 over the UTF-8 bytes of the array below.
- Compute SHA-256 over the file’s raw bytes and compare its lowercase hexadecimal digest with
payload.hash.
JSON.stringify([
'Momento timestamp receipt v2 / ML-DSA-65',
payload.version,
payload.hash,
payload.issuedAt,
payload.receiptId,
payload.keyId,
]);The order matters. The signature covers this array, not the serialized receipt object. File names are not signed.
The browser tool and CLI use the shared protocol implementation in packages/protocol/src/index.ts for receipt validation and offline verification. Checking the signature authenticates the receipt; comparing the file hash ties it to your file. See Trust and limits for what a Momento timestamp proves.