Human Presence

Human Presence (HP) is a security feature that requires explicit human verification for certain transactions. It uses WebAuthn/FIDO2 to cryptographically prove a human authorized the action.

Overview

The Human Presence flow:

  1. Agent Request — Agent submits a transaction requiring HP
  2. HP Required — AutopayOS determines HP verification is needed
  3. WebAuthn Challenge — User's browser receives a cryptographic challenge
  4. Signed Response — User authenticates (Touch ID, Face ID, security key)
  5. HP Verified — Transaction proceeds with proof of human authorization

When HP is required

HP verification is triggered by policy rules:

TriggerExample
Amount thresholdTransactions over $500
New merchantFirst purchase from a merchant
High-risk categoryLuxury goods, cryptocurrency
Anomaly detectedUnusual spending pattern
Policy settingAlways require HP

Configure in your policy:

JSON
{
  "context": {
    "presence": "optional",
    "presence_threshold": 500.00,
    "presence_on_new_merchant": true
  }
}

Register a credential

Before using HP, users must register a WebAuthn credential (passkey).

Step 1: Begin registration

<tabs> <tab title="TypeScript">
TypeScript
// Request registration options from server
const response = await fetch('/ap2/hp/webauthn/register/begin', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentDid: 'did:key:z6Mk...',
    deviceName: 'MacBook Pro',
  }),
});

const options = await response.json();
</tab> <tab title="cURL">
Bash
curl https://api.autopayos.com/ap2/hp/webauthn/register/begin \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agentDid": "did:key:z6Mk...",
    "deviceName": "MacBook Pro"
  }'
</tab> </tabs>

Response:

JSON
{
  "publicKey": {
    "challenge": "base64-encoded-challenge",
    "rp": {
      "name": "AutopayOS",
      "id": "autopayos.com"
    },
    "user": {
      "id": "base64-user-id",
      "name": "[email protected]",
      "displayName": "User"
    },
    "pubKeyCredParams": [
      { "type": "public-key", "alg": -7 },
      { "type": "public-key", "alg": -257 }
    ],
    "authenticatorSelection": {
      "authenticatorAttachment": "platform",
      "userVerification": "required"
    },
    "timeout": 60000
  }
}

Step 2: Create credential

TypeScript
// Use browser WebAuthn API
const credential = await navigator.credentials.create({
  publicKey: {
    ...options.publicKey,
    challenge: base64ToArrayBuffer(options.publicKey.challenge),
    user: {
      ...options.publicKey.user,
      id: base64ToArrayBuffer(options.publicKey.user.id),
    },
  },
});

Step 3: Complete registration

TypeScript
const completeResponse = await fetch('/ap2/hp/webauthn/register/complete', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentDid: 'did:key:z6Mk...',
    credential: {
      id: credential.id,
      rawId: arrayBufferToBase64(credential.rawId),
      type: credential.type,
      response: {
        clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
        attestationObject: arrayBufferToBase64(credential.response.attestationObject),
      },
    },
  }),
});

const result = await completeResponse.json();
console.log('Credential registered:', result.credentialId);

Authenticate with HP

When HP is required, authenticate using the registered credential.

Step 1: Begin authentication

TypeScript
const response = await fetch('/ap2/hp/webauthn/auth/begin', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentDid: 'did:key:z6Mk...',
    intentId: 'intent_abc123', // Optional: link to intent
  }),
});

const options = await response.json();

Response:

JSON
{
  "publicKey": {
    "challenge": "base64-encoded-challenge",
    "rpId": "autopayos.com",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": "base64-credential-id",
        "transports": ["internal"]
      }
    ],
    "userVerification": "required",
    "timeout": 60000
  },
  "challengeId": "hp_xyz789"
}

Step 2: Get credential assertion

TypeScript
const assertion = await navigator.credentials.get({
  publicKey: {
    ...options.publicKey,
    challenge: base64ToArrayBuffer(options.publicKey.challenge),
    allowCredentials: options.publicKey.allowCredentials.map(cred => ({
      ...cred,
      id: base64ToArrayBuffer(cred.id),
    })),
  },
});

Step 3: Complete authentication

TypeScript
const completeResponse = await fetch('/ap2/hp/webauthn/auth/complete', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentDid: 'did:key:z6Mk...',
    challengeId: options.challengeId,
    credential: {
      id: assertion.id,
      rawId: arrayBufferToBase64(assertion.rawId),
      type: assertion.type,
      response: {
        clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
        authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
        signature: arrayBufferToBase64(assertion.response.signature),
        userHandle: assertion.response.userHandle 
          ? arrayBufferToBase64(assertion.response.userHandle)
          : null,
      },
    },
  }),
});

const hpProof = await completeResponse.json();

Response (HP Proof):

JSON
{
  "verified": true,
  "hpProof": {
    "type": "WebAuthnSignature2023",
    "agentDid": "did:key:z6Mk...",
    "challengeId": "hp_xyz789",
    "credentialId": "cred_abc123",
    "timestamp": "2025-12-17T10:00:00Z",
    "signature": "base64-signature"
  }
}

Use HP proof in payment flow

Include the HP proof when verifying a cart:

TypeScript
// Step 1: Request permission
const permission = await client.requestPermission({
  agentDid: 'did:key:z6Mk...',
  principalDid: 'did:key:z6Mp...',
  amount: 750.00, // High amount triggers HP
  currency: 'USD',
});

