messpunkt.io Partner API · DPoP
Preview V1 release candidate 1.0.0-rc.3 — production live since 30 September 2026; access per customer organisation on request: kontakt@messpunkt.io

DPoP — proof of possession

DPoP (RFC 9449) is supported and optional on the messpunkt.io Partner API. If you send a DPoP proof when you obtain the token, the token is bound to your key — a stolen token is then useless without the private key. Tokens obtained without a proof are plain Bearer tokens, which is why Swagger UI and curl work without extra setup. messpunkt.io can make DPoP mandatory for an individual registered ERP client on request.

What DPoP does in one paragraph

Your client generates an ECDSA (ES256) or RSA (RS256) key pair once and keeps it in memory or a secure store. On every API call, you sign a short-lived JWT — the DPoP proof — that contains the HTTP method, full URL, a unique jti, and the current timestamp. You send it in a DPoP header alongside the access token. The server checks the signature against the public-key thumbprint bound into the access token (cnf.jkt claim). If they match, the request is authentic.

Request shape

GET /v1/whoami HTTP/2
Host: api.sandbox.messpunkt.io
Authorization: DPoP eyJhbGciOiJSUzI1NiIs...   ← the access token
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZy...       ← the proof, a separate JWT

Proof contents

HeaderValue
typdpop+jwt
algES256 (recommended) or RS256
jwkThe public key in JWK form
ClaimValue
jtiRandom UUID — unique per request
htmRequest method, e.g. GET
htuRequest URL without query string, e.g. https://api.sandbox.messpunkt.io/v1/whoami
iatCurrent Unix time (seconds)
athBase64url(SHA-256(access_token)) — access-token hash

Working code

Same request, four languages. Pick your stack, copy, adapt.

// dotnet add package Microsoft.IdentityModel.JsonWebTokens
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

var ec  = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var key = new ECDsaSecurityKey(ec);
var jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(key);
jwk.Use = "sig"; jwk.Alg = "ES256";

string Proof(string method, string url, string accessToken)
{
    using var sha = SHA256.Create();
    var ath = Base64UrlEncoder.Encode(
        sha.ComputeHash(Encoding.UTF8.GetBytes(accessToken)));

    var descriptor = new SecurityTokenDescriptor {
        Claims = new Dictionary<string, object> {
            ["jti"] = Guid.NewGuid().ToString("N"),
            ["htm"] = method,
            ["htu"] = url,
            ["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            ["ath"] = ath,
        },
        SigningCredentials = new SigningCredentials(
            key, SecurityAlgorithms.EcdsaSha256),
        TokenType = "dpop+jwt",
        AdditionalHeaderClaims = new Dictionary<string, object> {
            ["jwk"] = new { kty = jwk.Kty, crv = jwk.Crv, x = jwk.X, y = jwk.Y }
        },
    };
    return new JsonWebTokenHandler().CreateToken(descriptor);
}

var method = HttpMethod.Get;
var url    = "https://api.sandbox.messpunkt.io/v1/whoami";
var accessToken = "...";  // from /oauth/token

var req = new HttpRequestMessage(method, url);
req.Headers.Add("Authorization", $"DPoP {accessToken}");
req.Headers.Add("DPoP", Proof(method.Method, url, accessToken));

var http = new HttpClient();
var res  = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// npm i jose
import { generateKeyPair, SignJWT, exportJWK, calculateJwkThumbprint } from 'jose';
import { createHash, randomUUID } from 'crypto';

const { privateKey, publicKey } = await generateKeyPair('ES256', { extractable: true });
const publicJwk = await exportJWK(publicKey);
publicJwk.alg = 'ES256';

async function dpopProof(method, url, accessToken) {
  const ath = createHash('sha256').update(accessToken).digest('base64url');

  return await new SignJWT({
      jti: randomUUID(),
      htm: method,
      htu: url,
      iat: Math.floor(Date.now() / 1000),
      ath,
    })
    .setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
    .sign(privateKey);
}

const accessToken = '...';  // from /oauth/token
const url    = 'https://api.sandbox.messpunkt.io/v1/whoami';
const proof  = await dpopProof('GET', url, accessToken);

const res = await fetch(url, {
  headers: {
    Authorization: `DPoP ${accessToken}`,
    DPoP: proof,
  },
});
console.log(await res.json());
# pip install authlib httpx
from authlib.jose import JsonWebKey, jwt
from cryptography.hazmat.primitives.asymmetric import ec
from hashlib import sha256
import base64, time, uuid, httpx

priv = ec.generate_private_key(ec.SECP256R1())
jwk_priv = JsonWebKey.import_key(
    priv.private_bytes_raw() if False else priv, {"kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig"})
jwk_pub  = jwk_priv.as_dict(is_private=False)

def dpop_proof(method: str, url: str, access_token: str) -> str:
    ath = base64.urlsafe_b64encode(sha256(access_token.encode()).digest()).rstrip(b"=").decode()
    header = {"typ": "dpop+jwt", "alg": "ES256", "jwk": jwk_pub}
    payload = {
        "jti": str(uuid.uuid4()),
        "htm": method,
        "htu": url,
        "iat": int(time.time()),
        "ath": ath,
    }
    return jwt.encode(header, payload, jwk_priv).decode()

url = "https://api.sandbox.messpunkt.io/v1/whoami"
access_token = "..."  # from /oauth/token

r = httpx.get(url, headers={
    "Authorization": f"DPoP {access_token}",
    "DPoP": dpop_proof("GET", url, access_token),
})
print(r.json())
# needs: openssl, jose-util (https://github.com/latchset/jose) or step-cli

# 1. one-time key generation
jose jwk gen -i '{"alg":"ES256"}' -o priv.jwk
jose jwk pub -i priv.jwk -o pub.jwk

# 2. per-request proof (swap ACCESS, URL, METHOD)
ACCESS="eyJhbGciOi..."
URL="https://api.sandbox.messpunkt.io/v1/whoami"
METHOD="GET"

ATH=$(printf "%s" "$ACCESS" | openssl dgst -binary -sha256 \
  | openssl base64 | tr -d '=' | tr '/+' '_-')

PAYLOAD=$(jq -c -n \
  --arg jti "$(uuidgen)" --arg htm "$METHOD" --arg htu "$URL" \
  --arg ath "$ATH" --argjson iat "$(date +%s)" \
  '{jti:$jti, htm:$htm, htu:$htu, iat:$iat, ath:$ath}')

HEADER=$(jq -c -n --slurpfile jwk pub.jwk \
  '{typ:"dpop+jwt", alg:"ES256", jwk:$jwk[0]}')

PROOF=$(jose jws sig -I <(echo "$PAYLOAD") -k priv.jwk \
  -s "$HEADER" -c -o -)

curl -H "Authorization: DPoP $ACCESS" -H "DPoP: $PROOF" "$URL"

Token-endpoint DPoP

Binding happens at /oauth/token: send a DPoP proof when you exchange the code or refresh token. The proof has the same shape minus ath, and the returned access token carries a cnf.jkt claim with your key's thumbprint (RFC 7638). From then on, every API request with that token must use Authorization: DPoP plus a valid proof signed with the same key; presenting a bound token as Bearer, or sending the DPoP scheme with an unbound token, returns 401. Use the same key for all refreshes of a session.

Common pitfalls

Next

Bruno, Swagger UI, and Scalar do not generate DPoP proofs out of the box. Use the sandbox's plain Bearer mode for interactive exploration, and implement DPoP for server-to-server integration. DPoP stays optional in V1; a per-client requirement is only enabled on your request.

See also: Error handling · Quickstart · Full API reference