Skip to main content
Memberstack Docs
Dashboard

Verification

The Memberstack Admin Package provides essential verification functionality for secure server-side operations, including token verification and webhook validation. These features are crucial for maintaining security in your Memberstack implementation.

/admin-node-package/verification.md
Before You Start
  • Make sure you’ve initialized the Admin Package with your secret key as shown in the Quick Start guide
  • For webhook verification, you’ll need your webhook secret from the Memberstack dashboard (Dev Tools → Webhooks)
  • Understanding of JWT tokens is helpful for token verification

Verify Member Token

Validate a member’s JWT token and access the payload.

The verifyToken() method allows you to validate Memberstack JWT tokens and extract member information:

// Basic token verification
const tokenData = await memberstack.verifyToken({
  token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
});

// With audience validation (recommended)
const tokenData = await memberstack.verifyToken({
  token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  audience: "app_your_app_id"
});

if (tokenData) {
  console.log(`Verified member ID: ${tokenData.id}`);
  console.log(`Token expires at: ${new Date(tokenData.exp * 1000).toLocaleString()}`);
}
Token Verification Response
{
  id: "mem_abc123",
  type: "member",
  iat: 1633486880169,  // Issued at timestamp
  exp: 1633486880369,  // Expiration timestamp
  iss: "https://api.memberstack.com",  // Issuer
  aud: "app_xyz789"    // Audience (your app ID)
}
memberstack. verifyToken (params) async

Token Verification Parameters.

tokenstringRequired
The JWT token to verify (usually from Authorization header).
audiencestring
Your app ID for additional verification (recommended for enhanced security).

Best practices for token verification:

  • Always include audience validation when possible
  • Implement proper error handling for expired or invalid tokens
  • Check the token expiration time to handle near-expiry scenarios
  • Store the member ID from the token for database lookups

Using the Token in Your Application

Here’s an example of how to create an Express.js middleware for authenticating member requests:

auth.middleware.js
// auth.middleware.js
const memberstackAdmin = require('@memberstack/admin');
const memberstack = memberstackAdmin.init(process.env.MEMBERSTACK_SECRET_KEY);

/**
 * Express middleware to verify Memberstack authentication
 */
async function requireAuth(req, res, next) {
  try {
    // Extract token from Authorization header
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const token = authHeader.split(' ')[1];

    // Verify the token
    const verifiedToken = await memberstack.verifyToken({
      token,
      audience: process.env.MEMBERSTACK_APP_ID
    });

    // Add member data to the request object
    req.member = verifiedToken;

    // Token is valid, proceed
    next();
  } catch (error) {
    console.error('Authentication error:', error.message);

    // Determine the appropriate error response
    if (error.message.includes('expired')) {
      return res.status(401).json({ error: 'Authentication expired' });
    }

    return res.status(401).json({ error: 'Invalid authentication' });
  }
}

module.exports = { requireAuth };
Common Token Issues
  • Expired tokens: The token has passed its expiration time and is no longer valid
  • Invalid signature: The token has been tampered with or was created with a different key
  • Audience mismatch: The token was issued for a different application than expected
  • Format errors: The token is malformed or doesn’t follow JWT standards

Verify Webhook Signature

Ensure webhook payloads are authentic and haven’t been tampered with.

Memberstack uses Svix under the hood for secure webhook delivery. This section explains how to properly verify incoming webhook requests to ensure they are legitimate and haven’t been tampered with.

Important Header Information

Memberstack webhook verification requires specific headers that are sent with each webhook request:

  • svix-id - Unique identifier for the webhook event
  • svix-timestamp - When the webhook was sent
  • svix-signature - The cryptographic signature for verification

When you pass these to verifyWebhookSignature, the headers object must be keyed in uppercaseSVIX-ID, SVIX-TIMESTAMP, and SVIX-SIGNATURE — which is the exact key the SDK looks up. Read the values from the incoming request headers (which arrive in lowercase) as shown in the example below. Previous header formats (like ms-webhook-id) are no longer supported.

