Skip to content
'; user_status_content.firstChild.appendChild(avatarContainer); } else { // Placeholder for LoggedOutUserMenu let loggedOutContainer = document.createElement('div'); // if LoggedOutUserMenu fallback let userBtn = document.createElement('button'); userBtn.style.width = "33px"; userBtn.style.height = "33px"; userBtn.style.display = "flex"; userBtn.style.alignItems = "center"; userBtn.style.justifyContent = "center"; userBtn.style.color = "var(--ds-gray-900)"; userBtn.style.border = "1px solid var(--ds-gray-300)"; userBtn.style.borderRadius = "100%"; userBtn.style.cursor = "pointer"; userBtn.style.background = "transparent"; userBtn.style.padding = "0"; // user icon ( from geist) let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('data-testid', 'geist-icon'); svg.setAttribute('height', '16'); svg.setAttribute('stroke-linejoin', 'round'); svg.setAttribute('style', 'color:currentColor'); svg.setAttribute('viewBox', '0 0 16 16'); svg.setAttribute('width', '16'); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill-rule', 'evenodd'); path.setAttribute('clip-rule', 'evenodd'); path.setAttribute('d', 'M7.75 0C5.95507 0 4.5 1.45507 4.5 3.25V3.75C4.5 5.54493 5.95507 7 7.75 7H8.25C10.0449 7 11.5 5.54493 11.5 3.75V3.25C11.5 1.45507 10.0449 0 8.25 0H7.75ZM6 3.25C6 2.2835 6.7835 1.5 7.75 1.5H8.25C9.2165 1.5 10 2.2835 10 3.25V3.75C10 4.7165 9.2165 5.5 8.25 5.5H7.75C6.7835 5.5 6 4.7165 6 3.75V3.25ZM2.5 14.5V13.1709C3.31958 11.5377 4.99308 10.5 6.82945 10.5H9.17055C11.0069 10.5 12.6804 11.5377 13.5 13.1709V14.5H2.5ZM6.82945 9C4.35483 9 2.10604 10.4388 1.06903 12.6857L1 12.8353V13V15.25V16H1.75H14.25H15V15.25V13V12.8353L14.931 12.6857C13.894 10.4388 11.6452 9 9.17055 9H6.82945Z'); path.setAttribute('fill', 'currentColor'); svg.appendChild(path); userBtn.appendChild(svg); loggedOutContainer.appendChild(userBtn); loggedOutContainer.style.display = 'flex'; loggedOutContainer.style.gap = '8px'; loggedOutContainer.style.alignItems = 'center'; user_status_content.firstChild.appendChild(loggedOutContainer); } })();
Menu
 

Drains Security

Last updated February 27, 2026

All Drains support transport-level encryption using HTTPS protocol.

When your server starts receiving payloads, a third party could send data to your server if it knows the URL. Therefore, you should verify the request is coming from Vercel.

Vercel sends an x-vercel-signature header with each drain, which is a hash of the payload body created using your Drain signature secret. You can find or update this secret by clicking Edit in the Drains list.

To verify the request is coming from Vercel, you can generate the hash and compare it with the header value as shown below:

server.js
import crypto from 'crypto';
 
export async function POST(request) {
  // Store the signature secret in environment variables
  const signatureSecret = '<Drain signature secret>';
 
  const rawBody = await request.text();
  const rawBodyBuffer = Buffer.from(rawBody, 'utf-8');
  const bodySignature = sha1(rawBodyBuffer, signatureSecret);
 
  if (bodySignature !== request.headers.get('x-vercel-signature')) {
    return Response.json(
      {
        code: 'invalid_signature',
        error: "signature didn't match",
      },
      { status: 403 },
    );
  }
 
  console.log(rawBody);
 
  return Response.json({ success: true });
}
 
function sha1(data, secret) {
  return crypto.createHmac('sha1', secret).update(data).digest('hex');
}
server.ts
import crypto from 'crypto';
 
export async function POST(request: Request) {
  // Store the signature secret in environment variables
  const signatureSecret = '<Drain signature secret>';
 
  const rawBody = await request.text();
  const rawBodyBuffer = Buffer.from(rawBody, 'utf-8');
  const bodySignature = sha1(rawBodyBuffer, signatureSecret);
 
  if (bodySignature !== request.headers.get('x-vercel-signature')) {
    return Response.json(
      {
        code: 'invalid_signature',
        error: "signature didn't match",
      },
      { status: 403 },
    );
  }
 
  console.log(rawBody);
 
  return Response.json({ success: true });
}
 
