Skip to main content
Week 06 • Track 01 • The Theory

Webhooks &
Handshakes.

Your app doesn't live alone. When a user pays Stripe, Stripe has to secretly "call" your server to unlock the account. That invisible phone call? That's a webhook.

1

Browser → Stripe

User clicks "Pay $9.99"

2

Stripe → Your Server

Stripe sends a webhook POST

3

Server → Database

Your server unlocks the account

4

Account Unlocked!

User never saw any of this happen

Click "Pay" to see the invisible webhook flow.

The Mental Model

Your server has a private phone line.

When you integrate with a third-party service like Stripe, you're giving them a phone number—a URL on your server. When something important happens (a payment succeeds, a subscription cancels), they call that number and tell you about it. That call is a webhook.

Webhook = An Incoming Call

A third-party service sends an HTTP POST to your server when an event happens.

API Key = Your Backstage Pass

A secret string that proves you're authorized to use someone else's service.

Handshake = The Trust Agreement

The process of exchanging keys and verifying identity before any data flows.

Endpoint = The Phone Number

A URL on your server that listens for incoming webhook calls.

webhook-flow.txt

// The invisible conversation

User → clicks "Pay" → Stripe

(browser → stripe.com)

Stripe → charges card → Success

(stripe processes payment)

StripePOSTyour-app.com/api/webhook

(stripe "calls" your server)

Your Server → unlocks account

(you update the database)

// The user never sees steps 3-4.

// They just see: "Payment successful!"

How Webhooks Work

The Request

You call them. Your app reaches out to an external API to get or send data. Like ordering food at a restaurant.

your-app.ts

// YOU initiate the call

const response = await fetch(

'https://api.openai.com/v1/chat'

);

YOU → THEMYou decide when. You control the timing.

The Webhook

They call you. The external service sends data to your server when something happens on their side. Like a delivery notification arriving at your door.

api/webhook/route.ts

// THEY send data to your endpoint

export async function POST(req) {

const event = await req.json();

// handle the event...

}

THEM → YOUThey decide when. You just listen.

API Keys

Your secret backstage pass.

An API key is a long, random string that proves your identity to an external service. It's like a backstage pass at a concert—it lets you in, and if someone steals it, they can pretend to be you.

That's why API keys live in .env.local, never in your frontend code, and never in Git.

!

Secret Key (Server Only)

Starts with sk_ — never expose to the browser.

Publishable Key (Client OK)

Starts with pk_ — safe to use in the browser.

~

Test vs Live Keys

sk_test_ = sandbox. sk_live_ = real money.

.env.local

# Stripe

STRIPE_SECRET_KEY=sk_test_51N3x...

NEXT_PUBLIC_STRIPE_KEY=pk_test_51N3x...

# OpenAI

OPENAI_API_KEY=sk-proj-a8kF2...

# Resend (email)

RESEND_API_KEY=re_123abc...

# Webhook secret (to verify incoming calls)

STRIPE_WEBHOOK_SECRET=whsec_test_...

# NEVER commit this file to Git!

# Add .env.local to .gitignore

The Handshake

Three steps to trust.

Every third-party integration follows the same handshake ritual. Once you see the pattern, you'll recognize it everywhere—Stripe, OpenAI, Resend, Twilio, all of them.

1

Sign up for the API

Create an account on the service's website. Verify your identity. Agree to their terms.

2

They give you a key

In their dashboard, you'll find API keys. Copy the secret key into your .env.local.

3

Include the key in every request

Every API call you make includes an Authorization header with your key. That's how they know it's you.

api-call.ts

// Step 3: Include the key in every request

const response = await fetch(

'https://api.openai.com/v1/chat/completions',

{

method: 'POST',

headers: {

'Authorization':

`Bearer ${process.env.OPENAI_API_KEY}`,

'Content-Type':

'application/json',

},

body: JSON.stringify({

model: 'gpt-4',

messages: [{ role: 'user', content: 'Hello' }],

}),

},

);

// The "Bearer" prefix is an HTTP convention.

// It says: "I bear this token as proof."

Common Patterns

Integration Traps

"I'll just hardcode the API key"

Now it's in your Git history forever. Anyone with access to the repo can steal it. Use .env.local.

"I'll call the API from the browser"

Your secret key would be visible in DevTools → Network tab. API calls with secrets must go through your server.

"I don't need to verify webhooks"

Anyone could send fake webhook events to your endpoint. Always verify the signature using the webhook secret.

"I'll use the live key for testing"

You just charged a real customer $500. Use test keys (sk_test_) until you're ready for production.

The Clean Integration

Keys in .env.local, accessed via process.env

Never in code, never in Git. Your .gitignore should include .env*.local by default.

API calls go through your server (Route Handlers)

Create /api/chat/route.ts and call OpenAI from there. The browser never sees your key.

Verify every incoming webhook

Use the webhook secret to validate the signature. Stripe, Clerk, and others all provide verification helpers.

Separate test and production environments

Different .env files for dev vs prod. Test keys locally, live keys on Vercel.

The Exercises

Open the doors. Connect to the outside world.

01

The Postman

Your First API Call

  • Install Postman or use curl in the terminal
  • Call jsonplaceholder.typicode.com/posts/1
  • Read the JSON response. Notice the structure.
Goal: See what an API response looks like
02

The Webhook Listener

Receiving Data

  • Create app/api/webhook/route.ts
  • Export a POST function that logs the request body
  • Test it with curl -X POST from the terminal
Goal: Your server can receive incoming calls
03

The Key Vault

Environment Variables

  • Create a .env.local file with a test API key
  • Access it in a server component via process.env
  • Confirm it's invisible in the browser (check page source)
Goal: Secrets stay on the server

📋 Quick Reference Cheatsheet

Webhook Endpoint

export async function POST(req) { }

Auth Header

Authorization: Bearer ${KEY}

Env Variable

process.env.STRIPE_SECRET_KEY