How verification reports success or failure

verifyWebhookSignature returns true when the signature is valid and throws an error on any failure — missing svix headers, a timestamp outside the tolerance window, or a signature mismatch. It never returns false, so there is no falsy value to test. Call it inside a try/catch and treat any thrown error as an invalid webhook (reject with 401); don’t branch on its return value or you’ll let forged requests through.

Basic Verification Example

Here’s how to verify a webhook signature in an Express.js application:

webhook.js
// Express.js webhook verification example
const express = require('express');
const memberstackAdmin = require('@memberstack/admin');

const app = express();
const memberstack = memberstackAdmin.init(process.env.MEMBERSTACK_SECRET_KEY);

// Important: Use express.raw() to preserve the raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    // The raw body is needed for verification (don't parse to JSON first)
    const payload = req.body.toString();

    // verifyWebhookSignature looks these up by UPPERCASE key, so key the
    // object SVIX-ID / SVIX-TIMESTAMP / SVIX-SIGNATURE. The incoming request
    // header names are lowercase, hence req.headers['svix-id'].
    const headers = {
      'SVIX-ID': req.headers['svix-id'],
      'SVIX-TIMESTAMP': req.headers['svix-timestamp'],
      'SVIX-SIGNATURE': req.headers['svix-signature']
    };

    // Verify the webhook. verifyWebhookSignature returns true on success and
    // THROWS on any failure (missing headers, timestamp out of tolerance, or a
    // signature mismatch) — it never returns false. So if this call doesn't
    // throw, the signature is valid; an invalid signature jumps to catch below.
    memberstack.verifyWebhookSignature({
      payload: JSON.parse(payload),  // Parse the payload for processing after verification
      headers: headers,              // Pass the headers object
      secret: process.env.MEMBERSTACK_WEBHOOK_SECRET  // Your webhook secret from dashboard
    });

    // Webhook is verified, safe to process
    const data = JSON.parse(payload);
    console.log(`Verified webhook event: ${data.event}`);

    // Process the webhook based on event type
    switch (data.event) {
      case 'member.created':
        // Handle member creation
        break;
      case 'member.updated':
        // Handle member update
        break;
      // Add more event types as needed
    }

    // Return a 200 response to acknowledge receipt
    res.status(200).send('Webhook received and verified');
  } catch (error) {
    // A thrown error means verification failed (missing/invalid signature) — reject it.
    console.error('Webhook verification failed:', error.message);
    res.status(401).send('Invalid signature');
  }
});
Common Verification Issues
  • Use express.raw() - Not express.json() for the webhook endpoint
  • Preserve the raw body - Modifying the body before verification will cause signature failure
  • Header key casing - verifyWebhookSignature reads the headers by uppercase key (SVIX-ID, SVIX-TIMESTAMP, SVIX-SIGNATURE). Passing them lowercase triggers a “Please provide the svix-id, svix-timestamp, and svix-signature headers” error even when the headers are present
  • Correct webhook secret - Make sure you’re using the correct webhook secret from your dashboard (Dev Tools → Webhooks)
  • Parse JSON after verification - Only parse the JSON payload after verifying the signature

Troubleshooting Header Errors

If you encounter the error "Please provide the svix-id, svix-timestamp, and svix-signature headers", follow these steps:

Header Troubleshooting

1. Debug the headers you’re receiving:

// Log all headers to see what's actually being received
console.log('All headers:', req.headers);

2. Map the headers to the keys the SDK expects: verifyWebhookSignature looks them up by uppercase key, so build the object with SVIX-ID, SVIX-TIMESTAMP, and SVIX-SIGNATURE (reading the lowercase values Node provides):

// The SDK reads UPPERCASE keys; map the (lowercase) incoming headers to them
const headers = {
  'SVIX-ID': req.headers['svix-id'],
  'SVIX-TIMESTAMP': req.headers['svix-timestamp'],
  'SVIX-SIGNATURE': req.headers['svix-signature']
};

