Skip to content

Customer Registration for E-Commerce

Learn how to build a customer registration system for your e-commerce website using Hantera’s IAM APIs. This guide covers creating customer identities, email verification, and querying customer-scoped data.

Architecture Overview

Key Pattern:

  • Your e-commerce website is an OAuth client with full Hantera permissions
  • Customer login accounts are IAM principals (for authentication)
  • Customer business entities are Asset actors (for orders, data)
  • Principal links to Customer asset via actorId property
  • Website uses its own credentials to query customer’s asset and orders

Why Separate Principal from Customer Asset?

Security & Flexibility:

  • Email verification required before asset access (activated: true)
  • Prevents querying orders by guessing email addresses
  • Principal can exist before Customer asset (registration → verification → asset creation)
  • Customer asset can exist before Principal (import existing customers, then allow registration)
  • One Customer asset can have multiple Principals (family accounts, B2B users)

Data Architecture:

  • Customer assets have auto-generated customer numbers (e.g., CUST100001)
  • Customer assets have relations to orders, payments, etc.
  • Principals store minimal data (name, email, phone, actorId link)

Prerequisites

  • E-commerce website/app with backend API
  • Hantera access token with IAM permissions
  • Understanding of OAuth 2.0 flows
  • Sendings API for email verification
  • Asset Actors - Understanding custom asset types

Step 1: Define Customer Asset Type

Before you can create Customer assets, you must define the customer type in the Registry.

Create Registry manifest at /registry/actors/asset/types/customer:

uri: /registry/actors/asset/types/customer
spec:
value:
graphSetName: customer
defaultNumberPrefix: "CUST"
relations:
orders:
node: order
cardinality: many

Apply the manifest:

Terminal window
h_ apply customer-type.yaml

This defines:

  • graphSetName: customer - Creates asset.customer graph node
  • defaultNumberPrefix: CUST - Auto-generates customer numbers like CUST100001
  • Relations: Links to orders for querying customer’s order history

Step 2: Create OAuth Client for Website

First, register your e-commerce website as an OAuth client with full order access.

PUT /resources/iam/clients/018c5f3a-1234-5678-9abc-def012345678
Content-Type: application/json
Authorization: Bearer {adminToken}
{
"properties": {
"name": "E-Commerce Website",
"redirectUris": [
"https://mystore.com/auth/callback"
],
"grantTypes": [
"authorization_code",
"refresh_token",
"client_credentials"
]
}
}

Grant the client permissions:

PUT /resources/iam/clients/018c5f3a-1234-5678-9abc-def012345678
Content-Type: application/json
Authorization: Bearer {adminToken}
{
"properties": { ... },
"acl": {
"entries": [
{ "resource": "orders:*", "permission": "read" },
{ "resource": "graph:*", "permission": "query" },
{ "resource": "iam:principals:*", "permission": "read" }
]
}
}

Generate client secret:

POST /resources/iam/clients/018c5f3a-1234-5678-9abc-def012345678/secrets
Content-Type: application/json
Authorization: Bearer {adminToken}
{
"expiresAt": "2026-12-31T23:59:59Z"
}

Response:

{
"secretId": "secret-uuid",
"secret": "client_secret_abc123xyz",
"expiresAt": "2026-12-31T23:59:59Z"
}

Step 3: Create Customer Role

Create a customer role as a marker (no permissions needed).

PUT /resources/iam/roles/customer
Content-Type: application/json
Authorization: Bearer {adminToken}
{
"description": "Customer role marker for e-commerce users",
"acl": {
"entries": []
}
}

The role has no permissions because customers don’t directly access Hantera - your website does.

Step 4: Build Registration Endpoint

Create an API endpoint on your website to handle customer registration.

