DPoP (Demonstrating Proof-of-Possession, RFC 9449) binds an access token to a private key that only your application holds. A token obtained with DPoP is useless to anyone who does not also hold your key, so a leaked token cannot be replayed. DPoP is optional. If you do not send a DPoP proof, you get an ordinary Bearer token, which is fine for most integrations.
Use DPoP when your security policy requires proof-of-possession — for example, when tokens travel across less-trusted networks.
- Your application holds an asymmetric key pair.
- With each request, you send a short-lived DPoP proof: a JWT signed with your private key. Its header carries the matching public key (JWK).
- Hyperproof ties the access token to the thumbprint of that key (
cnf.jkt). Each call after establishing the key pair must present a fresh proof signed by the same key.
Supported proof-signing algorithms: ES256, ES384, ES512, RS256, PS256 (ES256 is recommended). A proof is valid for 60 seconds, and each proof's jti may be used only once.
Header
| Field | Value |
|---|---|
typ | dpop+jwt |
alg | your signing algorithm, e.g. ES256 |
jwk | your public key (public members only) |
Claims
| Claim | Meaning |
|---|---|
htm | HTTP method of the request, e.g. POST |
htu | HTTP URL of the request (without query or fragment) |
iat | issued-at time (Unix seconds) |
jti | unique random id (replay protection) |
ath | base64url SHA-256 hash of the access token — required on API calls; omit it on the token request |
Send a proof for the token endpoint (htm=POST, htu=…/oauth/token, no ath) in the DPoP header of the token request. The response token_type comes back as DPoP.
# Python — using PyJWT (cryptography backend)
import jwt, time, uuid, hashlib, base64
from cryptography.hazmat.primitives.asymmetric import ec
private_key = ec.generate_private_key(ec.SECP256R1())
public_numbers = private_key.public_key().public_numbers()
def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
jwk = {"kty": "EC", "crv": "P-256",
"x": b64u(public_numbers.x.to_bytes(32, "big")),
"y": b64u(public_numbers.y.to_bytes(32, "big"))}
def dpop_proof(htm, htu, access_token=None):
claims = {"htm": htm, "htu": htu, "iat": int(time.time()), "jti": uuid.uuid4().hex}
if access_token:
claims["ath"] = b64u(hashlib.sha256(access_token.encode()).digest())
return jwt.encode(claims, private_key, algorithm="ES256",
headers={"typ": "dpop+jwt", "jwk": jwk})
token_url = "https://api.expent.ai/tprm-api/oauth/token"
# POST token_url with header DPoP: dpop_proof("POST", token_url)
# and form grant_type=client_credentials -> token_type "DPoP"On every request, send both the token (with the DPoP auth scheme, not Bearer) and a fresh proof that includes ath:
curl -X POST https://api.expent.ai/tprm-api/vendors/search \
-H "Authorization: DPoP ACCESS_TOKEN" \
-H "DPoP: <proof with htm=POST, htu=…/vendors/search, ath=SHA256(ACCESS_TOKEN)>" \
-H "Content-Type: application/json" -d '{}'A missing or invalid proof returns 401 with client_code auth.invalid_dpop.
Tip: In the interactive reference, every operation shows ready-to-run request samples in curl, Python, JavaScript, and more. The samples are generated automatically from the spec.