3. Don’t pass req.headers directly: Node lowercases incoming header names, but the SDK looks them up by uppercase key — so the raw req.headers object won’t match. Map to the uppercase keys (as above) before calling verifyWebhookSignature:

// Pass the UPPERCASE-keyed object from step 2 (not req.headers directly).
// verifyWebhookSignature returns true on success and throws on failure, so call
// it inside a try/catch rather than assigning and testing its return value.
memberstack.verifyWebhookSignature({
  payload: JSON.parse(payload),
  headers,  // { 'SVIX-ID': ..., 'SVIX-TIMESTAMP': ..., 'SVIX-SIGNATURE': ... }
  secret: process.env.MEMBERSTACK_WEBHOOK_SECRET
});

Next.js API Route Example

For Next.js API routes, you’ll need to disable the built-in body parsing:

pages/api/webhook.js
// pages/api/webhook.js
import memberstackAdmin from '@memberstack/admin';

// Initialize Memberstack outside the handler
const memberstack = memberstackAdmin.init(process.env.MEMBERSTACK_SECRET_KEY);

// Important: Disable body parsing
export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    // Get the raw body
    const chunks = [];
    for await (const chunk of req) {
      chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
    }
    const rawBody = Buffer.concat(chunks).toString('utf8');

    // Map to the UPPERCASE keys verifyWebhookSignature expects
    const headers = {
      'SVIX-ID': req.headers['svix-id'],
      'SVIX-TIMESTAMP': req.headers['svix-timestamp'],
      'SVIX-SIGNATURE': req.headers['svix-signature']
    };

    // Verify webhook. verifyWebhookSignature returns true on success and THROWS
    // on any failure — it never returns false. If this call doesn't throw, the
    // signature is valid; an invalid signature jumps to the catch block below.
    memberstack.verifyWebhookSignature({
      payload: JSON.parse(rawBody),
      headers,
      secret: process.env.MEMBERSTACK_WEBHOOK_SECRET
    });

    const data = JSON.parse(rawBody);
    console.log(`Webhook event: ${data.event}`);

    // Process webhook...

    return res.status(200).json({ success: true });
  } catch (error) {
    // A thrown error means verification failed (missing/invalid signature) — reject it.
    console.error('Webhook verification failed:', error.message);
    return res.status(401).json({ error: 'Invalid signature' });
  }
}
Webhook Security Best Practices
  • Always verify the webhook signature to prevent spoofing
  • Implement idempotency using the webhook ID to prevent duplicate processing
  • Store webhook events in a queue to ensure reliable processing
  • Set up appropriate timeouts for webhook processing
  • Return 2xx status codes promptly to acknowledge receipt (even if processing continues asynchronously)
  • Rotate webhook secrets periodically for enhanced security

Finding Your Webhook Secret

To get your webhook secret:

  1. Log in to your Memberstack dashboard
  2. Navigate to "DevTools" > "Webhooks"
  3. Find or create your webhook endpoint
  4. Copy the "Signing Secret" value

Store this secret securely in your environment variables, not in your code.

Common Use Cases

Practical examples for implementing token verification.

Express.js Authentication Middleware

Here’s an example of creating a reusable middleware for authenticating requests in an Express.js application:

middleware/auth.js
// middleware/auth.js
const axios = require('axios');
require('dotenv').config();

const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com/members';

/**
 * Middleware to verify Memberstack authentication
 */
async function requireAuth(req, res, next) {
  try {
    // Extract token from Authorization header
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const token = authHeader.split(' ')[1];

    // Verify the token with Memberstack
    const response = await axios.post(
      `${BASE_URL}/verify-token`,
      { token },
      {
        headers: {
          'X-API-KEY': API_KEY,
          'Content-Type': 'application/json'
        }
      }
    );

    // Add member data to the request object
    req.member = response.data.data;

    // Token is valid, proceed
    next();
  } catch (error) {
    console.error('Authentication error:', error.message);

    // Determine the appropriate error response
    if (error.response?.status === 401) {
      return res.status(401).json({ error: 'Authentication expired' });
    }

    return res.status(401).json({ error: 'Invalid authentication' });
  }
}

