Documentation
Get started with the Anchorify API
REST API
The Anchorify API is a simple REST JSON API. Authenticate with an
API key via the X-API-Key header.
Base URL
https://api.anchorify.cloud OpenAPI Specification
openapi.jsonQuickstart
The Anchorify API allows you to create tamper-proof evidence by anchoring hashes on public blockchains. Here's how to get started:
1. Get your API key
Sign up for an account and generate an API key from the console.
2. Hash your data locally
Generate a hash of your file or data. The original data never needs to leave your system.
# Generate SHA-256 hash (hex output)
openssl dgst -sha256 document.pdf
# SHA256(document.pdf)= e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# Use the hex string directly in the API 3. Submit the hash
Send the hex-encoded hash to our REST API to create a notarization. You'll receive a notarization ID for tracking.
curl -X POST https://api.anchorify.cloud/v1/notarizations \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"algorithm": "SHA256",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"name": "document.pdf"
}' import hashlib
import requests
with open("document.pdf", "rb") as f:
hash_hex = hashlib.sha256(f.read()).hexdigest()
resp = requests.post(
"https://api.anchorify.cloud/v1/notarizations",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"algorithm": "SHA256",
"hash": hash_hex,
"name": "document.pdf",
},
)
print(resp.json()["id"]) package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
f, _ := os.Open("document.pdf")
defer f.Close()
h := sha256.New()
io.Copy(h, f)
hashHex := hex.EncodeToString(h.Sum(nil))
body, _ := json.Marshal(map[string]string{
"algorithm": "SHA256",
"hash": hashHex,
"name": "document.pdf",
})
req, _ := http.NewRequest("POST",
"https://api.anchorify.cloud/v1/notarizations",
bytes.NewReader(body))
req.Header.Set("X-API-Key", "YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result["id"])
} {
"id": "019c0e1a-d891-7f1f-ab6e-5588c46c2a4b",
"algorithm": "SHA256",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"status": "pending",
"blockchain_proofs": [
{ "provider_name": "Algorand", "status": "pending", "created_at": "2026-01-30T08:52:46Z" },
{ "provider_name": "Base", "status": "pending", "created_at": "2026-01-30T08:52:46Z" },
{ "provider_name": "Polygon", "status": "pending", "created_at": "2026-01-30T08:52:46Z" }
],
"created_at": "2026-01-30T08:52:46Z",
"name": "document.pdf"
} 4. Poll for status
Check the proof status. Once anchored on the blockchain, you can export your signed proof document.
curl https://api.anchorify.cloud/v1/notarizations/019c0e1a-d891-7f1f-ab6e-5588c46c2a4b \
-H "X-API-Key: YOUR_API_KEY" resp = requests.get(
"https://api.anchorify.cloud/v1/notarizations/019c0e1a-d891-7f1f-ab6e-5588c46c2a4b",
headers={"X-API-Key": "YOUR_API_KEY"},
)
notarization = resp.json()
print(f"Status: {notarization['status']}") req, _ := http.NewRequest("GET",
"https://api.anchorify.cloud/v1/notarizations/019c0e1a-d891-7f1f-ab6e-5588c46c2a4b",
nil)
req.Header.Set("X-API-Key", "YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var notarization map[string]any
json.NewDecoder(resp.Body).Decode(¬arization)
fmt.Printf("Status: %s\n", notarization["status"]) {
"id": "019c0e1a-d891-7f1f-ab6e-5588c46c2a4b",
"algorithm": "SHA256",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"status": "completed",
"timestamp_proof": {
"status": "timestamped",
"tsa_provider": { "name": "FreeTSA" },
"data": "MIIB...",
"validation": {
"chain": "MIIDXj...",
"ocsp": "MIIBpg...",
"this_update": "2026-01-30T08:52:49Z",
"captured_at": "2026-01-30T08:52:51Z"
},
"timestamped_at": "2026-01-30T08:52:48Z",
"created_at": "2026-01-30T08:52:47Z"
},
"blockchain_proofs": [
{
"provider_name": "Polygon",
"transaction_id": "0xabc123...",
"explorer_url": "https://polygonscan.com/tx/0xabc123...",
"status": "anchored",
"anchored_at": "2026-01-30T08:53:32Z",
"created_at": "2026-01-30T08:52:47Z"
}
],
"merkle_proof": {
"hash_algorithm": "SHA256",
"leaf_index": 2,
"siblings": [{ "position": "left", "hash": "9f8e7d6c..." }]
},
"merkle_root": "a1b2c3d4...",
"accumulator": {
"merkle_root": "a1b2c3d4...",
"sealed_at": "2026-01-30T08:52:47Z"
},
"created_at": "2026-01-30T08:52:46Z",
"updated_at": "2026-01-30T08:53:32Z",
"notarized_at": "2026-01-30T08:53:32Z",
"name": "document.pdf"
} API reference
POST /v1/notarizations Create a notarization
Headers: X-API-Key (required),
Idempotency-Key (optional, see
idempotent retries)
Body: algorithm (required, SHA256
| SHA384 | SHA512),
hash (required, hex string),
name (optional),
qualified_timestamp (optional)
GET /v1/notarizations/{id} Get a notarization by ID
Path: id (required, UUID)
Quotas and spending limit
Every notarization counts in your organization's billing period, which follows the anniversary of your subscription. Each plan includes a quota of standard notarizations; Pro and Enterprise also include qualified (eIDAS) timestamps. A qualified notarization counts only in the qualified quota.
Beyond the included quota, each notarization is billed at your plan's overage price, and each qualified one pay as you go, within the spending limit your organization's owners and admins set on the console's billing page: one amount excluding VAT per billing period, from €0 to €1,000, for both. The limit is €0 by default: notarizations stop at the included quota until you set one.
A notarization the spending limit cannot cover is refused with 429 quota_exceeded, without Retry-After: the quota comes back with the next billing period, or as soon as the limit is raised.
Owners and admins receive an email at 80% and 100% of each included quota, then at 80% and 100% of the spending limit. A notarization that fails for good is not billed.
Qualified timestamps also depend on the stock we hold with the qualified timestamp authority. When it runs out, a qualified request is refused with 429 qualified_capacity_exhausted, while standard notarizations go on: retry the qualified one later.
Idempotent retries
A timeout or a network error leaves you unsure whether a notarization was created. Send an Idempotency-Key header with POST /v1/notarizations, and retry with the same key and the same body: the notarization is created, and billed, once.
curl -X POST https://api.anchorify.cloud/v1/notarizations \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0c2b8e-6d1a-4c3e-9b7a-2e4f8d1c6a90" \
-d '{
"algorithm": "SHA256",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"name": "document.pdf"
}' A retry within 24 hours of the first request creates nothing and is not counted again: it answers 201 with the notarization as it now stands, and the Idempotent-Replayed: true header.
HTTP/1.1 201 Created
Content-Type: application/json
Idempotent-Replayed: true The key is yours to choose: up to 255 visible ASCII characters, without spaces; a UUID works. Keys are scoped to your organization and kept 24 hours; after that, the same key creates a new notarization. Use a new key for each notarization you want.
The same key with a different body answers 422 idempotency_key_reused, and creates nothing.
Errors
An error answers a JSON body with a machine-readable code and a human-readable message. Handle the code: the message may change.
{
"error": {
"code": "quota_exceeded",
"message": "quota exceeded: the spending limit does not cover this notarization"
}
} | Status | Code | Meaning |
|---|---|---|
| 400 | invalid_argument | The request is invalid: its JSON, algorithm, hash or Idempotency-Key header. Fix it before retrying. |
| 401 | unauthenticated | The API key is missing or invalid. |
| 402 | subscription_inactive | Your organization has no subscription that grants notarization: none, pending, cancelled, unpaid, or past due for more than 7 days. Your data stays readable. |
| 403 | permission_denied | Your plan does not include what the request uses: SHA-384, SHA-512 or qualified timestamps. |
| 404 | not_found | GET /v1/notarizations/{id} only: the notarization does not exist, or belongs to another organization. |
| 422 | idempotency_key_reused | The Idempotency-Key was already used within 24 hours for a different request. Nothing was created: send a new key. |
| 429 | resource_exhausted | Your plan's rate limit is exceeded. Retry after the Retry-After seconds. |
| 429 | quota_exceeded | Your included quota is used, and your spending limit does not cover the notarization. No Retry-After: raise the limit, or wait for the next billing period. |
| 429 | qualified_capacity_exhausted | Qualified timestamps are unavailable for now. Standard notarizations still go through; retry the qualified one later. |
| 500 | internal | Unexpected error. Retry with the same Idempotency-Key: the notarization is not created twice. |
Webhooks
ProWebhooks let your application receive real-time HTTP POST notifications when notarization events occur. Configure endpoint URLs and subscribe to specific event types from the console. An endpoint URL must use HTTPS and point to a public address: loopback, private networks and cloud metadata services are refused.
Event types
Subscribe to the events relevant to your workflow. Each webhook delivery includes a full notarization snapshot with the current state of proofs.
| Event type | Description |
|---|---|
notarization.created | Notarization submitted and accepted. |
notarization.sealed | Accumulator sealed, Merkle proof assigned. Blockchain anchoring begins. |
notarization.timestamped | RFC 3161 timestamp obtained. The event_context includes timestamp_proof details. |
notarization.anchor_submitted | Blockchain transaction submitted for one provider. Fires per provider. |
notarization.anchored | Blockchain transaction confirmed on-chain for one provider. |
notarization.completed | Terminal success: both the RFC 3161 timestamp and the blockchain anchoring finished. The event_context includes completed_at. |
notarization.failed | Terminal failure: the timestamp or the blockchain anchoring could not complete. The event_context includes failure_reason. |
Terminal event guarantees
notarization.completed and notarization.failed are terminal, mutually exclusive, and delivered exactly once per notarization. You will never receive both for the same notarization, in either order.
- completed is only sent once both the timestamp and the blockchain anchoring have finished — never after just the first of the two.
- A notarization anchored by at least one blockchain provider completes, even if other providers of the same anchor profile failed. Check anchors[] for the per-provider outcome.
- If every provider fails permanently, the notarization is reported as failed — it has no blockchain proof.
- Delivery order is not guaranteed. Events are emitted in lifecycle order, but deliveries are independent HTTP requests: order your processing on the envelope created_at, not on arrival order.
Payload structure
Every webhook delivery sends a JSON envelope with a full notarization snapshot. The data.notarization object is self-contained — you do not need to call GET /v1/notarizations/{id}.
{
"id": "019c0e1a-e7a2-7f3b-8c1d-4a9b2e6f8d3c",
"type": "notarization.completed",
"api_version": "2026-09-01",
"created_at": "2026-01-30T08:53:32Z",
"organization_id": "019b7a3c-5d2e-7f1a-9b4c-3e8f1a2d5c7b",
"data": {
"notarization": {
"id": "019c0e1a-d891-7f1f-ab6e-5588c46c2a4b",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"algorithm": "SHA256",
"status": "completed",
"name": "document.pdf",
"created_at": "2026-01-30T08:52:46Z",
"updated_at": "2026-01-30T08:53:32Z",
"notarized_at": "2026-01-30T08:53:32Z",
"merkle_root": "a1b2c3d4e5f6...",
"timestamp_proof": {
"status": "timestamped",
"serial_number": "1A2B3C",
"tsa_provider": {
"name": "FreeTSA",
"cert_subject": "CN=FreeTSA,O=Free TSA,C=DE",
"cert_serial": "04B9A5",
"cert_ski": "A1B2C3D4E5F6...",
"policy_oid": "1.2.3.4.1"
},
"timestamped_at": "2026-01-30T08:52:48.123456Z",
"created_at": "2026-01-30T08:52:47.987654Z"
},
"blockchain_proofs": [
{
"provider_name": "Polygon",
"status": "anchored",
"transaction_id": "0xabc123...",
"explorer_url": "https://polygonscan.com/tx/0xabc123...",
"submitted_at": "2026-01-30T08:52:50Z",
"anchored_at": "2026-01-30T08:53:32Z",
"created_at": "2026-01-30T08:52:47.987654Z"
}
],
"accumulator": {
"merkle_root": "a1b2c3d4e5f6..."
}
},
"event_context": {
"completed_at": "2026-01-30T08:53:32Z"
}
}
} Note: The timestamp_proof.data field (the RFC 3161 ASN.1 token, base64-encoded) and timestamp_proof.validation (the material needed to verify that token years from now) are only included if the endpoint opts in via the include_timestamp_token setting — they share one flag because the validation material describes the token and is meaningless without it. The merkle_proof is always delivered.
HTTP headers
Every webhook request includes the following headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Anchorify-Webhooks/1.0 |
X-Anchorify-Signature | t=<unix>,v1=<hex> (HMAC-SHA256) |
X-Anchorify-Event-Type | Event type string |
X-Anchorify-Event-ID | Event UUID |
X-Anchorify-Delivery-ID | Delivery attempt UUID |
Signature verification
Every webhook is signed with HMAC-SHA256 using your endpoint secret. The signature header format is t=<timestamp>,v1=<hex_signature>, computed over <timestamp>.<raw_body>. We recommend verifying the signature and checking the timestamp freshness (5-minute tolerance).
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
# Check timestamp freshness (5 min tolerance)
if abs(time.time() - int(timestamp)) > 300:
raise ValueError("Timestamp expired")
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature) const crypto = require("crypto");
function verifyWebhook(payload, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=", 2))
);
// Check timestamp freshness (5 min tolerance)
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
throw new Error("Timestamp expired");
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${payload}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1)
);
} package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strings"
"time"
)
func verifyWebhook(payload []byte, header, secret string) error {
parts := make(map[string]string)
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(p, "=", 2)
parts[kv[0]] = kv[1]
}
// Check timestamp freshness (5 min tolerance)
ts, _ := time.Parse("", parts["t"])
if math.Abs(float64(time.Now().Unix()-ts.Unix())) > 300 {
return fmt.Errorf("timestamp expired")
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(parts["t"] + "."))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(parts["v1"])) {
return fmt.Errorf("invalid signature")
}
return nil
} Retry policy
Failed deliveries are retried with exponential backoff. Your endpoint must respond with a 2xx status within 30 seconds.
- Up to 5 attempts per delivery.
- Exponential backoff: min(30s × 2^attempt, 24h) + 0–5s jitter.
-
2xx— 2xx — delivery successful. -
429 / 5xx— 429 or 5xx — retried with backoff. -
410— 410 Gone — endpoint immediately disabled. -
4xx— Other 4xx — permanent failure, no retry. -
3xx— Redirects are not followed — permanent failure, no retry. Register the final URL. - Endpoints are automatically disabled after 10 consecutive failures.
Supported Hash Algorithms
Supported hash algorithms: SHA-256 (all plans), SHA-384 and SHA-512 (Pro and Enterprise). The hash must be provided as a hex-encoded string.
Async API
The API is asynchronous. Submit your hash and poll for status, or use webhooks (Pro) for notifications.
Signed Proof Documents
Pro and Enterprise plans can export signed proof documents containing all cryptographic evidence. Pro
Qualified timestamps
Pro and Enterprise plans can add eIDAS-qualified timestamps for enhanced legal standing. Pro