// Step 2: Check if HP required
if (permission.hpRequired) {
  console.log('Human presence required');
  
  // Get HP proof from user
  const hpProof = await getHumanPresenceProof(permission.hpChallenge);
  
  // Step 3: Verify cart with HP proof
  const verification = await client.verifyCart({
    intentVc: permission.intentVc,
    cartVc: merchantCartVc,
    hpProof: hpProof, // Include HP proof
  });
} else {
  // Normal flow without HP
  const verification = await client.verifyCart({
    intentVc: permission.intentVc,
    cartVc: merchantCartVc,
  });
}

HP proof structure

JSON
{
  "type": "WebAuthnSignature2023",
  "agentDid": "did:key:z6Mk...",
  "challengeId": "hp_xyz789",
  "credentialId": "cred_abc123",
  "timestamp": "2025-12-17T10:00:00Z",
  "freshnessSeconds": 300,
  "signature": "base64-signature",
  "authenticatorData": "base64-auth-data"
}
FieldDescription
typeProof type (WebAuthnSignature2023)
agentDidAgent the proof is for
challengeIdChallenge being responded to
credentialIdWebAuthn credential used
timestampWhen proof was created
freshnessSecondsHow long proof is valid
signatureCryptographic signature

Manage credentials

List credentials

TypeScript
const credentials = await client.listHpCredentials(agentDid);

for (const cred of credentials) {
  console.log(`${cred.deviceName}: ${cred.credentialId}`);
  console.log(`  Created: ${cred.createdAt}`);
  console.log(`  Last used: ${cred.lastUsedAt}`);
}

Revoke credential

TypeScript
await client.revokeHpCredential(agentDid, credentialId);

React component example

TSX
import { useState } from 'react';
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

function HumanPresenceButton({ 
  agentDid, 
  onProofObtained 
}: { 
  agentDid: string;
  onProofObtained: (proof: HpProof) => void;
}) {
  const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
    setIsLoading(true);
    
    try {
      // Begin authentication
      const optionsRes = await fetch('/api/hp/auth/begin', {
        method: 'POST',
        body: JSON.stringify({ agentDid }),
      });
      const options = await optionsRes.json();

      // Get WebAuthn assertion
      const assertion = await startAuthentication(options.publicKey);

      // Complete authentication
      const proofRes = await fetch('/api/hp/auth/complete', {
        method: 'POST',
        body: JSON.stringify({
          agentDid,
          challengeId: options.challengeId,
          credential: assertion,
        }),
      });
      const { hpProof } = await proofRes.json();

      onProofObtained(hpProof);
    } catch (error) {
      console.error('HP verification failed:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <button 
      onClick={handleClick} 
      disabled={isLoading}
      className="hp-button"
    >
      {isLoading ? 'Verifying...' : '🔐 Verify Identity'}
    </button>
  );
}

Policy configuration

Always require HP

JSON
{
  "context": {
    "presence": "required"
  }
}

HP above threshold

JSON
{
  "context": {
    "presence": "optional",
    "presence_threshold": 500.00
  }
}

HP for new merchants

JSON
{
  "context": {
    "presence": "optional",
    "presence_on_new_merchant": true
  }
}

HP for specific categories

JSON
{
  "context": {
    "presence": "optional",
    "presence_categories": ["luxury", "crypto", "gambling"]
  }
}

Testing HP

Demo HP endpoint

For testing, use the demo HP proof endpoint:

TypeScript
const demoProof = await client.demoHpProof({
  agentDid: 'did:key:z6Mk...',
  freshnessSeconds: 300,
});

// Use in cart verification
const verification = await client.verifyCart({
  intentVc: permission.intentVc,
  cartVc: cartVc,
  hpProof: demoProof.hpProof,
});

Warning: Demo HP proofs are only accepted in sandbox mode. Production requires real WebAuthn credentials.

Best practices

1. Register multiple credentials

Encourage users to register backup credentials:

TypeScript
const credentials = await client.listHpCredentials(agentDid);

if (credentials.length < 2) {
  showNotification('Consider adding a backup passkey for account recovery');
}

2. Handle credential loss

If a user loses all credentials, provide account recovery:

TypeScript
// Send recovery email
await client.requestHpRecovery(agentDid, {
  email: '[email protected]',
});

3. Fresh proofs only

Reject stale HP proofs:

TypeScript
const proofAge = Date.now() - new Date(hpProof.timestamp).getTime();
const maxAge = 5 * 60 * 1000; // 5 minutes

if (proofAge > maxAge) {
  throw new Error('HP proof expired, please re-authenticate');
}

4. User-friendly prompts

Explain why HP is required:

TSX
{hpRequired && (
  <div className="hp-notice">
    <h3>🔐 Verification Required</h3>
    <p>
      This transaction requires identity verification because:
      {reason === 'HIGH_AMOUNT' && 'The amount exceeds $500'}
      {reason === 'NEW_MERCHANT' && 'This is your first purchase from this merchant'}
      {reason === 'POLICY' && 'Your security settings require verification'}
    </p>
    <HumanPresenceButton agentDid={agentDid} onProofObtained={handleProof} />
  </div>
)}

Supported authenticators

TypeExamplesSupport
PlatformTouch ID, Face ID, Windows HelloFull
RoamingYubiKey, Titan KeyFull
HybridPhone as authenticatorFull

Next steps