module.exports = { requireAuth };

// Usage in your routes
// routes/api.js
const express = require('express');
const router = express.Router();
const { requireAuth } = require('../middleware/auth');

// Public route - no authentication required
router.get('/public', (req, res) => {
  res.json({ message: 'This is a public endpoint' });
});

// Protected route - requires authentication
router.get('/protected', requireAuth, (req, res) => {
  res.json({
    message: 'This is a protected endpoint',
    memberId: req.member.id
  });
});

module.exports = router;

Next.js API Route Protection

Here’s how to protect API routes in a Next.js application:

pages/api/protected-data.js
// pages/api/protected-data.js
import axios from 'axios';

// Helper function to verify Memberstack token
async function verifyToken(token) {
  const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
  const BASE_URL = 'https://admin.memberstack.com/members';

  try {
    const response = await axios.post(
      `${BASE_URL}/verify-token`,
      { token },
      {
        headers: {
          'X-API-KEY': API_KEY,
          'Content-Type': 'application/json'
        }
      }
    );

    return response.data.data;
  } catch (error) {
    return null;
  }
}

export default async function handler(req, res) {
  // Get auth token from header
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  const token = authHeader.split(' ')[1];

  // Verify the token
  const memberData = await verifyToken(token);

  if (!memberData) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }

  // Check if token is expired
  const now = Math.floor(Date.now() / 1000);
  if (memberData.exp < now) {
    return res.status(401).json({ error: 'Token expired' });
  }

  // Token is valid, return protected data
  return res.status(200).json({
    message: 'Protected data accessed successfully',
    memberId: memberData.id,
    data: {
      // Your protected data here
      sensitiveInformation: 'This is protected content',
      memberSpecificData: `Data for member ${memberData.id}`
    }
  });
}

Permission-Based Access Control

Implement role-based or permission-based access control by combining token verification with member data:

middleware/rbac.js
// middleware/rbac.js
const axios = require('axios');
require('dotenv').config();

const API_KEY = process.env.MEMBERSTACK_SECRET_KEY;
const BASE_URL = 'https://admin.memberstack.com';

/**
 * Middleware to check if member has a required plan
 * @param {string} requiredPlanId - The plan ID to check for
 */
function requirePlan(requiredPlanId) {
  return async (req, res, next) => {
    try {
      // First ensure we have a verified member
      if (!req.member || !req.member.id) {
        return res.status(401).json({ error: 'Authentication required' });
      }

      // Get full member details
      const memberResponse = await axios.get(
        `${BASE_URL}/members/${req.member.id}`,
        {
          headers: { 'X-API-KEY': API_KEY }
        }
      );

      const member = memberResponse.data.data;

      // Check if member has the required plan
      const hasPlan = member.planConnections.some(
        plan => plan.planId === requiredPlanId && plan.status === 'ACTIVE'
      );

      if (!hasPlan) {
        return res.status(403).json({ error: 'Access denied. Required plan not found.' });
      }

      // Member has the required plan, proceed
      next();
    } catch (error) {
      console.error('Plan verification error:', error.message);
      return res.status(500).json({ error: 'Error verifying access' });
    }
  };
}

module.exports = { requirePlan };

// Usage example
// routes/premium-api.js
const express = require('express');
const router = express.Router();
const { requireAuth } = require('../middleware/auth');
const { requirePlan } = require('../middleware/rbac');

// Protected premium route
router.get(
  '/premium-content',
  requireAuth,
  requirePlan('pln_premium'),
  (req, res) => {
    res.json({
      message: 'This is premium content',
      content: 'Your exclusive premium data here'
    });
  }
);

module.exports = router;
Need help?
Can't find what you're looking for? Our team is here to help.