function sha1(data: Buffer, secret: string): string {
  return crypto.createHmac('sha1', secret).update(data).digest('hex');
}
server.js
import crypto from 'crypto';
import getRawBody from 'raw-body';
 
export default async function handler(
  request,
  response,
) {
  if (request.method !== 'POST') {
    return response.status(405).json({ error: 'Method not allowed' });
  }
 
  // Store the signature secret in environment variables
  const signatureSecret = '<Drain signature secret>';
 
  const rawBody = await getRawBody(request);
  const bodySignature = sha1(rawBody, signatureSecret);
 
  if (bodySignature !== request.headers['x-vercel-signature']) {
    return response.status(403).json({
      code: 'invalid_signature',
      error: "signature didn't match",
    });
  }
 
  console.log(rawBody);
 
  response.status(200).json({ success: true });
}
 
function sha1(data: Buffer, secret: string): string {
  return crypto.createHmac('sha1', secret).update(data).digest('hex');
}
 
export const config = {
  api: {
    bodyParser: false,
  },
};
server.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';
import getRawBody from 'raw-body';
 
export default async function handler(
  request: NextApiRequest,
  response: NextApiResponse,
) {
  if (request.method !== 'POST') {
    return response.status(405).json({ error: 'Method not allowed' });
  }
 
  // Store the signature secret in environment variables
  const signatureSecret = '<Drain signature secret>';
 
  const rawBody = await getRawBody(request);
  const bodySignature = sha1(rawBody, signatureSecret);
 
  if (bodySignature !== request.headers['x-vercel-signature']) {
    return response.status(403).json({
      code: 'invalid_signature',
      error: "signature didn't match",
    });
  }
 
  console.log(rawBody);
 
  response.status(200).json({ success: true });
}
 
async function sha1(data: Buffer, secret: string): string {
  return crypto.createHmac('sha1', secret).update(data).digest('hex');
}
 
export const config = {
  api: {
    bodyParser: false,
  },
};
server.js
import crypto from 'crypto';
import getRawBody from 'raw-body';
 
export default async function handler(
  request,
  response,
) {
  if (request.method !== 'POST') {
    return response.status(405).json({ error: 'Method not allowed' });
  }
 
  // Store the signature secret in environment variables
  const signatureSecret = '<Drain signature secret>';
 
  const rawBody = await getRawBody(request);
  const bodySignature = sha1(rawBody, signatureSecret);
 
  if (bodySignature !== request.headers['x-vercel-signature']) {
    return response.status(403).json({
      code: 'invalid_signature',
      error: "signature didn't match",
    });
  }
 
  console.log(rawBody);
 
  response.status(200).json({ success: true });
}
 
function sha1(data: Buffer, secret: string): string {
  return crypto.createHmac('sha1', secret).update(data).digest('hex');
}
 
export const config = {
  api: {
    bodyParser: false,
  },
};
server.ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
import crypto from 'crypto';
import getRawBody from 'raw-body';
 
export default async function handler(
  request: VercelRequest,
  response: VercelResponse,
) {
  if (request.method !== 'POST') {
    return response.status(405).json({ error: 'Method not allowed' });
  }
 
  // Store the signature secret in environment variables
  const signatureSecret = '<Drain signature secret>';
 
  const rawBody = await getRawBody(request);
  const bodySignature = sha1(rawBody, signatureSecret);
 
  if (bodySignature !== request.headers['x-vercel-signature']) {
    return response.status(403).json({
      code: 'invalid_signature',
      error: "signature didn't match",
    });
  }
 
  console.log(rawBody);
 
  response.status(200).json({ success: true });
}
 
function sha1(data: Buffer, secret: string): string {
  return crypto.createHmac('sha1', secret).update(data).digest('hex');
}
 
export const config = {
  api: {
    bodyParser: false,
  },
};

For enhanced security against timing attacks, use constant-time comparison when verifying the x-vercel-signature header. See x-vercel-signature in Request Headers.

For additional authentication or identification purposes, you can also add custom headers when configuring the Drain destination

Managing IP address visibility is available on Enterprise and Pro plans

Those with the owner, admin role can access this feature

Drains can include public IP addresses in the data, which may be considered personal information under certain data protection laws. To hide IP addresses in your drains:

  1. Go to the Vercel dashboard and ensure your team is selected in the team switcher
  2. Open Settings in the sidebar and navigate to Security & Privacy
  3. Under IP Address Visibility, toggle the switch off so the text reads IP addresses are hidden in your Drains

This setting is applied team-wide across all projects and drains.

For more information on Drains security and how to use them, check out the following resources:


Was this helpful?

supported.