// POST /api/register
app.post('/api/register', async (req, res) => {
const { email, name, password } = req.body;
// Validate input
if (!email || !name || !password) {
return res.status(400).json({ error: 'Missing required fields' });
}
try {
// Generate unique IDs
const customerId = crypto.randomUUID();
const customerNumber = `CUST-${Date.now()}`;
const activationCode = crypto.randomBytes(32).toString('hex');
// Create principal in Hantera
const principal = await fetch(`https://<tenant url>/resources/iam/principals/${customerId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`
},
body: JSON.stringify({
properties: {
name,
email,
customerNumber,
activationCode,
activated: false
},
roles: ['customer']
})
});
if (!principal.ok) {
const error = await principal.json();
return res.status(principal.status).json(error);
}
// Set password
await fetch(`https://<tenant url>/resources/iam/principals/${customerId}/password/reset`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`
}
});
const passwordResponse = await passwordReset.json();
// Update with actual password (not temp)
await fetch(`https://<tenant url>/resources/me/password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}` // Get token for user
},
body: JSON.stringify({
currentPassword: passwordResponse.temporaryPassword,
newPassword: password
})
});
// Send activation email
await fetch('https://<tenant url>/resources/sendings/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`
},
body: JSON.stringify({
to: email,
subject: 'Activate Your Account',
body: {
html: `
<html>
<body>
<h1>Welcome ${name}!</h1>
<p>Click the link below to activate your account:</p>
<p><a href="https://mystore.com/activate?id=${customerId}&code=${activationCode}">Activate Account</a></p>
</body>
</html>
`,
plainText: `Welcome ${name}!\n\nClick the link below to activate your account:\n\nhttps://mystore.com/activate?id=${customerId}&code=${activationCode}`
},
category: 'customer_activation',
dynamic: {
customerId: customerId,
activationCode: activationCode
}
})
});
res.json({
message: 'Registration successful. Please check your email to activate your account.',
customerId
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'Registration failed' });
}
});

Step 5: Email Verification Endpoint

Handle activation code verification using the IAM API.

// GET /api/activate?id=<principalId>&code=xyz123
app.get('/api/activate', async (req, res) => {
const { id, code } = req.query;
if (!id || !code) {
return res.status(400).json({ error: 'Principal ID and activation code required' });
}
try {
// Fetch principal directly from IAM API (Graph doesn't expose activationCode)
const principalResponse = await fetch(`https://<tenant url>/resources/iam/principals/${id}`, {
headers: {
'Authorization': `Bearer ${clientToken}` // Website's token
}
});
if (!principalResponse.ok) {
return res.status(404).json({ error: 'Invalid activation link' });
}
const principal = await principalResponse.json();
// Verify activation code matches
if (principal.properties.activationCode !== code) {
return res.status(404).json({ error: 'Invalid activation code' });
}
// Check if already activated
if (principal.properties.activated) {
return res.json({
message: 'Account already activated. You can log in.'
});
}
// Check for existing Customer asset by email
const existingCustomerQuery = await fetch('https://<tenant url>/resources/graph', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${clientToken}`
},
body: JSON.stringify([{
edge: 'customers',
filter: `dynamic.email == '${principal.properties.email}'`,
node: {
fields: ['assetId', 'assetNumber']
}
}])
});
const existingResult = await existingCustomerQuery.json();
let assetId;
if (existingResult[0]?.nodes?.length > 0) {
// Link to existing Customer asset
assetId = existingResult[0].nodes[0].assetId;
console.log('Linking to existing customer asset:', assetId);
} else {
// Create new Customer asset
const assetResponse = await fetch('https://<tenant url>/resources/actors/asset/new', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${clientToken}`
},
body: JSON.stringify([{
type: 'create',
body: {
typeKey: 'customer',
// assetNumber auto-generated (e.g., CUST100001)
// Dynamic properties for the customer
commands: [{
type: 'setDynamic',
body: {
fields: {
email: principal.properties.email,
name: principal.properties.name
}
}
}]
}
}])
});
if (!assetResponse.ok) {
const error = await assetResponse.json();
console.error('Failed to create customer asset:', error);
return res.status(500).json({ error: 'Failed to create customer account' });
}
const assetResult = await assetResponse.json();
assetId = assetResult.paths[0].split('/').pop(); // Extract asset ID from path
}
// Update principal: activated + link to asset
const updatedProperties = {
...principal.properties,
activated: true,
activationCode: null, // Remove code after use
actorId: assetId // Link principal to Customer asset
};
await fetch(`https://<tenant url>/resources/iam/principals/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${clientToken}`,
'If-Match': principal.etag
},
body: JSON.stringify({
properties: updatedProperties,
roles: principal.roles
})
});
res.json({
message: 'Account activated successfully! You can now log in.',
assetId: assetId
});
} catch (error) {
console.error('Activation error:', error);
res.status(500).json({ error: 'Activation failed' });
}
});

Step 6: Customer Login Flow

Implement OAuth login for customers.

// GET /api/auth/login
app.get('/api/auth/login', (req, res) => {
const authUrl = `https://<tenant url>/oauth/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent('https://mystore.com/auth/callback')}&` +
`response_type=code&` +
`scope=me:*`;
res.redirect(authUrl);
});
// GET /api/auth/callback
app.get('/api/auth/callback', async (req, res) => {
const { code } = req.query;
try {
// Exchange code for token
const tokenResponse = await fetch('https://<tenant url>/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: 'https://mystore.com/auth/callback'
})
});
const tokens = await tokenResponse.json();
// Get customer identity
const identityResponse = await fetch('https://<tenant url>/resources/me', {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
const identity = await identityResponse.json();
// Check if activated
if (!identity.properties.activated) {
return res.status(403).json({
error: 'Please activate your account via email before logging in'
});
}
// Store session
req.session.customerId = identity.identityId;
req.session.customerNumber = identity.properties.customerNumber;
req.session.customerToken = tokens.access_token;
res.redirect('/dashboard');
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
});

Step 7: Query Customer Orders

Use customer’s customerNumber to scope order queries.

// GET /api/my-orders
app.get('/api/my-orders', async (req, res) => {
if (!req.session.customerNumber) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
// Website uses ITS OWN token (not customer's)
const orders = await fetch('https://<tenant url>/resources/graph', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${clientToken}` // Website token, NOT customer token
},
body: JSON.stringify([{
edge: 'orders',
filter: `customerNumber == '${req.session.customerNumber}'`,
orderBy: 'createdAt desc',
node: {
fields: [
'orderId',
'orderNumber',
'createdAt',
'totalAmount',
'status'
]
}
}])
});
const results = await orders.json();
res.json({
orders: results[0]?.nodes || []
});
} catch (error) {
console.error('Orders query error:', error);
res.status(500).json({ error: 'Failed to fetch orders' });
}
});

Complete Flow Diagram

Customer Registration & Login Flow:
1. Registration
Customer → Website: Submit email, name, password
Website → Hantera IAM: Create principal (activated=false)
Website → Hantera Sendings: Send activation email
2. Activation
Customer → Email: Click activation link
Website → Hantera IAM: Verify activation code
Website → Hantera Assets: Create Customer asset (auto-generates CUST100001)
Website → Hantera IAM: Update principal (activated=true, actorId=<asset>)
3. Login
Customer → Website: Click login
Website → Hantera OAuth: Redirect to authorization
Customer → Hantera: Enter credentials
Hantera → Website: Authorization code
Website → Hantera OAuth: Exchange code for token
Website → Hantera IAM: GET /resources/me
Website: Check activated=true, store actorId in session
4. View Orders
Customer → Website: Request /my-orders
Website → Hantera Graph: Query asset.customer node
Website → Hantera Graph: Follow orders relation
Filter: Using actorId from session
Hantera → Website: Return customer's orders
Website → Customer: Display orders

Key Security Point: Principal cannot access Customer asset until activated: true. This prevents email-guessing attacks where someone registers with another person’s email.

Key Concepts

Customer Has No Permissions

{
"type": "principal",
"properties": {
"customerNumber": "CUST-12345"
},
"roles": ["customer"],
"acl": { "entries": [] } // Empty! No permissions.
}

The customer role is just a marker. All actual data access happens through the website’s OAuth client.

Website Acts as Proxy

// ❌ WRONG: Using customer's token
Authorization: Bearer {customerToken}
// ✅ CORRECT: Using website's token
Authorization: Bearer {clientToken}

The website has full permissions and filters data based on customer identity properties.

Identity Properties Drive Access

// Customer's identity
{
customerNumber: "CUST-12345",
activated: true
}
// Query scoped by property
filter: "customerNumber == 'CUST-12345'"

Security Considerations

Validate Activation Status

Always check activated: true before allowing login:

if (!identity.properties.activated) {
return res.status(403).json({
error: 'Please activate your account first'
});
}

Secure Session Management

Store customer data in secure, HTTP-only cookies:

req.session.customerNumber = identity.properties.customerNumber;
// Never expose raw Hantera tokens to frontend

Input Validation

Sanitize all user input before creating principals:

const email = validator.isEmail(req.body.email)
? req.body.email
: null;
if (!email) {
return res.status(400).json({ error: 'Invalid email' });
}

Troubleshooting

Customer Can’t Log In After Registration

Problem: User registered but login fails.

Check:

  • Is activated: true in their principal properties?
  • Did the activation email send successfully?
  • Is the activation code correct?

Orders Not Showing

Problem: Customer logged in but sees no orders.

Check:

  • Is customerNumber stored in session?
  • Do orders have matching customerNumber field?
  • Is website’s OAuth client token valid?
  • Does client have orders:* read permission?

Email Verification Not Working

Problem: Activation link doesn’t work.

Check:

  • Does the activation URL include both id and code parameters?
  • Is the principal ID valid (exists in IAM)?
  • Does the activationCode in the principal match the URL parameter?
  • Is the activation code URL-encoded properly?

Best Practices

1. Handle Existing Customer Assets

If importing existing customers, check for existing assets by email before creating a new one:

// Query for existing customer asset by email
const existingCustomer = await fetch('https://<tenant url>/resources/graph', {
method: 'POST',
body: JSON.stringify([{
edge: 'customers',
filter: `email == '${email}'`,
node: { fields: ['assetId', 'assetNumber'] }
}])
});
if (existingCustomer.nodes.length > 0) {
// Link principal to existing asset
actorId = existingCustomer.nodes[0].assetId;
} else {
// Create new asset
// ...
}

2. Expire Activation Codes

Store expiration timestamp:

properties: {
activationCode: 'xyz123',
activationCodeExpires: new Date(Date.now() + 24*60*60*1000).toISOString()
}

3. Validate Asset Creation

Always check that the Customer asset was created successfully before linking:

const assetResult = await assetResponse.json();
if (!assetResult.paths || assetResult.paths.length === 0) {
console.error('Asset creation failed:', assetResult);
return res.status(500).json({ error: 'Failed to create customer account' });
}
const assetId = assetResult.paths[0].split('/').pop();

4. Handle Email Uniqueness

Check before registration:

// Query existing customers by email
const existing = await fetch('https://<tenant url>/resources/graph', {
method: 'POST',
body: JSON.stringify([{
edge: 'identities',
filter: `type == 'principal' and properties.email == '${email}'`
}])
});
if (existing.nodes.length > 0) {
return res.status(409).json({ error: 'Email already registered' });
}

5. Rate Limit Registration

Prevent spam registrations:

const rateLimit = require('express-rate-limit');
const registerLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5 // 5 registrations per IP
});
app.post('/api/register', registerLimiter, async (req, res) => {
// ...
});