Upwork Automation Lab

Execute complete client engagements back to back. Build, fix, deliver, and hand over real automation systems across different Upwork client archetypes — then take each capability to market.

Why most freelancers and businesses fail
66%
of freelancers say finding clients is their biggest challenge
50%
of businesses fail within 5 years — 22.6% in year one alone
16%
fail specifically from poor marketing — not from poor product
The pattern is consistent across freelancers and businesses: capability alone doesn't produce revenue. The #1 reason businesses close is that the market doesn't need their product — not that they built it badly. Among freelancers, two-thirds struggle not with delivery but with finding and converting clients. Production creates value. Market conversion turns it into profit. This course teaches both — the technical skill and the positioning, proposals, and delivery strategy to sell it. Breaking into the market is part of the work, not a side effect of competence.
Sources: DemandSage Freelance Statistics 2026, Fortunly Business Failure Rates, Bureau of Labor Statistics
Track A — Active Modules
Module 1

Meta CAPI Automation Lab

Build a complete B2B lead-gen tracking system from scratch — Typeform, Calendly, Stripe, n8n, Zapier — then package and price the deliverable for Upwork.

12 parts 20 layers $300–$400 client
Module 2

Shopify Tracking Diagnostic

Walk into a broken Shopify store, audit all 5 tracking installation points, fix the duplication, and deliver a handover package you can replicate for every client after.

11 parts 12 layers $10–$30/hr client
Upcoming Modules

Module 3

GTM & Server-Side GTM Pipelines

Module 4

Multi-Platform Tracking (Google, TikTok, LinkedIn)

Module 5

iOS Privacy, Consent & Optimization

Track C — AI Integration & Automation
Module 1

AI Integration Laboratory

Execute a complete SaaS B2B client engagement — build three production AI integrations, price a $2,800 fixed project, and position for retainer revenue.

12 parts 16 layers $2,800 client
Market Intelligence
Research

Upwork Automation Niches

Factual breakdown of 8 automation sub-markets on Upwork — growth rates, rate ranges, saturation analysis, and opportunity rankings sourced from Upwork's official reports.

8 niches 13 sources Aug 2026 data
Study Plan
Timetable

Part-Time Study Plan

12-week timetable covering all three labs in recommended learning order — paced at 5–6 hours per week for busy professionals.

12 weeks ~60 hours 3 labs
Course Overview
Reference

Cost, Tools & Career Path

Total cost commitment, every tool you'll use, current Upwork market positioning, and what each module unlocks for your freelance career.

$2–$5 total 15+ tools 3 career tiers

MODULE 1 Conversion Tracking & Attribution Track

Meta Conversion Tracking & Automation Laboratory

A complete, hands-on student laboratory based on a real Upwork client project.

Covers Meta Pixel, Conversions API, n8n, Zapier, Typeform, Calendly, Google Sheets, and Stripe. 20 implementation layers. Platform-independent automation architecture.

How to use this laboratory: Work through each Part and Layer in sequence. Every layer builds on the one before it. You will set up real accounts, build real workflows, and send real test events. By the end, you will have built and verified an end-to-end conversion tracking system and be able to explain every component of it.

Part 1 — Raw Client Brief

Read this exactly as a freelancer would see it on Upwork. Do not skim it. Do not skip sections.

Looking for a specialist who has previously set up Meta Conversions API + Zapier for a lead generation funnel. If you've done Typeform → Calendly → Stripe tracking before, this project should take 4-6 hours. ABOUT THE PROJECT I run a B2B marketing agency (ScaleUp Partner) that helps BUSINESSES get more qualified enquiries. My funnel is: User sees Meta ad → clicks Lands on my website (scaleuppartner.co) Fills out Typeform (7-8 qualifying questions, captures email + phone) Gets redirected to Calendly to book a discovery call If qualified, signs a contract via Stripe (€1,400-€1,700/month) Right now I only track the "Lead" event when someone submits the Typeform. This is a problem — Meta is optimising for form fills, not actual booked calls or signed clients. I need Meta to know which leads are GOOD (booked, showed up, signed) so it can find me more like them. WHAT I NEED YOU TO BUILD 1. Meta Pixel Audit Verify the Pixel is installed correctly on scaleuppartner.co Confirm Lead event is firing on Typeform submission Send me a screenshot from Meta Events Manager 2. Add Meta Pixel to Calendly Set up so a "Schedule" event fires when someone books a call 3. Build 4 Zapier Automations: Zap 1: Calendly booking → Fire Meta "Schedule" offline conversion (with email + phone) Zap 2: Call attended (marked in Calendly) → Fire Meta custom event "CallAttended" Zap 3: Lead marked "Qualified" in Google Sheet → Fire Meta custom event "QualifiedLead" Zap 4: Stripe payment received → Fire Meta "Purchase" event with value (€1,400 or €1,700) 4. Set Up Lead Tracking Google Sheet Auto-populated with columns for: Date | Name | Email | Phone | Country | Typeform Submitted | Calendly Booked | Attended | Qualified | Signed | Value 5. Configure Custom Conversions in Meta Events Manager So all 5 events (Lead, Schedule, CallAttended, QualifiedLead, Purchase) are usable as optimisation events in Ads Manager. 6. Documentation Handover Loom video walking me through the whole setup + written doc with screenshots so I can troubleshoot later. TOOLS I ALREADY HAVE Meta Business Manager + Pixel installed Typeform (Business plan) Calendly (Standard) Stripe Shopify (scaleuppartner.co is on Shopify) Google Workspace I do NOT have Zapier yet — you'll need to guide me on signing up for the correct plan. BUDGET & TIMELINE Fixed price: $300-400 Timeline: Complete within 5-7 days Should take: 4-6 hours for someone experienced TO BE CONSIDERED, PLEASE INCLUDE: A screenshot or Loom of a Meta Conversions API setup you've completed before (Typeform/Calendly/Stripe stack ideally) Your answer to this question: "In one sentence, what's the difference between the Meta Pixel and the Conversions API?" How long you estimate this will take you Your fixed price I will ignore generic applications and templated responses. If you can't share proof of past CAPI work, please don't apply. Looking to hire within 24 hours.

End of raw client brief. Everything above is the client's exact language. We have not changed a single word.

Part 2 — Client Requirements Analysis

Translate the client's business language into technical requirements. Challenge every assumption.

A professional freelancer does not simply execute a client brief line by line. They read the brief, identify what the client actually needs, separate it from the specific words the client used, and flag anything that requires verification or discussion. Below, we analyze each major requirement.

Requirement 1: "Meta Pixel Audit"

CLIENT SAID"Verify the Pixel is installed correctly on scaleuppartner.co. Confirm Lead event is firing on Typeform submission."
INTERPRETATIONThe client has an existing Meta Pixel on a Shopify site. They believe a Lead event fires when a visitor submits the embedded Typeform.
TECH QUESTIONHow is the Lead event currently being triggered? Via Pixel base code on the thank-you page? Via Typeform's embed SDK callback? Via a Shopify app? Is the Typeform embedded (iframe) or linked externally?
TECH REALITYAn embedded Typeform renders inside an iframe. The Meta Pixel running on the parent page cannot see events inside the iframe. If the Lead event fires, it is likely triggered by a page redirect after submission (a thank-you URL), by the Typeform embed SDK's onSubmit callback, or by a Shopify app — not by the Pixel "watching" the form.
RECOMMENDATIONAudit the actual mechanism. Open the site, submit the form, check Meta Events Manager and the browser Network tab. Do not assume it works just because the client says it does.

Requirement 2: "Add Meta Pixel to Calendly"

CLIENT SAID"Set up so a 'Schedule' event fires when someone books a call."
INTERPRETATIONThe client wants a Meta event sent when a Calendly booking occurs.
TECH QUESTIONCan you install the Meta Pixel on a Calendly booking page?
TECH REALITYCalendly's hosted pages (calendly.com/username/event-type) do not allow custom JavaScript injection. You cannot install the Meta Pixel on a Calendly page. If Calendly is embedded on the client's own site via an iframe, the Pixel on the parent page still cannot see inside the iframe. The phrase "Add Meta Pixel to Calendly" reveals a common misunderstanding.
RECOMMENDATIONThe Schedule event must be sent server-side via the Conversions API, triggered by a Calendly webhook (booking created). This is exactly what Zap 1 does — so the Pixel audit item and Zap 1 are actually the same deliverable. Explain this to the client.

Requirement 3: "Fire Meta 'Schedule' offline conversion"

CLIENT SAID"Calendly booking → Fire Meta 'Schedule' offline conversion (with email + phone)"
INTERPRETATIONThe client wants Calendly bookings reported to Meta with the event name "Schedule" along with identity data.
TECH QUESTIONIs "Schedule" a Meta standard event? Is the term "offline conversion" correct?
TECH REALITY"Schedule" is a Meta standard event. The term "offline conversion" is outdated — Meta's Offline Conversions API was deprecated and merged into the Conversions API. The correct term is a server event sent via the Conversions API. The action_source for a Calendly booking that originated from a website visit would be "website" (the booking flow started from the client's site), though "system_generated" is also defensible.
RECOMMENDATIONUse the Conversions API with event name Schedule, action_source: "website", and include hashed email + phone in user_data.

Requirement 4: "Call attended (marked in Calendly)"

CLIENT SAID"Call attended (marked in Calendly) → Fire Meta custom event 'CallAttended'"
INTERPRETATIONThe client believes Calendly has an "attended" status that can trigger an automation.
TECH QUESTIONDoes Calendly expose an "attended" or "completed" status via API/webhook?
TECH REALITYCalendly supports marking invitees as "No Show" (via the dashboard and the API endpoint POST /invitee_no_shows), but it does not have a positive "attended" status or webhook. The absence of a no-show marking does not reliably indicate attendance, because the host may not have marked anyone. There is no invitee.attended webhook event.
RECOMMENDATIONAttendance is a manual business decision, not an automated trigger. The recommended workflow: after the call, the team member updates the Google Sheets lead tracker (sets Attended = TRUE). A Google Sheets change trigger fires the automation. This separates the human judgment from the automated transmission.

Requirement 5: "Lead marked 'Qualified' in Google Sheet"

CLIENT SAID"Lead marked 'Qualified' in Google Sheet → Fire Meta custom event 'QualifiedLead'"
INTERPRETATIONA human marks a lead as qualified, the sheet triggers an automation, and Meta receives the event.
TECH QUESTIONHow is "Qualified" stored? Boolean column? Status column? New row?
TECH REALITYThis is architecturally sound. Google Sheets triggers (in both n8n and Zapier) can detect row updates. The main risk is duplicate firing — if someone edits the row again, the trigger fires again, and Meta receives a duplicate QualifiedLead event. Event ID–based deduplication is critical here.
RECOMMENDATIONUse a dedicated boolean column (Qualified = TRUE/FALSE). Add a "QualifiedLead_Sent" timestamp column that the automation fills after successful transmission, and only fire if that column is empty.

Requirement 6: "Configure Custom Conversions in Meta Events Manager"

CLIENT SAID"So all 5 events (Lead, Schedule, CallAttended, QualifiedLead, Purchase) are usable as optimisation events in Ads Manager."
INTERPRETATIONThe client wants all five events available for campaign optimization.
TECH QUESTIONDoes every event need a Custom Conversion? Are all events usable for optimization?
TECH REALITYLead, Schedule, and Purchase are Meta standard events — they appear automatically in Events Manager once received. They do not need Custom Conversions to be usable. CallAttended and QualifiedLead are custom event names — they also appear automatically once sent, and can be selected as optimization events directly. Custom Conversions are a separate feature: they let you create filtered subsets of events (e.g., "Purchase where value > €1000"). The client may or may not need them. Additionally, Meta requires approximately 50 events per week to exit the learning phase for a given optimization event. Lower-funnel events like Purchase may not have enough volume initially.
RECOMMENDATIONSend all five events. Verify they appear in Events Manager. Explain the difference between event transmission, event configuration, and optimization eligibility. Let the client decide which event to optimize for based on volume and business goals — do not promise that "all 5 will work as optimization events" without discussing volume requirements.

Requirement 7: "Zapier" as the automation platform

CLIENT SAID"Build 4 Zapier Automations" / "I do NOT have Zapier yet"
INTERPRETATIONThe client specifically wants Zapier.
TECH QUESTIONWhat Zapier plan is needed? What are the cost implications?
TECH REALITYZapier's free plan (2026): 100 tasks/month, limited to 2-step Zaps, 5 Zaps total. The Calendly trigger and conditional logic (e.g., filter for Attended = TRUE) require multi-step Zaps — meaning the Professional plan ($29.99/month) at minimum. With four Zaps running, a busy funnel could consume tasks quickly. The Facebook Conversions integration on Zapier supports actions: "Send Lead Event," "Send Purchase Event," "Send Other Event," and "Send Funnel Event."
RECOMMENDATIONIn this laboratory, we build in n8n first (free, self-hosted, full HTTP visibility) to learn the architecture. Then we reproduce in Zapier because the client requires it. Advise the client on the Professional plan and expected task consumption.

Part 3 — Client Problem Statement

What is the actual business problem the client needs solved?

Current State

The client runs Meta ads that drive traffic to a B2B marketing agency website. Visitors fill out a Typeform, which fires a Lead event to Meta. Meta's ad delivery algorithm receives this single signal and optimizes for it — finding more people likely to fill out a form.

The Information Gap

After the form submission, the client's funnel continues: qualified leads book Calendly calls, attend those calls, get evaluated, and some sign contracts worth €1,400–€1,700/month via Stripe. Meta knows nothing about any of this. As far as Meta's algorithm is concerned, every form submission is equally valuable. A tire-kicker who submits the form and never books a call looks identical to a client who signs a €1,700/month contract.

Why This Matters

Meta's ad delivery optimizes for the signal it receives. If the only signal is "Lead" (form submission), Meta finds more people who fill out forms — not more people who book calls, attend calls, qualify, and pay. The client is spending ad budget attracting form-fillers instead of buyers.

Desired State

The client wants Meta to receive five signals along the funnel, each representing a progressively more valuable business action:

SignalBusiness ActionBusiness Value
LeadTypeform submittedInterest expressed
ScheduleCalendly call bookedCommitment made
CallAttendedProspect showed upEngagement confirmed
QualifiedLeadProspect evaluated as fitSales opportunity
PurchaseStripe payment received€1,400–€1,700 revenue

With all five signals, Meta can learn which ad clicks ultimately lead to paying clients — and optimize ad delivery to find more people like them.

Part 4 — Client Objectives

Separate the client's request into distinct objective categories.

Business Objectives

Improve Meta ad performance by giving the algorithm richer downstream conversion signals. Reduce cost per acquisition by optimizing for qualified leads and purchases rather than form fills. Gain visibility into the full funnel from ad click to signed contract.

Tracking Objectives

Track five conversion events: Lead (form submission), Schedule (Calendly booking), CallAttended (prospect showed up), QualifiedLead (manually assessed), Purchase (Stripe payment with value). Each event must carry identity data sufficient for Meta to match it back to the original ad click.

Automation Objectives

Automate the transmission of conversion events to Meta whenever a business action occurs. Minimize manual work — the team should only need to mark attendance and qualification in a spreadsheet; everything else should happen automatically.

Meta Objectives

All five events visible in Meta Events Manager. Events usable for campaign optimization in Ads Manager. Event Match Quality high enough for reliable attribution.

Reporting & Documentation Objectives

A Google Sheet tracking every lead through the funnel. A Loom walkthrough video. Written documentation with screenshots. The client must be able to troubleshoot independently after handover.

Commercial Constraints

ConstraintValue
Budget$300–$400 fixed price
Timeline5–7 days
Estimated effort4–6 hours for experienced implementer
Existing toolsMeta Business Manager, Pixel, Typeform (Business), Calendly (Standard), Stripe, Shopify, Google Workspace
Missing toolsZapier (client needs guidance on plan selection)
Required deliverablesWorking system + Loom video + written documentation

Part 5 — Technical Objectives

What the student must be able to do after completing this laboratory.

#Technical ObjectiveCovered In
1Audit an existing Meta Pixel installationLayers 1–2
2Verify Lead event trackingLayer 4
3Understand website → embedded Typeform data flowLayer 3
4Understand Typeform → Calendly redirectLayer 5
5Capture a Calendly booking via webhookLayer 9
6Send a Schedule event to Meta via CAPILayer 9
7Capture attendance via manual entry + sheet triggerLayer 10
8Send a CallAttended event to Meta via CAPILayer 10
9Capture qualification via sheet update + triggerLayer 11
10Send a QualifiedLead event to Meta via CAPILayer 11
11Capture a Stripe payment via webhookLayer 12
12Send a Purchase event with value/currency to MetaLayer 12
13Preserve identity (email, phone, fbp, fbc) across the funnelLayer 16
14Generate and manage event IDs for deduplicationLayer 17
15Test every handshake individuallyLayers 9–12
16Troubleshoot common failuresLayer 18
17Verify events in Meta Events ManagerLayer 14
18Understand Meta event configuration vs. transmissionLayer 15
19Reproduce n8n workflows in ZapierLayer 13
20Document the final implementation for client handoverLayer 20

Part 6 — Client's Requested Architecture

The funnel and the five handshakes, exactly as the client describes them.

The Funnel

META ADS
WEBSITE (scaleuppartner.co)
TYPEFORM (7–8 questions)
↓ Lead
CALENDLY (book discovery call)
↓ Schedule
DISCOVERY CALL
↓ CallAttended
QUALIFICATION
↓ QualifiedLead
STRIPE PAYMENT (€1,400–€1,700)
↓ Purchase
META (receives all 5 events)

The Five Handshakes

HANDSHAKE #1 AUTOMATED

Typeform submission → Meta Pixel (browser-side) → Lead

Trigger: user submits form. Transport: browser JavaScript. No automation platform needed.

HANDSHAKE #2 AUTOMATED

Calendly booking → Webhook → n8n/Zapier → CAPI → Schedule

Trigger: Calendly invitee.created webhook. Transport: server-to-server HTTP POST. Identity: email + phone from Calendly invitee data.

HANDSHAKE #3 MANUAL + AUTOMATED

Call occurs → Human marks Attended = TRUE in Google Sheet → n8n/Zapier → CAPI → CallAttended

Trigger: Google Sheet row update. Manual step: team member marks attendance. Transport: server-to-server HTTP POST.

HANDSHAKE #4 MANUAL + AUTOMATED

Team evaluates lead → Human marks Qualified = TRUE in Google Sheet → n8n/Zapier → CAPI → QualifiedLead

Trigger: Google Sheet row update. Manual step: team member marks qualification. Transport: server-to-server HTTP POST.

HANDSHAKE #5 AUTOMATED

Client pays via Stripe → Webhook → n8n/Zapier → CAPI → Purchase (with value + currency)

Trigger: Stripe checkout.session.completed webhook. Transport: server-to-server HTTP POST. Identity: email from Stripe customer. Value: transaction amount in EUR.

Part 7 — Architecture Review

Challenge every assumption before building anything.

Before implementation, a professional reviews each requirement against technical reality.

Client RequestTechnical ValidationRecommended Implementation
"Add Meta Pixel to Calendly" Not possible. Calendly hosted pages do not support custom JS. Embedded Calendly uses an iframe the parent Pixel cannot access. Send Schedule event via CAPI (server-side), triggered by Calendly webhook.
"Fire Meta 'Schedule' offline conversion" "Offline Conversions" is deprecated terminology. The Offline Conversions API was merged into the Conversions API. Use the Conversions API. The event name Schedule is a standard Meta event and is correct.
"Call attended (marked in Calendly)" Calendly has no "attended" status or webhook. It only supports marking no-shows. Use Google Sheets as the attendance record. Team marks Attended = TRUE after the call. Sheet change triggers the automation.
"Configure Custom Conversions for all 5 events" Lead, Schedule, Purchase are standard events — they do not require Custom Conversions to appear in Meta. CallAttended and QualifiedLead are custom events — they also appear automatically once received. Send all events via CAPI. Verify they appear in Events Manager. Create Custom Conversions only if the client needs filtered subsets (e.g., "Purchase > €1,500").
"All 5 events usable as optimisation events" Any event received by Meta can be selected for optimization, but Meta needs ~50 events/week to exit learning phase. Lower-funnel events (Purchase) may not have enough volume. Start optimizing for Lead or Schedule (higher volume). Move to Purchase optimization once volume is sufficient. Explain this tradeoff to the client.
"Zapier" as the automation platform Free plan insufficient (2-step limit, 100 tasks/month). Professional plan required ($29.99/month+). Build in n8n first to learn. Reproduce in Zapier for the client. Advise client on Professional plan.
"Auto-populated" Google Sheet Typeform and Calendly can push data to Sheets via integrations. Manual columns (Attended, Qualified, Signed) cannot be auto-populated — they require human input. Auto-populate from Typeform submissions. Add Calendly booking data via n8n/Zapier. Manual columns for human decisions.

Part 8 — Laboratory Environment

Set up the tools. Understand what is free, what is trial, and what costs money.

Primary Automation Platform: n8n

n8n is a workflow automation tool that you can self-host for free. Unlike Zapier (which abstracts the HTTP layer), n8n exposes every HTTP request, every JSON payload, and every transformation. This makes it the ideal laboratory for understanding what actually happens when you "send an event to Meta."

n8n ≠ "a free Zapier"

n8n and Zapier solve the same category of problem (workflow automation), but they are architecturally different. n8n is a technical tool that exposes raw data flows. Zapier is a consumer tool that abstracts them. In this laboratory, n8n is where you learn the architecture. Zapier is where you reproduce it for the client.

Tool Inventory

ToolCostLab UsageNotes
n8n (self-hosted)FREEPrimary automation labInstall via npm or Docker
n8n CloudFREE TRIAL (14 days)Alternative if Docker unavailableNo credit card required for trial
Meta Business SuiteFREEPixel, Events Manager, CAPIRequires Facebook account
GitHub PagesFREETest websiteStatic hosting, custom JS allowed
TypeformFREE (Basic)Test form10 questions/form, 10 responses/month on free plan
CalendlyFREE (Basic)Test booking page1 event type on free plan
Google SheetsFREELead trackingRequires Google account
Stripe (test mode)FREETest paymentsTest card: 4242 4242 4242 4242
ZapierFREE / PAIDClient-facing reproductionFree plan: 100 tasks/month, 2-step Zaps only

Free-Sandbox Limitations

Typeform Free: Limited to 10 responses/month. Redirect-on-completion may require a paid plan. The embed SDK's onSubmit callback is available on all plans.

Calendly Free: Only 1 event type. No Zapier integration (requires Standard plan, $10/month). Webhooks require a developer account (free).

Zapier Free: 100 tasks/month, 5 Zaps, 2-step Zaps only. Multi-step Zaps (with filters/conditionals) require Professional plan ($29.99/month).

n8n self-hosted: Fully free and unlimited. Requires Node.js 18+ or Docker.

Installation: n8n Self-Hosted

Choose one method:

Option A: npm (simplest for learning)

npx n8n

This downloads and starts n8n. Access the UI at http://localhost:5678. Your workflows are saved locally.

Option B: Docker (recommended for persistence)

docker run -it --rm \
  --name n8n \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n

Access at http://localhost:5678. The -v flag persists your data.

Option C: n8n Cloud Trial

Go to n8n.io → Start Free → Create account. 14-day trial, no credit card. Webhooks work without tunneling.

Webhook accessibility note

If you use self-hosted n8n (Options A/B), your webhooks are on localhost and unreachable from external services (Calendly, Stripe). You need a tunnel. Options: ngrok http 5678 (free), cloudflared tunnel (free), or set n8n's WEBHOOK_URL environment variable to your tunnel URL. n8n Cloud (Option C) handles this automatically.

Part 9 — Complete Implementation

20 layers. Work through them in order. Do not skip any layer.

LAYER 01

Meta Foundation

OBJECTIVE

Set up and understand the Meta infrastructure that receives conversion events.

WHY IT EXISTS

Every conversion event in this laboratory is ultimately sent to Meta. Before building anything, you must understand what Meta's tracking infrastructure is, how it works, and where to verify that events arrive.

ARCHITECTURE
BROWSER (visitor's device)          SERVER (your automation)
         │                                    │
    Meta Pixel JS                        HTTP POST
    (fbevents.js)                   (Conversions API)
         │                                    │
         └──────────┐          ┌──────────────┘
                    ▼          ▼
              ┌─────────────────────┐
              │   META DATASET      │
              │   (one Pixel ID)    │
              │   (two inputs)      │
              └─────────────────────┘
                        │
                        ▼
              ┌─────────────────────┐
              │   EVENTS MANAGER    │
              │   (view, test,      │
              │    verify events)   │
              └─────────────────────┘
CONCEPTS

Meta Business Suite / Business Manager: The administrative container for all business assets — ad accounts, Pages, Pixels/Datasets, catalogs, users. Everything lives inside this container.

Dataset (formerly "Pixel"): Meta now uses the term "Dataset" for the unified tracking asset. A Dataset has one ID (the Pixel ID) and accepts events from two sources: browser-side (Meta Pixel JavaScript) and server-side (Conversions API). Both paths feed the same Dataset.

Meta Pixel: A JavaScript snippet (fbevents.js) that runs in the visitor's browser. It sends events to Meta when the visitor performs actions on your website. It has access to browser cookies (_fbp, _fbc) that help Meta identify the visitor.

Conversions API (CAPI): A server-to-server HTTP API. Your server sends an HTTP POST request to https://graph.facebook.com/v21.0/{PIXEL_ID}/events with event data. No browser is involved. The API version may change — always verify the current version at developers.facebook.com/docs/graph-api/changelog.

API Version Notice

As of mid-2026, Meta Graph API v26.0 is the latest version. However, Conversions API calls typically work with versions v18.0 and above. This lab uses v21.0 in examples. Before production implementation, check the Graph API Changelog for the current version and any deprecation notices.

Events Manager: The Meta dashboard where you see incoming events, test events, verify event parameters, check Event Match Quality, and diagnose problems. This is your primary verification tool throughout the lab.

Test Events: A feature in Events Manager that lets you send events with a test_event_code. Test events appear in a separate tab and do not affect your ad delivery or reporting. Use this for all laboratory work.

PREREQUISITES
EXACT STEPS
  1. Access Meta Business Suite. Go to business.facebook.com. If you don't have a Business account, create one — it's free. You'll need a Facebook Page (create a test one if needed).
  2. Navigate to Events Manager. In the left sidebar, find "Events Manager" (or go to facebook.com/events_manager2 directly). This is where all event data flows.
  3. Locate or create a Dataset. In Events Manager, look at "Data Sources" in the left panel. If you have an existing Pixel/Dataset, click it. If not, click "Connect Data Sources" → "Web" → name it (e.g., "Lab Test Dataset") → "Create." Note the Pixel ID — a numeric string like 123456789012345. You will use this ID in every CAPI request.
  4. Generate an access token. In Events Manager, select your Dataset → Settings → scroll to "Conversions API" section → "Generate access token." Click through the prompts. Copy this token and store it securely — you will need it for every CAPI HTTP request. This token does not expire but can be revoked.
  5. Find the Test Events tab. In Events Manager, select your Dataset → "Test Events" tab. Note the Test Event Code displayed (e.g., TEST12345). Any CAPI request that includes this code in the payload will appear here without affecting production data.
  6. Record your credentials. Save these three values — you will need them throughout the lab:
PIXEL_ID       = ___________________________
ACCESS_TOKEN   = ___________________________
TEST_EVENT_CODE = ___________________________
DATA MAPPING

No data mapping in this layer — we are setting up infrastructure, not sending events.

TRIGGER

No trigger in this layer.

WORKFLOW

No workflow in this layer.

MANUAL INTERVENTION

All steps are manual in this layer — this is one-time setup.

TRANSMISSION

No event transmission in this layer.

META EVENT

No event sent yet.

META CONFIGURATION

Verify: Dataset is active (green dot in Events Manager). Access token is generated. Test Event Code is visible.

TEST

Send a manual test event using curl to verify your credentials work:

curl -X POST \
  "https://graph.facebook.com/v21.0/YOUR_PIXEL_ID/events" \
  -H "Content-Type: application/json" \
  -d '{
    "data": [{
      "event_name": "PageView",
      "event_time": CURRENT_UNIX_TIMESTAMP,
      "action_source": "website",
      "user_data": {
        "client_ip_address": "1.2.3.4",
        "client_user_agent": "Mozilla/5.0 Test"
      }
    }],
    "test_event_code": "YOUR_TEST_EVENT_CODE",
    "access_token": "YOUR_ACCESS_TOKEN"
  }'

Replace YOUR_PIXEL_ID, CURRENT_UNIX_TIMESTAMP (get it: date +%s in terminal, or use an online converter), YOUR_TEST_EVENT_CODE, and YOUR_ACCESS_TOKEN.

VERIFICATION

Go to Events Manager → Test Events tab. You should see a PageView event appear within 30 seconds.

EXPECTED RESULT

A PageView event in the Test Events tab with "Conversions API" as the source. The response from curl should be:

{"events_received": 1, "messages": [], "fbtrace_id": "..."}
TROUBLESHOOTING
Error: "Invalid OAuth access token"

Your access token is wrong or has been revoked. Go back to Events Manager → Settings → generate a new one.

Error: "Invalid parameter" or "(#100)"

Check your JSON syntax carefully. Common issues: missing commas, wrong quotes (use straight double quotes, not curly), event_time must be a number (no quotes), and the timestamp must be within the last 7 days.

Event does not appear in Test Events

Wait 60 seconds and refresh. Check that your test_event_code matches exactly (case-sensitive). Check the curl response — if events_received is 1, the event was accepted; give Meta time to process it.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. What is the difference between the Meta Pixel and the Conversions API?

2. Why does one Dataset have two input paths (browser and server)?

3. What does the Test Event Code do, and why should you use it during development?

4. What would happen if you sent a CAPI event without the test_event_code?

LAYER 02

Free Website + Meta Pixel Installation

OBJECTIVE

Create a free test website and install the Meta Pixel to track browser-side page views.

WHY IT EXISTS

The client's real site is on Shopify, but for this laboratory we use a free static website (GitHub Pages). This lets you understand Pixel installation at the HTML level — without Shopify's abstraction. In production, Shopify installs the Pixel via its Meta channel app, but the underlying mechanism is the same: a JavaScript snippet in the page's <head>.

ARCHITECTURE
VISITOR (browser)
    │
    ▼
┌────────────────────────────┐
│  YOUR WEBSITE              │
│  (GitHub Pages)            │
│                            │
│  <head>                    │
│    Meta Pixel base code    │
│    fbq('init', PIXEL_ID)  │
│    fbq('track','PageView')│
│  </head>                   │
└────────────────────────────┘
    │
    │  fbevents.js fires
    │  HTTP request to Meta
    ▼
┌────────────────────────────┐
│  META EVENTS MANAGER       │
│  → PageView received       │
│  → Source: Pixel (browser) │
└────────────────────────────┘
CONCEPTS

Meta Pixel base code: A JavaScript snippet that loads fbevents.js, initializes with your Pixel ID, and fires a PageView event on every page load. This is the foundation of browser-side tracking.

GitHub Pages: Free static website hosting from GitHub. You push HTML files to a repository and GitHub serves them at https://yourusername.github.io/reponame/. You have full control over the HTML, including the <head> section where the Pixel goes.

Why not Shopify for the lab: Shopify requires a paid plan ($39/month+). The Meta channel on Shopify installs the Pixel automatically — useful for a real client, but it hides the mechanism. You need to understand what the Pixel code actually is before relying on a platform to install it for you.

PREREQUISITES
EXACT STEPS
  1. Create a GitHub repository. Go to github.com → New repository → Name it meta-capi-lab → Public → check "Add a README file" → Create.
  2. Enable GitHub Pages. In the repository, go to Settings → Pages → Source: "Deploy from a branch" → Branch: main, folder: / (root) → Save. Your site will be live at https://YOURUSERNAME.github.io/meta-capi-lab/ within a minute.
  3. Create index.html. In the repository, click "Add file" → "Create new file" → name it index.html → paste the following:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ScaleUp Partner — Discovery Call</title>

  <!-- META PIXEL BASE CODE -->
  <script>
    !function(f,b,e,v,n,t,s)
    {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
    n.callMethod.apply(n,arguments):n.queue.push(arguments)};
    if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
    n.queue=[];t=b.createElement(e);t.async=!0;
    t.src=v;s=b.getElementsByTagName(e)[0];
    s.parentNode.insertBefore(t,s)}(window, document,'script',
    'https://connect.facebook.net/en_US/fbevents.js');
    fbq('init', 'YOUR_PIXEL_ID');
    fbq('track', 'PageView');
  </script>
  <noscript><img height="1" width="1" style="display:none"
    src="https://www.facebook.com/tr?id=YOUR_PIXEL_ID&ev=PageView&noscript=1"
  /></noscript>
  <!-- END META PIXEL BASE CODE -->

  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; background: #f8f9fa;
           color: #1a1a2e; line-height: 1.6; }
    .hero { max-width: 640px; margin: 3rem auto; padding: 2rem;
            background: #fff; border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
    h1 { font-size: 1.6rem; margin-bottom: 0.5rem; }
    p { margin: 0.5rem 0; color: #555; }
    .cta { display: inline-block; margin-top: 1rem; padding: 0.8rem 2rem;
           background: #1565E6; color: #fff; text-decoration: none;
           border-radius: 8px; font-weight: 600; }
    #typeform-container { margin-top: 2rem; min-height: 500px; }
  </style>
</head>
<body>
  <div class="hero">
    <h1>Get More Qualified Leads for Your Business</h1>
    <p>ScaleUp Partner helps B2B companies generate qualified enquiries
       through Meta advertising. Fill out the form below to see if we're
       a good fit.</p>
    <div id="typeform-container">
      <!-- Typeform will be embedded here in Layer 3 -->
      <p style="color:#999; text-align:center; padding:3rem;">
        [Typeform embed placeholder — completed in Layer 3]
      </p>
    </div>
  </div>
</body>
</html>
  1. Replace YOUR_PIXEL_ID in both places (the fbq('init',...) call and the <noscript> image) with your actual Pixel ID from Layer 1.
  2. Commit the file. Scroll down → "Commit new file."
  3. Wait ~60 seconds for GitHub Pages to deploy, then visit your site URL.
  4. Install Meta Pixel Helper. In Chrome, install the "Meta Pixel Helper" extension from the Chrome Web Store. It shows a badge on pages where the Pixel is detected.
  5. Visit your site. Open https://YOURUSERNAME.github.io/meta-capi-lab/. The Pixel Helper should show a green badge with "1" (one PageView event detected).
TEST

Open Events Manager → Overview or Test Events. Within a few minutes, you should see a PageView event from your Pixel, with "Pixel" as the connection method.

VERIFICATION
EXPECTED RESULT

A live website with Meta Pixel installed. Every visit generates a PageView event visible in Events Manager.

TROUBLESHOOTING
Pixel Helper shows no events / red badge

View page source (Ctrl+U) and verify the Pixel base code is present and your Pixel ID is correct. Check the browser console (F12 → Console) for JavaScript errors. Ensure you replaced both instances of YOUR_PIXEL_ID.

Site shows 404

GitHub Pages takes 1–2 minutes to deploy. Check Settings → Pages to confirm it's enabled. Ensure the file is named exactly index.html (lowercase).

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. What does fbq('init', 'PIXEL_ID') do?

2. What does fbq('track', 'PageView') do?

3. Why is the Pixel code placed in the <head> rather than the <body>?

4. In the real client project, how would the Pixel be installed on Shopify instead of manually?

LAYER 03

Typeform Creation & Embedding

OBJECTIVE

Create a qualifying Typeform and embed it in your test website. Understand the boundary between the website and the embedded form.

WHY IT EXISTS

The client's funnel starts with a Typeform that captures lead information. You need to understand exactly how an embedded Typeform relates to the parent page — because this boundary determines whether the Meta Pixel can detect form submissions.

ARCHITECTURE
┌─────────────────────────────────────────────┐
│  YOUR WEBSITE  (GitHub Pages)               │
│  ┌─ Meta Pixel (runs here) ──────────────┐  │
│  │  Can see: page loads, clicks,         │  │
│  │  URL changes on THIS domain           │  │
│  └───────────────────────────────────────┘  │
│                                             │
│  ┌─ TYPEFORM IFRAME ─────────────────────┐  │
│  │  ╔═══════════════════════════════════╗ │  │
│  │  ║  Typeform renders HERE            ║ │  │
│  │  ║  Separate origin (typeform.com)   ║ │  │
│  │  ║  Pixel CANNOT see inside this     ║ │  │
│  │  ╚═══════════════════════════════════╝ │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘
CONCEPTS

iframe isolation: When Typeform is embedded on your page, it renders inside an <iframe>. The iframe's origin is typeform.com, not your domain. Due to browser same-origin policy, your Meta Pixel JavaScript cannot detect events inside the iframe — it cannot see form field inputs, button clicks, or submission. This is a fundamental browser security boundary.

Typeform embed SDK: Typeform provides an embed library (@typeform/embed) that offers callbacks — including onSubmit — that fire on the parent page when the user completes the form inside the iframe. This is the bridge that lets your page know something happened inside the Typeform.

PREREQUISITES
EXACT STEPS
  1. Create a Typeform account at typeform.com if you don't have one.
  2. Create a new form. Click "Create form" → Start from scratch. Name it "ScaleUp Partner — Discovery Application."
  3. Add these fields (matching the client's qualifying questions):
    Field 1: Short Text — "What is your full name?" (Required)
    Field 2: Email — "What is your email address?" (Required)
    Field 3: Phone Number — "What is your phone number?" (Required)
    Field 4: Dropdown — "What country are you based in?" (Add 5-10 countries)
    Field 5: Short Text — "What does your business do?"
    Field 6: Multiple Choice — "What is your monthly marketing budget?" (Options: Under €1,000 / €1,000–€5,000 / €5,000–€10,000 / Over €10,000)
    Field 7: Long Text — "What is your biggest marketing challenge right now?"
  4. Publish the form. Click "Publish." Note your form ID from the URL — it looks like https://form.typeform.com/to/AbCdEf. The AbCdEf part is your form ID.
  5. Get the embed code. On the Publish page, click "Embed" → "Inline" → copy the code. It will look something like:
<div data-tf-widget="AbCdEf" data-tf-inline-on-mobile
     data-tf-medium="snippet" style="width:100%;height:500px;">
</div>
<script src="//embed.typeform.com/next/embed.js"></script>
  1. Update your index.html. Replace the placeholder inside #typeform-container with the embed code from Typeform. Your updated container should look like:
<div id="typeform-container">
  <div data-tf-widget="AbCdEf" data-tf-inline-on-mobile
       data-tf-medium="snippet"
       data-tf-on-submit="typeformSubmitted"
       style="width:100%;height:500px;">
  </div>
  <script src="//embed.typeform.com/next/embed.js"></script>
</div>

Note the addition of data-tf-on-submit="typeformSubmitted" — this is the callback we'll use in Layer 4 to detect submission. Add it now.

  1. Commit and push. Wait for GitHub Pages to redeploy (~60 seconds).
  2. Test the embed. Visit your site. The Typeform should render inline on the page. Fill it out with test data to verify it works.
DATA MAPPING
Typeform FieldData CapturedUsed Later In
Full Namefn, lnCAPI user_data (hashed)
EmailemCAPI user_data (hashed), Google Sheet, identity chain
PhonephCAPI user_data (hashed), Google Sheet
CountrycountryCAPI user_data (hashed), Google Sheet
BusinessqualificationGoogle Sheet
BudgetqualificationGoogle Sheet
ChallengequalificationGoogle Sheet
TEST

Submit the form with test data. After submission, the Typeform should show a "Thank you" screen (or redirect, depending on settings). The Meta Pixel Helper should still show the PageView — but no Lead event yet. That comes in Layer 4.

VERIFICATION
TROUBLESHOOTING
Typeform shows a blank box or does not render

Check that the embed script URL is correct: //embed.typeform.com/next/embed.js. Ensure your form ID is correct. Try opening the form URL directly (https://form.typeform.com/to/AbCdEf) to verify it's published.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why can't the Meta Pixel detect form submission inside an embedded Typeform?

2. What is the same-origin policy and how does it apply here?

3. What does data-tf-on-submit do?

LAYER 04

Handshake #1 — Lead Event via Typeform SDK

OBJECTIVE

Fire a Meta Lead event when a visitor submits the Typeform. This is Handshake #1.

WHY IT EXISTS

This is the client's existing tracking mechanism — the only event Meta currently receives. We implement it properly so we understand the baseline before adding the four remaining handshakes.

ARCHITECTURE
Visitor submits Typeform
    │
    ▼ (Typeform embed SDK fires onSubmit callback)
    │
    ▼ (callback runs on PARENT page, where Pixel lives)
    │
    fbq('track', 'Lead')
    │
    ▼ (Pixel sends Lead event to Meta via browser)
    │
META EVENTS MANAGER → Lead received (source: Pixel)
CONCEPTS

The onSubmit callback bridge: When the Typeform embed SDK detects a successful submission inside the iframe, it calls the JavaScript function named in data-tf-on-submit on the parent page. This is the bridge across the iframe boundary. The callback receives the response ID and any hidden fields.

fbq('track', 'Lead'): This is a Meta Pixel standard event call. When executed, the Pixel JavaScript sends a Lead event to Meta via the visitor's browser. Because it runs in the browser, it has access to the _fbp cookie (first-party browser ID) and the _fbc cookie (click ID from fbclid parameter), which help Meta match the event back to the ad click.

EXACT STEPS
  1. Add the callback function to your index.html. Before the closing </body> tag, add this script:
<script>
  function typeformSubmitted(event) {
    // event.responseId contains the Typeform response ID
    console.log('Typeform submitted. Response ID:', event.responseId);

    // Fire the Meta Lead event via Pixel
    if (typeof fbq === 'function') {
      fbq('track', 'Lead', {
        content_name: 'Discovery Application',
        content_category: 'B2B Lead'
      });
      console.log('Meta Lead event fired via Pixel.');
    } else {
      console.error('fbq not found — Meta Pixel may not be loaded.');
    }
  }
</script>
  1. Commit and push. Wait for deployment.
  2. Open your site. Open the browser developer console (F12 → Console). Submit the Typeform with test data.
  3. Check the console. You should see both log messages: the response ID and the Lead event confirmation.
  4. Check Meta Pixel Helper. The badge should now show 2 events: PageView + Lead.
  5. Check Events Manager. Go to Events Manager → Overview or Test Events. You should see the Lead event.
DATA MAPPING
SourceMeta ParameterValue
Typeform submission eventevent_nameLead (standard event)
Pixel JavaScript_fbp cookieAuto-captured by Pixel
URL parameter_fbc cookieAuto-captured by Pixel (if fbclid present in URL)
BrowserIP address, user agentAuto-captured by Pixel
TRIGGER

Typeform form submission → onSubmit callback on parent page.

WORKFLOW

No automation platform involved. This is a pure browser-side event: Typeform SDK → JavaScript callback → Pixel → Meta.

MANUAL INTERVENTION

None. Fully automated — fires every time a visitor submits the form.

TRANSMISSION

Browser-side. The visitor's browser sends the event to Meta. This means: the visitor's IP address and user agent are automatically included; the _fbp and _fbc cookies provide identity; but the visitor's email and phone are not sent (the Pixel standard event call does not include form field data by default).

META EVENT
event_name: "Lead"
event source: Pixel (browser)
action_source: "website" (implicit — browser events are always website)
user_data: _fbp, _fbc, IP, user agent (auto-captured)
custom_data: content_name, content_category (from our fbq call)
META CONFIGURATION

Lead is a Meta standard event. It appears automatically in Events Manager once received. No Custom Conversion needed.

TEST

Submit the Typeform. Check: (1) browser console for log messages, (2) Pixel Helper for Lead event, (3) Events Manager for Lead event.

VERIFICATION
EXPECTED RESULT

Lead event in Events Manager with source "Pixel." Event Match Quality may be low because the browser event only carries cookies and IP — no hashed email or phone. This is normal for browser-side events and is one reason the Conversions API exists.

TROUBLESHOOTING
Console shows "fbq not found"

The Pixel base code is not loading. Check that the Pixel snippet is in the <head> and your Pixel ID is correct. Check for JavaScript errors above the callback in the console.

Typeform submits but no console log appears

The data-tf-on-submit="typeformSubmitted" attribute is missing from the Typeform embed div, or the function name doesn't match exactly (JavaScript is case-sensitive).

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why doesn't the Pixel automatically detect the Typeform submission?

2. What identity information does this browser-side Lead event carry?

3. Why might Event Match Quality be low for this event?

4. How could you improve identity matching for this event? (Hint: think about what the Conversions API can add.)

LAYER 05

Typeform → Calendly Redirect

OBJECTIVE

After Typeform submission, redirect the visitor to a Calendly booking page.

WHY IT EXISTS

In the client's funnel, after submitting the qualifying form, the visitor should be able to book a discovery call. This is a navigation step — moving the visitor from the form to the booking page.

ARCHITECTURE
Visitor submits Typeform
    │
    ▼ onSubmit callback fires
    │
    ├── fbq('track', 'Lead')   ← Handshake #1
    │
    └── window.location.href = Calendly URL   ← redirect
    │
    ▼
Visitor lands on Calendly booking page
CONCEPTS

Redirect mechanism: In the typeformSubmitted callback, after firing the Lead event, we redirect the visitor to the Calendly booking page. This is a simple JavaScript redirect. Alternatively, Typeform's own "Redirect on completion" setting (in form settings) can handle this — but that may require a paid Typeform plan.

Free-Sandbox Limitation

Typeform's built-in redirect-on-completion may require a paid plan (Plus or higher). In this lab, we use the JavaScript redirect in the onSubmit callback instead — this works on all plans because it runs on your page, not inside Typeform.

Calendly Free plan allows only 1 event type. Create a single "Discovery Call" event type for this lab.

PREREQUISITES
EXACT STEPS
  1. Create a Calendly account at calendly.com if you don't have one.
  2. Create an event type. Click "Create" → "One-on-One" → name it "Discovery Call" → set duration (30 min) → Save. Note your booking URL: https://calendly.com/yourusername/discovery-call.
  3. Update the typeformSubmitted function in your index.html to add the redirect:
<script>
  function typeformSubmitted(event) {
    console.log('Typeform submitted. Response ID:', event.responseId);

    // Fire the Meta Lead event via Pixel
    if (typeof fbq === 'function') {
      fbq('track', 'Lead', {
        content_name: 'Discovery Application',
        content_category: 'B2B Lead'
      });
      console.log('Meta Lead event fired via Pixel.');
    }

    // Redirect to Calendly after a short delay
    // (delay gives the Pixel time to send the Lead event)
    setTimeout(function() {
      window.location.href =
        'https://calendly.com/YOURUSERNAME/discovery-call';
    }, 1500);
  }
</script>
  1. Replace YOURUSERNAME with your actual Calendly username.
  2. Commit and push.
  3. Test: Submit the Typeform. After ~1.5 seconds, you should be redirected to the Calendly booking page.
TEST

Submit the form → verify Lead event fires (check Pixel Helper quickly before redirect) → confirm you land on the Calendly booking page.

VERIFICATION
TROUBLESHOOTING
Redirect happens but Lead event does not fire

The redirect may be happening before the Pixel has time to send the event. Increase the setTimeout delay to 2000–3000ms. In production, you might use the Pixel's fbq('track', 'Lead') with a callback, but the simple delay is sufficient for learning.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why do we use a setTimeout before redirecting?

2. Could we install the Meta Pixel on the Calendly page to capture the booking? Why or why not?

3. If this were a production implementation, what concerns would you have about the setTimeout approach?

LAYER 06

Google Sheets Lead Tracker

OBJECTIVE

Create a Google Sheet that tracks every lead through the entire funnel. This sheet serves as both a reporting tool and a trigger source for Handshakes #3 and #4.

WHY IT EXISTS

The client needs visibility into the full funnel. The sheet also serves as the trigger mechanism for the "attended" and "qualified" events — when a team member updates a row, it triggers an automation. This is where the manual business decisions (attendance, qualification) become automated technical transmissions.

EXACT STEPS
  1. Create a new Google Sheet. Go to sheets.google.com → Blank spreadsheet → Name it "ScaleUp Partner — Lead Tracker."
  2. Create the header row in Row 1 with these columns:
ColumnHeaderPurposePopulated By
ALead_IDUnique identifierAuto (formula or n8n)
BDateTypeform submission dateAuto
CNameFull nameTypeform
DEmailEmail addressTypeform
EPhonePhone numberTypeform
FCountryCountryTypeform
GTypeform_Response_IDTypeform's response IDAuto
HCalendly_BookedTRUE/FALSEn8n/Zapier
ICalendly_Event_URICalendly event identifiern8n/Zapier
JAttendedTRUE/FALSEManual (team member)
KQualifiedTRUE/FALSEManual (team member)
LSignedTRUE/FALSEStripe webhook / manual
MValueContract value (€)Stripe
NStripe_Payment_IDStripe payment identifiern8n/Zapier
OCallAttended_SentTimestampn8n (prevents duplicate send)
PQualifiedLead_SentTimestampn8n (prevents duplicate send)
QNotesFree textManual
  1. Add a Lead_ID formula. In cell A2, enter: =IF(D2="","",TEXT(ROW()-1,"0000")). This generates a simple sequential ID when an email is present. Drag down for 100 rows. In production, you would use a more robust ID (e.g., combining a date hash with a counter).
  2. Add a test row. In row 2, enter test data: a name, email (test@example.com), phone (+1234567890), and country. Leave Attended, Qualified, Signed as empty (they'll be filled manually later).
  3. Share the sheet. Click "Share" → add your Google account (the one n8n will use) as Editor. If using n8n Cloud or a service account, share with the appropriate email.

Why Lead_ID matters

Email alone is not a reliable primary identifier: people use multiple email addresses, typos occur, and email is PII that needs careful handling. A stable Lead_ID lets you track a lead across the funnel without depending solely on email matching. It also becomes part of event ID generation for deduplication (Layer 17).

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why are columns O and P (CallAttended_Sent, QualifiedLead_Sent) important?

2. What would happen if someone edited the Attended column twice without deduplication protection?

3. Why is Lead_ID superior to using email as the sole identifier?

LAYER 07

n8n Fundamentals

OBJECTIVE

Learn the core concepts of n8n by building a simple test workflow. Understand nodes, triggers, data flow, and executions before building the real CAPI workflows.

WHY IT EXISTS

n8n is where you will build the four server-side CAPI workflows (Handshakes #2–#5). Before doing that, you need to understand how n8n works: what a workflow is, how data flows between nodes, how webhooks trigger workflows, and how to inspect the data at each step.

CONCEPTS

Workflow: A sequence of connected nodes that processes data. Equivalent to a Zapier "Zap," but with more visibility and control.

Node: A single operation in a workflow — a trigger, a data transformation, an HTTP request, a conditional branch. Equivalent to a Zapier "step."

Trigger node: The first node in a workflow. It starts the workflow when an event occurs. Examples: Webhook (receives an HTTP request), Schedule (runs on a timer), Google Sheets Trigger (detects row changes).

Webhook: An HTTP endpoint that n8n creates for you. When an external service (Calendly, Stripe) sends an HTTP POST to this URL, n8n triggers the workflow.

HTTP Request node: Makes an HTTP request to an external API. This is what you will use to send events to Meta's Conversions API.

Code node: Runs custom JavaScript. You will use this for SHA-256 hashing of PII and event ID generation.

Execution: One run of a workflow. n8n logs every execution so you can see the input/output of each node — this is invaluable for debugging.

Expressions: Dynamic values in node fields. Syntax: {{ $json.fieldName }} to reference data from a previous node's output.

Credentials: Stored authentication details (API keys, OAuth tokens). Configured once, reusable across workflows.

EXACT STEPS
  1. Open n8n. Go to http://localhost:5678 (self-hosted) or your n8n Cloud URL. Create an account if prompted.
  2. Create a new workflow. Click "Add Workflow" → name it "Test — Webhook Echo."
  3. Add a Webhook trigger. Click "+" to add a node → search "Webhook" → select "Webhook." Set:
    HTTP Method: POST
    Path: test-echo
    This creates an endpoint like http://localhost:5678/webhook/test-echo (or your cloud URL equivalent).
  4. Add a Set node. Click "+" → search "Set" → select "Edit Fields (Set)." Add a field:
    Name: received_message
    Value: {{ $json.body.name || 'no name provided' }}
    Add another field:
    Name: timestamp
    Value: {{ new Date().toISOString() }}
  5. Add a Respond to Webhook node. Click "+" → search "Respond" → select "Respond to Webhook." Set Response Body to:
    {{ JSON.stringify({ status: 'ok', received: $json.received_message, at: $json.timestamp }) }}
  6. Connect the nodes: Webhook → Set → Respond to Webhook.
  7. Activate the workflow. Toggle the "Active" switch in the top right.
  8. Test it. Open a terminal and run:
curl -X POST http://localhost:5678/webhook/test-echo \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Student", "email": "test@example.com"}'

You should receive a JSON response with the status, received name, and timestamp.

  1. Inspect the execution. In n8n, go to "Executions" (left sidebar). Click the latest execution. Click each node to see its input and output data. This is how you will debug every workflow in this lab.

This is the key difference from Zapier

In n8n, you can see the raw HTTP request that arrived, the exact JSON body, every transformation, and the exact HTTP request that went out. Zapier abstracts this. When something goes wrong in Zapier, you see "Task failed." In n8n, you see the exact HTTP status code, the error response body, and the data that caused it.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. What is the difference between a Webhook trigger and a Schedule trigger?

2. What does the expression {{ $json.body.name }} mean?

3. Where do you look in n8n when a workflow fails?

4. How is this different from building the same thing in Zapier?

LAYER 08

CAPI Fundamentals — Raw API Anatomy

OBJECTIVE

Understand the exact structure of a Conversions API request. Build and send a manual CAPI event from n8n, with proper PII hashing.

WHY IT EXISTS

Before building automated workflows, you must understand what happens at the HTTP level when you "send an event to Meta." Every Zapier action and every n8n HTTP Request node is ultimately making this same HTTP POST request. If you understand the raw API, you can debug any integration on any platform.

ARCHITECTURE
YOUR SERVER (n8n)
    │
    │  HTTP POST
    │  https://graph.facebook.com/v21.0/{PIXEL_ID}/events
    │
    │  Headers:
    │    Content-Type: application/json
    │
    │  Body (JSON):
    │  {
    │    "data": [{
    │      "event_name": "Lead",
    │      "event_time": 1700000000,
    │      "event_id": "unique-id-here",
    │      "action_source": "website",
    │      "user_data": {
    │        "em": ["sha256-hashed-email"],
    │        "ph": ["sha256-hashed-phone"],
    │        "fn": ["sha256-hashed-first-name"],
    │        "ln": ["sha256-hashed-last-name"],
    │        "client_ip_address": "1.2.3.4",
    │        "client_user_agent": "Mozilla/5.0..."
    │      },
    │      "custom_data": {}
    │    }],
    │    "access_token": "YOUR_ACCESS_TOKEN",
    │    "test_event_code": "TEST12345"
    │  }
    │
    ▼
META GRAPH API
    │
    ▼
Response: {"events_received": 1, "messages": []}
CONCEPTS

Endpoint: https://graph.facebook.com/v21.0/{PIXEL_ID}/events — replace {PIXEL_ID} with your Pixel ID. Verify the current API version at developers.facebook.com/docs/graph-api/changelog.

Required fields in every CAPI event:

FieldTypeDescription
event_namestringThe event name: Lead, Schedule, Purchase, or custom names like CallAttended
event_timeintegerUnix timestamp (seconds). Must be within the last 7 days.
action_sourcestring"website" for events originating from web interactions. "system_generated" for internal business process events (attendance, qualification).
user_dataobjectIdentity information. At minimum: one of em, ph, or external_id.

SHA-256 hashing: All PII in user_data must be hashed with SHA-256 before sending. The process: normalize (lowercase, trim whitespace) → hash. Meta hashes its own data the same way and compares hashes to match users.

Hashing rules:

FieldNormalizeThen Hash
em (email)lowercase, trimSHA-256
ph (phone)digits only (remove +, -, spaces, parens), include country codeSHA-256
fn (first name)lowercase, trimSHA-256
ln (last name)lowercase, trimSHA-256
ct (city)lowercase, trim, no punctuationSHA-256
country2-letter ISO code, lowercaseSHA-256

Fields that are NOT hashed: client_ip_address, client_user_agent, fbc, fbp, external_id.

EXACT STEPS — Build a CAPI test event in n8n
  1. Create a new n8n workflow. Name it "Test — Manual CAPI Event."
  2. Add a Manual Trigger. Search "Manual" → select "Manual Trigger." This lets you run the workflow on demand by clicking "Test Workflow."
  3. Add a Code node (search "Code"). This node will build the CAPI payload with proper hashing. Paste this JavaScript:
const crypto = require('crypto');

// --- Test identity data ---
const email = 'test@example.com';
const phone = '+1 (234) 567-8900';
const firstName = 'Test';
const lastName = 'Student';

// --- Hashing helper ---
function sha256(value) {
  return crypto.createHash('sha256')
    .update(value)
    .digest('hex');
}

// --- Normalize and hash ---
const hashedEmail = sha256(email.toLowerCase().trim());
const hashedPhone = sha256(phone.replace(/[^0-9]/g, ''));
const hashedFn = sha256(firstName.toLowerCase().trim());
const hashedLn = sha256(lastName.toLowerCase().trim());

// --- Build the CAPI payload ---
const payload = {
  data: [{
    event_name: 'Lead',
    event_time: Math.floor(Date.now() / 1000),
    event_id: 'test-lead-' + Date.now(),
    action_source: 'website',
    user_data: {
      em: [hashedEmail],
      ph: [hashedPhone],
      fn: [hashedFn],
      ln: [hashedLn],
      client_ip_address: '1.2.3.4',
      client_user_agent: 'Mozilla/5.0 (Lab Test)'
    },
    custom_data: {
      content_name: 'Discovery Application'
    }
  }],
  access_token: 'YOUR_ACCESS_TOKEN',
  test_event_code: 'YOUR_TEST_EVENT_CODE'
};

return [{ json: payload }];
  1. Replace YOUR_ACCESS_TOKEN and YOUR_TEST_EVENT_CODE with your actual values from Layer 1.
  2. Add an HTTP Request node. Configure:
    Method: POST
    URL: https://graph.facebook.com/v21.0/YOUR_PIXEL_ID/events
    Body Content Type: JSON
    Body: use Expression → {{ JSON.stringify($json) }}
    (Or set "Send Body" → "Specify Body" → "JSON" and reference the previous node's output)
  3. Connect: Manual Trigger → Code → HTTP Request.
  4. Click "Test Workflow."
  5. Inspect each node's output. Click the Code node — verify the hashed values. Click the HTTP Request node — verify the response is {"events_received": 1}.
  6. Check Events Manager → Test Events. Your Lead event should appear with "Conversions API" as the source.

What you just did

You built the exact same HTTP request that Zapier's "Facebook Conversions" action makes internally. The difference: you can see every byte of the request and response. When Zapier says "Task failed," you'll know where to look because you understand what's underneath.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why must PII be hashed before sending to Meta?

2. What is the normalization step and why is it necessary before hashing?

3. What is action_source and when would you use "website" vs "system_generated"?

4. What is event_id and why is it important? (Detailed answer in Layer 17.)

5. What happens if event_time is older than 7 days?

LAYER 09

Handshake #2 — Calendly → n8n → CAPI → Schedule

OBJECTIVE

Build an automated workflow: when a Calendly booking occurs, send a Schedule event to Meta via the Conversions API.

WHY IT EXISTS

This is the first fully automated server-side handshake. A visitor books a call on Calendly → Calendly sends a webhook to n8n → n8n transforms the data and sends it to Meta. The client specifically requested this as "Zap 1."

ARCHITECTURE
CALENDLY
  │ invitee.created webhook
  │ (name, email, questions_and_answers, event URI)
  ▼
N8N WEBHOOK TRIGGER
  │
  ▼
CODE NODE (extract data, normalize, SHA-256 hash)
  │
  ▼
HTTP REQUEST NODE
  │ POST https://graph.facebook.com/v21.0/{PIXEL_ID}/events
  │ event_name: "Schedule"
  │ user_data: { em, ph, fn, ln }
  │ action_source: "website"
  ▼
META EVENTS MANAGER → Schedule received
CONCEPTS

Calendly webhooks: Calendly can send HTTP POST notifications when events occur. The key event for us is invitee.created — fired when someone books a meeting. The webhook payload includes the invitee's name, email, and answers to any custom questions you've set up. Phone number may be included if you've added a phone field to the booking form.

Calendly Webhook Setup

Calendly Free plan: Does not include API webhook subscriptions through the Calendly dashboard. However, you can create webhook subscriptions via the Calendly API using a Personal Access Token (free). Alternatively, n8n has a built-in Calendly trigger node that handles this if you connect your Calendly account.

To get a Personal Access Token: Go to calendly.com → Integrations → API & Webhooks → Personal Access Tokens → Create.

PREREQUISITES
EXACT STEPS
  1. Create a new n8n workflow. Name it "Handshake #2 — Calendly → Schedule."
  2. Add a Webhook trigger node. Set:
    HTTP Method: POST
    Path: calendly-schedule
    Note the webhook URL (e.g., https://your-tunnel.ngrok.io/webhook/calendly-schedule).
  3. Register the webhook with Calendly. Option A — use n8n's built-in Calendly Trigger node (requires connecting your Calendly account via OAuth or Personal Access Token). Option B — manually register via the Calendly API:
curl -X POST https://api.calendly.com/webhook_subscriptions \
  -H "Authorization: Bearer YOUR_CALENDLY_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-n8n-url/webhook/calendly-schedule",
    "events": ["invitee.created"],
    "organization": "https://api.calendly.com/organizations/YOUR_ORG_ID",
    "scope": "organization"
  }'

To find your organization URI, call: curl -H "Authorization: Bearer YOUR_PAT" https://api.calendly.com/users/me and look at current_organization.

  1. Add a Code node for data extraction and hashing. The Calendly webhook payload structure includes the invitee's name and email in payload.invitee:
const crypto = require('crypto');

function sha256(val) {
  return crypto.createHash('sha256').update(val).digest('hex');
}

// Extract from Calendly webhook payload
const invitee = $json.body?.payload?.invitee || $json.payload?.invitee || {};
const event = $json.body?.payload?.event || $json.payload?.event || {};

const email = (invitee.email || '').toLowerCase().trim();
const name = (invitee.name || '').trim();
const nameParts = name.split(' ');
const firstName = nameParts[0] || '';
const lastName = nameParts.slice(1).join(' ') || '';

// Look for phone in questions_and_answers if configured
const questions = invitee.questions_and_answers || [];
const phoneAnswer = questions.find(q =>
  q.question.toLowerCase().includes('phone'));
const phone = phoneAnswer ? phoneAnswer.answer.replace(/[^0-9]/g, '') : '';

// Calendly event URI — used as the basis for event_id
const calendlyEventUri = event.uri || '';
const calendlyEventId = calendlyEventUri.split('/').pop() || '';

// Build CAPI payload
const payload = {
  data: [{
    event_name: 'Schedule',
    event_time: Math.floor(Date.now() / 1000),
    event_id: 'schedule-' + calendlyEventId,
    action_source: 'website',
    user_data: {
      em: email ? [sha256(email)] : [],
      ph: phone ? [sha256(phone)] : [],
      fn: firstName ? [sha256(firstName.toLowerCase())] : [],
      ln: lastName ? [sha256(lastName.toLowerCase())] : []
    }
  }],
  access_token: 'YOUR_ACCESS_TOKEN',
  test_event_code: 'YOUR_TEST_EVENT_CODE'
};

return [{ json: payload }];
  1. Add an HTTP Request node. Configure:
    Method: POST
    URL: https://graph.facebook.com/v21.0/YOUR_PIXEL_ID/events
    Send Body: JSON, set to expression {{ JSON.stringify($json) }}
  2. Connect: Webhook → Code → HTTP Request.
  3. Activate the workflow.
  4. Test it. Go to your Calendly booking page and book a test appointment using a test email. After booking, check n8n's execution log to verify the workflow ran. Check Events Manager → Test Events for the Schedule event.
DATA MAPPING
Calendly Fieldn8n TransformationCAPI Field
invitee.emaillowercase, trim, SHA-256user_data.em
invitee.namesplit, lowercase, SHA-256user_data.fn, user_data.ln
questions_and_answers[phone]digits only, SHA-256user_data.ph
event.uriextract ID → prefix with "schedule-"event_id
TRIGGER

Calendly invitee.created webhook → n8n Webhook node.

MANUAL INTERVENTION

None. Fully automated.

TRANSMISSION

Server-to-server. n8n sends the HTTP POST to Meta. No browser involved.

META EVENT
event_name: "Schedule" (standard event)
action_source: "website"
event_id: "schedule-{calendly_event_id}" (for deduplication)
user_data: em, ph, fn, ln (all SHA-256 hashed)
META CONFIGURATION

Schedule is a Meta standard event. It appears automatically in Events Manager. No Custom Conversion required.

VERIFICATION
TROUBLESHOOTING
Webhook not firing

Verify the webhook URL is accessible from the internet (not just localhost). Check your ngrok tunnel is running. Verify the webhook subscription is active: curl -H "Authorization: Bearer YOUR_PAT" https://api.calendly.com/webhook_subscriptions?organization=YOUR_ORG_URI

Phone field is empty

Calendly doesn't include phone by default. You need to add a "Phone Number" custom question to your event type in Calendly settings. Go to Event Type → "Invitee Questions" → add a phone question.

Meta returns an error about event_time

The event_time must be a Unix timestamp in seconds (not milliseconds) and within the last 7 days. Verify: Math.floor(Date.now() / 1000) gives seconds.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why do we use the Calendly event URI as the basis for event_id instead of a random value?

2. Why is action_source set to "website" even though this is a server-to-server request?

3. What identity information is available from Calendly? What is NOT available?

4. If the client also wanted to fire a browser-side Schedule event (e.g., on a thank-you page), how would deduplication work?

LAYER 10

Handshake #3 — Attendance → n8n → CAPI → CallAttended

OBJECTIVE

Build the workflow for the CallAttended event: when a team member marks a lead as having attended the call (in Google Sheets), send the event to Meta.

WHY IT EXISTS

The client requested: "Call attended (marked in Calendly) → Fire Meta custom event 'CallAttended'." As we identified in Part 2, Calendly does not have an "attended" status. The real architecture uses Google Sheets as the trigger: a human marks Attended = TRUE, and the automation fires.

ARCHITECTURE
DISCOVERY CALL OCCURS
    │
    ▼ (human decision)
TEAM MEMBER opens Google Sheet
    │
    ▼ (manual action)
Sets column J (Attended) = TRUE
    │
    ▼ (automatic trigger)
N8N — Google Sheets Trigger (row updated)
    │
    ▼ (condition check)
IF node: Attended = TRUE AND CallAttended_Sent is empty
    │
    ▼ (data extraction)
CODE NODE: read email, phone from sheet row → SHA-256 hash
    │
    ▼ (API call)
HTTP REQUEST: POST to CAPI → event_name: "CallAttended"
    │
    ▼ (record keeping)
GOOGLE SHEETS NODE: write timestamp to CallAttended_Sent column
    │
    ▼
META EVENTS MANAGER → CallAttended received
CONCEPTS

Manual business decision vs automated transmission: This is the clearest example of the separation. "Did the prospect attend the call?" is a human judgment. "Send the CallAttended event to Meta" is an automated technical step. The Google Sheet is the bridge between the two worlds.

Duplicate-send prevention: If someone edits the Attended column again (e.g., corrects a typo in another column on the same row), the Google Sheets trigger may fire again. Without protection, Meta would receive a duplicate CallAttended event. Column O (CallAttended_Sent) prevents this: the workflow only fires if this column is empty, and it fills the column with a timestamp after successful transmission.

EXACT STEPS
  1. Create a new n8n workflow. Name it "Handshake #3 — Attendance → CallAttended."
  2. Add a Google Sheets Trigger. Connect your Google account (n8n will ask for OAuth permission). Configure:
    Spreadsheet: Select your "Lead Tracker" sheet
    Sheet: Sheet1 (or whatever your sheet name is)
    Trigger On: Row Updated
    Poll interval: Every minute (for testing; increase in production)
  3. Add an IF node. Condition: {{ $json.Attended }} equals TRUE AND {{ $json.CallAttended_Sent }} is empty.
  4. Add a Code node (connected to the IF node's "true" output):
const crypto = require('crypto');

function sha256(val) {
  return crypto.createHash('sha256').update(val).digest('hex');
}

const email = ($json.Email || '').toLowerCase().trim();
const phone = ($json.Phone || '').replace(/[^0-9]/g, '');
const name = ($json.Name || '').trim();
const nameParts = name.split(' ');
const firstName = nameParts[0] || '';
const lastName = nameParts.slice(1).join(' ') || '';
const leadId = $json.Lead_ID || '';

const payload = {
  data: [{
    event_name: 'CallAttended',
    event_time: Math.floor(Date.now() / 1000),
    event_id: 'callattended-' + leadId,
    action_source: 'system_generated',
    user_data: {
      em: email ? [sha256(email)] : [],
      ph: phone ? [sha256(phone)] : [],
      fn: firstName ? [sha256(firstName.toLowerCase())] : [],
      ln: lastName ? [sha256(lastName.toLowerCase())] : [],
      external_id: leadId ? [sha256(leadId)] : []
    }
  }],
  access_token: 'YOUR_ACCESS_TOKEN',
  test_event_code: 'YOUR_TEST_EVENT_CODE'
};

return [{ json: { ...payload, _rowNumber: $json._rowNumber || $json.rowIndex } }];
  1. Add an HTTP Request node. Same configuration as Layer 9 — POST to CAPI endpoint.
  2. Add a Google Sheets node (Update Row). Configure:
    Spreadsheet: Lead Tracker
    Sheet: Sheet1
    Row Number: {{ $json._rowNumber }} (from the Code node output)
    Column O (CallAttended_Sent): {{ new Date().toISOString() }}
  3. Connect: Google Sheets Trigger → IF → (true) Code → HTTP Request → Google Sheets Update.
  4. Activate the workflow.
  5. Test: Open your Google Sheet. In a test row that has an email, set Attended = TRUE. Wait for the poll interval. Check n8n execution log, then Events Manager.

action_source: "system_generated"

For this event, we use action_source: "system_generated" rather than "website". The attendance decision is an internal business process — it didn't happen on a website. Meta documentation specifies that system_generated is for events that occur "as a result of your system" rather than a direct user action on a website or app.

VERIFICATION
TROUBLESHOOTING
Google Sheets Trigger not detecting changes

Check: (1) the poll interval — if set to every minute, wait at least 60 seconds; (2) the OAuth connection is valid; (3) the sheet is shared with the correct Google account; (4) you're editing the correct sheet (Sheet1 vs Sheet2).

IF node always goes to false branch

Check the exact value in the Attended column. It must be exactly TRUE (or a boolean true, depending on how Google Sheets reports it). Use n8n's expression editor to inspect the actual value. Try: {{ $json.Attended === true || $json.Attended === 'TRUE' || $json.Attended === 'true' }}.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why is action_source set to "system_generated" here but "website" for Schedule?

2. Why do we need the CallAttended_Sent column?

3. Is CallAttended a standard Meta event or a custom event? How does Meta treat it?

4. What would happen if the team accidentally set Attended = TRUE for the wrong person?

LAYER 11

Handshake #4 — Sheets → n8n → CAPI → QualifiedLead

OBJECTIVE

Build the workflow for the QualifiedLead event: when Qualified = TRUE in Google Sheets, send the event to Meta.

WHY IT EXISTS

This is architecturally identical to Layer 10 — same trigger mechanism (Google Sheets row update), same conditional logic (check column + deduplication guard), same CAPI transmission pattern. The only differences are: the column being watched (K: Qualified instead of J: Attended), the event name (QualifiedLead instead of CallAttended), and the deduplication column (P: QualifiedLead_Sent instead of O: CallAttended_Sent).

ARCHITECTURE
Same as Layer 10, but:
- Watch column: K (Qualified)
- Guard column: P (QualifiedLead_Sent)
- event_name: "QualifiedLead"
- event_id: "qualifiedlead-{Lead_ID}"
EXACT STEPS
  1. Create a new n8n workflow. Name it "Handshake #4 — Qualified → QualifiedLead."
  2. Duplicate the Layer 10 workflow (or build from scratch following the same pattern).
  3. Change the IF node: Check Qualified = TRUE AND QualifiedLead_Sent is empty.
  4. Change the Code node: Set event_name: 'QualifiedLead' and event_id: 'qualifiedlead-' + leadId.
  5. Change the Google Sheets Update node: Write to column P (QualifiedLead_Sent) instead of column O.
  6. Activate and test: Set Qualified = TRUE on a test row. Verify in Events Manager.

Design decision: one workflow or two?

You could handle both Attended and Qualified in a single workflow with branching logic. In this lab, we use separate workflows for clarity — each handshake is its own workflow. In production, the choice depends on maintainability: separate workflows are easier to debug; combined workflows reduce the number of Google Sheets triggers polling your sheet.

VERIFICATION
ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. What are the similarities and differences between Handshake #3 and #4?

2. Could a lead be marked as Qualified without first being marked as Attended? Should the workflow enforce this?

3. In what order should the events reach Meta for a single lead? Does Meta care about order?

LAYER 12

Handshake #5 — Stripe → n8n → CAPI → Purchase

OBJECTIVE

Build the workflow for the Purchase event: when a Stripe payment is received, send the event to Meta with the transaction value and currency.

WHY IT EXISTS

This is the most valuable signal in the funnel. A Purchase event with value tells Meta exactly how much revenue each conversion generates — enabling return-on-ad-spend (ROAS) optimization.

ARCHITECTURE
CLIENT PAYS VIA STRIPE
    │
    ▼ (Stripe sends webhook)
    │ checkout.session.completed
    │ (customer email, payment amount, currency)
    │
N8N WEBHOOK TRIGGER
    │
    ▼
CODE NODE
    │ Extract: email, amount (convert cents → whole), currency
    │ SHA-256 hash email
    │ Generate event_id from Stripe session/payment ID
    │
    ▼
HTTP REQUEST → CAPI
    │ event_name: "Purchase"
    │ custom_data: { value: 1400.00, currency: "EUR" }
    │ user_data: { em: [hashed_email] }
    │
    ▼
META EVENTS MANAGER → Purchase with value received
CONCEPTS

Stripe test mode: Every Stripe account has a test mode with separate API keys. Test mode uses test card numbers (like 4242 4242 4242 4242) and no real money is charged. Webhooks in test mode work the same as production.

Amounts in cents: Stripe reports amounts in the currency's smallest unit. For EUR, this means cents. A payment of €1,400.00 is reported as 140000 in Stripe. You must divide by 100 to get the actual amount for Meta's value field.

Purchase as a standard event: Purchase is a Meta standard event. It supports value and currency in custom_data, which enables ROAS-based optimization in Ads Manager.

PREREQUISITES
EXACT STEPS
  1. Create or log into your Stripe account at dashboard.stripe.com.
  2. Switch to Test Mode. Toggle "Test mode" in the top right of the Stripe dashboard. Confirm the badge says "TEST DATA."
  3. Create a test product. Go to Products → Add Product → Name: "Monthly Retainer" → Price: €1,400.00 EUR (recurring, monthly). Create a second price: €1,700.00 EUR. This simulates the client's two contract values.
  4. Create a new n8n workflow. Name it "Handshake #5 — Stripe → Purchase."
  5. Add a Webhook trigger node. Path: stripe-purchase. Note the URL.
  6. Register the webhook in Stripe. Go to Stripe Dashboard → Developers → Webhooks → Add endpoint:
    URL: your n8n webhook URL
    Events to send: checkout.session.completed
    Click "Add endpoint."
  7. Add a Code node:
const crypto = require('crypto');

function sha256(val) {
  return crypto.createHash('sha256').update(val).digest('hex');
}

// Extract from Stripe webhook
const session = $json.body?.data?.object || $json.data?.object || {};

const email = (session.customer_details?.email
  || session.customer_email || '').toLowerCase().trim();

// Stripe amounts are in cents — convert to whole currency units
const amountInCents = session.amount_total || 0;
const value = amountInCents / 100;
const currency = (session.currency || 'eur').toUpperCase();

// Stripe session/payment ID — stable identifier for event_id
const stripeSessionId = session.id || '';
const paymentIntent = session.payment_intent || '';

const payload = {
  data: [{
    event_name: 'Purchase',
    event_time: Math.floor(Date.now() / 1000),
    event_id: 'purchase-' + (paymentIntent || stripeSessionId),
    action_source: 'website',
    user_data: {
      em: email ? [sha256(email)] : [],
      external_id: stripeSessionId ? [sha256(stripeSessionId)] : []
    },
    custom_data: {
      value: value,
      currency: currency
    }
  }],
  access_token: 'YOUR_ACCESS_TOKEN',
  test_event_code: 'YOUR_TEST_EVENT_CODE'
};

return [{ json: payload }];
  1. Add an HTTP Request node. POST to CAPI endpoint (same as previous layers).
  2. Connect: Webhook → Code → HTTP Request.
  3. Activate the workflow.
  4. Test with Stripe's test mode. In the Stripe dashboard, go to Payments → create a test payment using the Stripe checkout flow, or trigger a webhook event manually: Developers → Webhooks → your endpoint → "Send test webhook" → select checkout.session.completed → Send.
  5. Alternatively, create a test Checkout Session using the Stripe CLI or a simple curl (this sends a real-looking webhook):
# Install Stripe CLI, then:
stripe trigger checkout.session.completed
  1. Verify: Check n8n execution log. Check Events Manager → Test Events for a Purchase event with the correct value.
DATA MAPPING
Stripe FieldTransformationCAPI Field
customer_details.emaillowercase, trim, SHA-256user_data.em
amount_total÷ 100 (cents to whole)custom_data.value
currencyuppercasecustom_data.currency
payment_intentprefix "purchase-"event_id
VERIFICATION
TROUBLESHOOTING
Value shows as 140000 instead of 1400

You forgot to divide by 100. Stripe sends amounts in cents. Add: const value = amountInCents / 100;

Email is empty in the CAPI payload

The Stripe webhook's checkout.session.completed event structure varies based on how the Checkout Session was created. Try session.customer_details.email, session.customer_email, or fetch the customer object using the customer ID. For test webhooks sent via "Send test webhook," the email field may be empty — use the Stripe CLI's stripe trigger instead.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why is the Purchase event more valuable to Meta's optimization algorithm than the Lead event?

2. Why do we divide the Stripe amount by 100?

3. Why do we use payment_intent for the event ID rather than a timestamp?

4. What would happen if this event were sent with value: 0?

LAYER 13

Reproduce in Zapier

OBJECTIVE

Reproduce the four CAPI workflows (Handshakes #2–#5) in Zapier. The client specifically requested Zapier — this is the client-facing deliverable.

WHY IT EXISTS

The client said "Build 4 Zapier Automations." They don't have Zapier yet and need guidance on plan selection. You've already built and tested the architecture in n8n. Now you translate it to Zapier, understanding what Zapier abstracts and what it exposes.

CONCEPTS
n8n ConceptZapier Equivalent
WorkflowZap
Trigger NodeTrigger Step
Action NodeAction Step
Code NodeCode by Zapier (JavaScript or Python)
IF NodeFilter Step or Paths
HTTP Request NodeWebhooks by Zapier (POST) or the Facebook Conversions app
Expression {{ $json.field }}Field mapping (point-and-click)
Execution logTask History
CredentialsConnected Accounts

Zapier Plan Requirements

Free plan: 100 tasks/month, 5 Zaps, 2-step Zaps only. Cannot use Filters, Paths, or multi-step Zaps.

Professional plan ($29.99/month): Required for multi-step Zaps (Zaps 2 and 3 need a Filter step). 750 tasks/month.

Recommendation for client: Professional plan. Estimated monthly task usage with moderate lead volume (~50 leads/month): approximately 200 tasks.

EXACT STEPS — Zap 1: Calendly → Schedule
  1. Create a Zapier account at zapier.com (free trial of Professional available).
  2. Create a new Zap. Name: "Calendly → Meta Schedule."
  3. Trigger: Search for "Calendly" → select "Invitee Created" event.
    Connect your Calendly account (OAuth flow).
    Zapier will ask for the event scope — select your "Discovery Call" event type.
    Test: Zapier will pull a recent booking as sample data.
  4. Action: Search for "Facebook Conversions" → select "Send Event" action (or "Send Other Event" if Schedule is not listed as a preset).
    Connect your Meta account (OAuth flow — must have access to the Pixel/Dataset).
    Dataset: Select your Dataset/Pixel.
    Event Name: Schedule
    Event Time: Use Zapier's timestamp formatter to get Unix time, or map Calendly's created_at field.
    Action Source: website
    Email: Map to Calendly's Invitee Email field. (Zapier's Facebook Conversions app handles hashing automatically.)
    Phone: Map to the phone field if available.
    First Name / Last Name: Map from Calendly's invitee name (you may need a Formatter step to split).
    Test Event Code: Your test event code.
  5. Test the Zap. Zapier will send a test event to Meta. Verify in Events Manager.
  6. Turn on the Zap.

What Zapier handles automatically

The Facebook Conversions app in Zapier automatically SHA-256 hashes the PII fields (email, phone, name) before sending to Meta. You don't need a Code step for hashing. This is convenient but means you can't inspect the hashed values — you trust Zapier to hash correctly.

EXACT STEPS — Zap 2: Attendance → CallAttended
  1. Create a new Zap. Name: "Attendance → Meta CallAttended."
  2. Trigger: "Google Sheets" → "New or Updated Spreadsheet Row."
    Spreadsheet: Lead Tracker
    Worksheet: Sheet1
    Trigger Column: Column J (Attended) — Zapier triggers when this specific column changes.
  3. Filter (requires Professional plan): Add a Filter step:
    Only continue if: Attended = TRUE AND CallAttended_Sent is empty.
  4. Action: "Facebook Conversions" → "Send Other Event."
    Event Name: CallAttended
    Action Source: system_generated
    Email: Map from sheet's Email column
    Phone: Map from sheet's Phone column
  5. Action 2: "Google Sheets" → "Update Spreadsheet Row."
    Row: same row that triggered
    CallAttended_Sent: current timestamp
EXACT STEPS — Zap 3: Qualified → QualifiedLead

Same pattern as Zap 2. Change: Trigger Column to K (Qualified), Filter checks Qualified = TRUE AND QualifiedLead_Sent is empty, Event Name to QualifiedLead, and update column P.

EXACT STEPS — Zap 4: Stripe → Purchase
  1. Create a new Zap. Name: "Stripe → Meta Purchase."
  2. Trigger: "Stripe" → "New Payment" (or "New Charge" / "Payment Intent Succeeded" depending on the client's Stripe setup).
    Connect your Stripe account (test mode).
  3. Action: "Facebook Conversions" → "Send Purchase Event."
    Email: Map from Stripe's customer email
    Value: Map from Stripe's amount field. Important: Use a Formatter step to divide by 100 if Stripe sends cents.
    Currency: Map from Stripe's currency field, or hard-code EUR.
VERIFICATION
STUDENT QUESTIONS

1. What does Zapier's Facebook Conversions app do automatically that you had to do manually in n8n?

2. What can you see in n8n's execution log that you cannot see in Zapier's task history?

3. Why does Zap 2 require a Professional plan but Zap 1 does not?

4. If a Zap fails, what information does Zapier give you for debugging compared to n8n?

LAYER 14

Meta Event Verification

OBJECTIVE

Verify that all five events are being received correctly by Meta, with proper parameters and identity data.

EXACT STEPS
  1. Go to Events Manager → your Dataset → Overview.
  2. For each of the five events, record:
Check Lead Schedule CallAttended QualifiedLead Purchase
Received?
SourcePixelCAPICAPICAPICAPI
Event typeStandardStandardCustomCustomStandard
Has email?
Has phone?
Has event_id?
Has value?N/AN/AN/AN/A
Warnings?
Event Match Quality
  1. Check Event Match Quality (EMQ). In Events Manager, click each event → view the "Event Match Quality" score. Meta scores the quality of identity data you're sending. Higher is better. Email alone is typically 4/10. Email + phone + name gets you 6-7/10. Adding fbc and fbp can push it to 9-10/10.
  2. Check for warnings. Meta may flag issues: missing recommended parameters, low match quality, potential duplicates. Record any warnings.
ACCEPTANCE CRITERIA
LAYER 15

Meta Event Configuration

OBJECTIVE

Understand the difference between event transmission, event configuration, and ad optimization. Configure events appropriately in Meta — but do not blindly create Custom Conversions for everything.

CONCEPTS

Three separate things:

ConceptWhat It MeansWhere It Happens
Event TransmissionSending the event data to Meta (via Pixel or CAPI)Your website / your server (n8n/Zapier)
Event ConfigurationHow Meta categorizes, names, and organizes the event in its systemMeta Events Manager
Ad OptimizationUsing the event as a signal for ad delivery optimizationMeta Ads Manager (campaign settings)

Standard Events (Lead, Schedule, Purchase): Meta recognizes these names automatically. They appear in Events Manager with predefined categories. They can be used for optimization immediately.

Custom Events (CallAttended, QualifiedLead): These are event names Meta does not pre-define. They appear in Events Manager automatically once received. They can also be used for optimization — you can select them when creating a campaign.

Custom Conversions: A separate feature that lets you define a conversion based on rules — e.g., "Purchase events where value > €1,500." You do NOT need a Custom Conversion just to use an event for optimization. Custom Conversions are for creating filtered subsets of events.

Common Misconception

The client said: "Configure Custom Conversions in Meta Events Manager so all 5 events are usable as optimisation events." This conflates Custom Conversions with optimization eligibility. The events are usable for optimization without creating Custom Conversions. Custom Conversions are only needed if you want to optimize for a subset (e.g., only purchases above a certain value).

EXACT STEPS
  1. Verify all 5 events appear in Events Manager. Go to Events Manager → your Dataset → Overview. All five event names should be listed.
  2. Check each event's type. Click on each event → Details:
    Lead — Standard ✓
    Schedule — Standard ✓
    Purchase — Standard ✓ (with value and currency)
    CallAttended — Custom ✓
    QualifiedLead — Custom ✓
  3. Verify optimization eligibility. Go to Ads Manager → create a test campaign (don't publish it) → at the Ad Set level, under "Conversion event" or "Optimization goal," look for your events in the dropdown. Standard events appear by default. Custom events should appear if they've been received.
  4. Create a Custom Conversion only if needed. If the client wants to optimize for "high-value purchases only" (€1,700+ vs €1,400), then create a Custom Conversion:
    Events Manager → Custom Conversions → Create
    Name: "High-Value Purchase"
    Rule: Event = Purchase AND Value > 1500
    This is the only scenario where a Custom Conversion adds value in this project.
  5. Discuss volume requirements with the client. Meta's learning phase requires approximately 50 optimization events per week. If the client gets 10 leads/week but only 1 purchase/month, optimizing for Purchase will not work — Meta won't have enough data. Start with Lead or Schedule optimization, then move to Purchase as volume grows.
ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. What is the difference between a custom event and a Custom Conversion?

2. When would you create a Custom Conversion?

3. Why might optimizing for Purchase not work immediately for this client?

4. What does "learning phase" mean in Meta's ad delivery?

LAYER 16

Identity & Attribution

OBJECTIVE

Understand how identity is preserved across the entire funnel — from ad click to purchase — and why this is critical for Meta's ability to attribute conversions to the correct ad.

WHY IT EXISTS

When Meta receives a Purchase event for test@example.com, it needs to connect that purchase back to the specific ad click that started the journey. If the identity chain is broken, Meta receives the event but cannot attribute it — and the advertiser gets no optimization benefit.

THE IDENTITY CHAIN
META AD CLICK
│ fbclid parameter added to URL
│ _fbc cookie set in browser
│ _fbp cookie set in browser
▼
WEBSITE (your page)
│ Pixel captures: _fbc, _fbp, IP, user agent
│ URL: example.com?fbclid=abc123
▼
TYPEFORM (iframe)
│ Captures: email, phone, name, country
│ ⚠ Does NOT have access to _fbc, _fbp (different origin)
▼
CALENDLY (separate site)
│ Captures: email, name
│ ⚠ Does NOT have _fbc, _fbp
│ ⚠ Does NOT have phone (unless custom question added)
▼
GOOGLE SHEETS
│ Has: email, phone, name, country (from Typeform)
│ Has: Calendly booking info (from n8n/Zapier)
│ ⚠ Does NOT have _fbc, _fbp
▼
STRIPE
│ Has: email (from customer)
│ Has: payment amount, currency
│ ⚠ Does NOT have phone, _fbc, _fbp
▼
CAPI → META
│ Can send: em, ph, fn, ln, external_id, fbc, fbp
│ ⚠ fbc and fbp are ONLY available if captured on the website
IDENTITY PARAMETERS FOR CAPI
ParameterNameEMQ ImpactSourceHashed?
emEmail+4 pointsTypeform, Calendly, StripeYes
phPhone+3 pointsTypeformYes
fnFirst name+1 pointTypeform, CalendlyYes
lnLast name+1 pointTypeform, CalendlyYes
countryCountry+0.5 pointTypeformYes (2-letter ISO)
fbcClick ID+3 pointsBrowser cookie (website only)No
fbpBrowser ID+2 pointsBrowser cookie (website only)No
external_idYour ID+1 pointLead_IDYes (recommended)
client_ip_addressIP+1 pointRequest headersNo
client_user_agentUser agent+0.5 pointRequest headersNo
THE FBP/FBC GAP

Critical Production Issue

The _fbc and _fbp cookies are set by the Meta Pixel on your website. They exist only in the visitor's browser. When the visitor submits a Typeform and moves to Calendly and beyond, these cookie values are lost — they cannot cross iframe or domain boundaries automatically.

In this laboratory: We do not have these values for CAPI events because we cannot easily extract them from the browser and pass them through Typeform → Calendly → Google Sheets. Our CAPI events rely on email + phone matching, which works but produces lower EMQ.

In production: To capture _fbp and _fbc, you would: (1) read these cookies on the website using JavaScript, (2) pass them as hidden fields through Typeform (using URL parameters or hidden fields), (3) include them in the Google Sheet, and (4) send them with every CAPI event. This is an advanced optimization that significantly improves event match quality.

Production strategy for _fbp/_fbc capture

// On your website, BEFORE the Typeform embed, capture the cookies:
<script>
function getCookie(name) {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? match[2] : '';
}
const fbp = getCookie('_fbp');
const fbc = getCookie('_fbc');

// Pass to Typeform via hidden fields in the embed URL:
// data-tf-hidden="fbp=VALUE,fbc=VALUE"
</script>

Then include these values in your Google Sheet and send them (unhashed) with every CAPI event as user_data.fbp and user_data.fbc.

ACCEPTANCE CRITERIA
LAYER 17

Deduplication

OBJECTIVE

Understand how Meta deduplicates events and why stable, source-system-derived event IDs are critical.

CONCEPTS

Why duplicates occur: In a system with both browser-side (Pixel) and server-side (CAPI) tracking, the same action can generate two events. Example: a visitor submits the Typeform → the Pixel fires a Lead event → simultaneously, a Typeform webhook triggers n8n, which sends the same Lead event via CAPI. Meta now has two Lead events for the same action. Without deduplication, Meta counts it twice — inflating your conversion numbers and corrupting optimization signals.

How Meta deduplicates: Meta uses the combination of event_name + event_id within a 48-hour window. If two events have the same event_name and event_id, Meta keeps one and discards the duplicate.

Stable vs random event IDs:

ApproachExampleRisk
Random (bad)Math.random().toString()Every send generates a unique ID — no deduplication possible
Timestamp-based (risky)"lead-" + Date.now()Two sends milliseconds apart get different IDs — deduplication fails
Source-system ID (good)"schedule-" + calendlyEventIdSame booking always produces the same event_id — deduplication works even if the workflow fires twice
EVENT ID DESIGN FOR THIS PROJECT
Eventevent_id FormatSourceWhy It's Stable
Leadlead-{typeform_response_id}Typeform response IDEach form submission has exactly one response ID
Scheduleschedule-{calendly_event_id}Calendly event URIEach booking has exactly one event ID
CallAttendedcallattended-{lead_id}Lead_ID from Google SheetEach lead can only attend once
QualifiedLeadqualifiedlead-{lead_id}Lead_ID from Google SheetEach lead can only be qualified once
Purchasepurchase-{stripe_payment_intent_id}Stripe payment intentEach payment has exactly one payment intent ID
ACCEPTANCE CRITERIA
LAYER 18

Debugging Lab

OBJECTIVE

Deliberately create failures, diagnose them, and fix them. A freelancer who cannot troubleshoot is not worth $300.

Failure Exercise 1: Wrong event name

Create the failure: In one of your n8n workflows, change event_name to "Purchse" (misspelled).

Symptom: Meta accepts the event (HTTP 200, events_received: 1), but it appears as a new custom event called "Purchse" — not as a Purchase standard event.

Where to inspect: Events Manager → Overview. You'll see "Purchse" as a separate event from "Purchase."

How to confirm: The event count for "Purchase" stops increasing. A new "Purchse" event appears.

Fix: Correct the event_name to "Purchase" (exact spelling, case-sensitive). Re-send the event.

Lesson: Meta does not validate event names against its standard events list. It accepts anything. A typo creates a new custom event rather than failing.

Failure Exercise 2: Missing email

Create the failure: Send a CAPI event with an empty user_data (no email, no phone, no external_id).

Symptom: Meta may still accept the event, but Event Match Quality will be very low or zero. The event cannot be attributed to any user.

Where to inspect: Events Manager → your event → Event Match Quality.

Fix: Always include at least one identity parameter (email is the most impactful).

Failure Exercise 3: Unhashed PII

Create the failure: Send user_data.em as "test@example.com" instead of the SHA-256 hash.

Symptom: Meta will return a warning or error: "Invalid format for parameter 'em'. Expected SHA-256 hash."

Where to inspect: The HTTP response body from the CAPI call. n8n shows this in the HTTP Request node's output.

Fix: Hash all PII with SHA-256 before sending.

Failure Exercise 4: Duplicate events

Create the failure: Run the same workflow twice with the same data — but use a random event_id (e.g., Math.random()).

Symptom: Two events appear in Events Manager for the same action. Conversion count is inflated.

Where to inspect: Events Manager → event count is higher than expected.

Fix: Use stable, source-system-derived event IDs (Layer 17).

Failure Exercise 5: Invalid payload (malformed JSON)

Create the failure: Send a CAPI request with broken JSON (e.g., missing closing brace).

Symptom: HTTP 400 error response.

Where to inspect: n8n HTTP Request node → output → error message.

Fix: Validate JSON structure. Use JSON.stringify() to build the body rather than string concatenation.

Failure Exercise 6: Automation succeeds but Meta does not receive event

Create the failure: Use an expired or invalid access token.

Symptom: The n8n workflow completes (the webhook was received, the Code node processed the data), but the HTTP Request node returns an error: "Invalid OAuth access token."

Where to inspect: n8n execution log → HTTP Request node → response body.

Key insight: "Automation succeeded" (n8n ran) ≠ "Meta received the event" (CAPI accepted it). Always check the HTTP response.

Fix: Generate a new access token in Events Manager.

Failure Exercise 7: Wrong purchase value

Create the failure: Send a Purchase event with value: 140000 instead of value: 1400.

Symptom: Meta accepts it. The event shows a purchase value of €140,000 — absurdly high. ROAS calculations are wrong.

Where to inspect: Events Manager → Purchase event → value parameter.

Fix: Divide Stripe's cent-based amount by 100. Add validation: if value > 10000 for this client, log a warning.

Failure Exercise 8: Event received but match quality is poor

Create the failure: Send a CAPI event with only external_id and no email or phone.

Symptom: Event is received, but EMQ is 1-2/10. Meta cannot reliably attribute it.

Where to inspect: Events Manager → Event Match Quality score → breakdown of which parameters contributed.

Fix: Include email (em) at minimum. Add phone (ph) and name (fn, ln) for better matching.

LAYER 19

End-to-End Test

OBJECTIVE

Run one complete test journey through the entire funnel and verify every stage.

TEST IDENTITY
Name:      Lab TestUser
Email:     labtestuser@example.com
Phone:     +1 555 000 1234
Country:   United States
EXECUTION SEQUENCE
  1. Visit your website. Open https://YOURUSERNAME.github.io/meta-capi-lab/. Verify PageView in Pixel Helper.
  2. Submit the Typeform with the test identity. Verify: Lead event fires (Pixel Helper), console logs show "Meta Lead event fired."
  3. After redirect, book a Calendly meeting. Use the same email. Complete the booking. Verify: n8n Handshake #2 workflow fires, Schedule event appears in Events Manager.
  4. Add the lead to Google Sheets. If not auto-populated, manually add a row with the test identity data.
  5. Set Attended = TRUE. Wait for the poll interval. Verify: n8n Handshake #3 fires, CallAttended event in Events Manager, CallAttended_Sent column populated.
  6. Set Qualified = TRUE. Verify: Handshake #4 fires, QualifiedLead event in Events Manager, QualifiedLead_Sent column populated.
  7. Create a Stripe test payment. Use test card 4242 4242 4242 4242, expiry any future date, CVC any 3 digits. Amount: €1,400. Verify: Handshake #5 fires, Purchase event with value in Events Manager.
EVENT TIMELINE

After completing all steps, fill out this timeline:

TimeActionEventSourceVerified In Meta?
T+0:00Page visitPageViewPixel
T+0:02Typeform submittedLeadPixel
T+0:05Calendly bookedScheduleCAPI
T+1:00Attended markedCallAttendedCAPI
T+1:05Qualified markedQualifiedLeadCAPI
T+2:00Stripe paymentPurchaseCAPI
ACCEPTANCE CRITERIA
LAYER 20

Production Architecture Review

OBJECTIVE

Review the original client brief as a professional freelancer. Identify every assumption, ambiguity, limitation, and risk. Produce a production-ready assessment.

INSTRUCTIONS

Re-read the Raw Client Brief from Part 1. For each major point, produce:

ItemClient RequestWhat It MeansWhat Must Be VerifiedRecommended Implementation
1 "Verify the Pixel is installed correctly on scaleuppartner.co" Audit existing Pixel on Shopify site Is Pixel installed via Meta channel app or custom code? Is it firing correctly? What events are currently configured? Use Meta Pixel Helper + Events Manager. Check Shopify's Meta channel settings.
2 "Add Meta Pixel to Calendly" Client wants Schedule tracking on Calendly Cannot install Pixel on Calendly's hosted pages Use CAPI via Zapier, triggered by Calendly webhook. Explain to client.
3 "offline conversion" Outdated term Offline Conversions API deprecated Use Conversions API. Educate client on current terminology.
4 "Call attended (marked in Calendly)" Attendance tracking Calendly has no positive attendance status Use Google Sheet manual entry + Zapier trigger. Explain to client.
5 "Configure Custom Conversions for all 5 events" Events usable for optimization Custom Conversions not required for standard/custom event optimization Send events, verify in Events Manager, explain optimization requirements (volume).
6 "Zapier" Client's chosen platform Free plan insufficient. Professional plan required. Recommend Professional plan ($29.99/month). Estimate task usage.
7 "$300–$400 budget" Fixed price for implementation Scope includes audit, 4 automations, sheet, documentation, education Achievable for experienced implementer. Ongoing Zapier cost is separate.
8 "Shopify (scaleuppartner.co is on Shopify)" Website platform Shopify's Meta channel may already handle Pixel installation Verify if Meta channel is installed. Check if it handles CAPI for Shopify checkout events.
9 No mention of identity matching Client not aware of EMQ Current events may have low match quality Implement _fbp/_fbc capture for production. Explain EMQ to client.
10 No mention of deduplication Risk of double-counting if Pixel + CAPI fire for same event Lead event: Pixel fires on submit. If CAPI also sends Lead, dedup needed. Use consistent event_ids. Educate client on dedup mechanism.

Additional Production Considerations

Privacy/Compliance: PII is being hashed and sent to Meta. The client should verify compliance with GDPR (EU data), cookie consent requirements, and their privacy policy. Meta's Terms of Service require that advertisers have appropriate consent.

Error handling: What happens when a CAPI call fails? Production workflows need retry logic, error notifications (email/Slack), and a dead-letter queue for failed events.

Monitoring: Who checks that the automations are still running? Set up alerts for: n8n/Zapier workflow failures, drop in event volume in Events Manager, changes to access tokens or permissions.

Documentation: The client requested a Loom video and written documentation. This should cover: how each Zap works, how to troubleshoot common failures, how to read Events Manager, and who is responsible for marking attendance/qualification.

Part 10 — Student Deliverables

Complete all of these before considering the laboratory finished.

Part 11 — Client Definition of Done

What does success look like from the client's perspective?

The system is "done" when the client's business can:

Part 12 — Final Exam

Answer these without looking at the guide. If you cannot answer all 20, go back and review.

01. What is Meta Pixel?
02. What is the Conversions API (CAPI)?
03. What is n8n?
04. What is Zapier?
05. What is the difference between n8n and CAPI?
06. What is the difference between Zapier and CAPI?
07. What is a Meta event?
08. What is a standard event?
09. What is a custom event?
10. What is a Custom Conversion?
11. What is an optimization event?
12. Why does identity matching matter?
13. Why does deduplication matter?
14. Which handshakes require human intervention?
15. Why is Purchase generally a more valuable business signal than Lead?
16. Why might Purchase not immediately be the best optimization event?
17. What happens when Zapier/n8n succeeds but Meta does not receive the event?
18. How would you diagnose it?
19. What parts of the client's requested architecture require technical verification?
20. Explain the entire architecture in your own words.

Pedagogical Standard

You have not completed this laboratory if you can only click buttons. You must be able to reason through the chain for any event:

WHAT HAPPENED? → WHICH SYSTEM KNOWS? → WHAT TRIGGERED? → WHICH WORKFLOW RUNS? → WHAT DATA MOVES? → HOW IS IT TRANSFORMED? → HOW IS IT TRANSMITTED? → WHAT DOES META RECEIVE? → HOW DOES META IDENTIFY THE USER? → HOW IS THE EVENT CONFIGURED? → HOW CAN THE SIGNAL BE USED? → WHAT COULD FAIL? → HOW DO WE DIAGNOSE IT?

— End of Module 1 —

MODULE 2 Conversion Tracking & Attribution Track

Shopify Tracking Diagnostic Lab

Audit, diagnose, and fix broken Meta Pixel & CAPI tracking on a Shopify store. Built from real Upwork client problems.

Prerequisite

You must complete Module 1: Meta CAPI Automation Lab before this module. Module 1 teaches the raw mechanics of Meta Pixel, Conversions API, SHA-256 hashing, event deduplication, and Event Match Quality. This module assumes you understand those fundamentals and can now apply them to a different context: diagnosing and fixing an existing Shopify store's broken tracking.

Mindset shift: In Module 1 you built everything from scratch. In this module, someone else already built it — badly. Your job is to walk into a mess, find every problem, fix it, and hand back a clean system. This is the most common CAPI job on Upwork.

Part 1 — Raw Client Brief

Reproduced exactly as posted on Upwork. Do not correct, rewrite, or modernize.

Meta Pixel & Conversions API Specialist — Fix Over-Counting Purchase Events We run a social sweepstakes gaming platform on Shopify. Our Meta ad campaigns are reporting dramatically inflated ROAS. When we compare Meta's reported purchases and purchase values against our actual Shopify orders and Stripe payouts, Meta is showing roughly 5x the real numbers. On some days the discrepancy is even worse — Meta reports 60x ROAS while our actual return is closer to 2x. We believe the root cause is one or more of the following: • Dual firing — both the browser Pixel and the Conversions API are sending Purchase events without proper event_id deduplication • Repeated firing per single transaction — page reloads, SPA re-renders, or navigation triggers are causing the same purchase to fire multiple times • Purchase events firing on non-purchase actions (e.g. page views on the order status page triggering a Purchase event) • Incorrect or multiplied value parameters in the event payload What we need: 1. Audit the current Pixel and CAPI setup across our tag manager (GTM), site code (theme + checkout), and server-side configuration 2. Identify the precise over-counting mechanism with evidence (screenshots, event logs, before/after) 3. Implement fixes — proper deduplication, correct firing triggers, accurate values 4. Validate corrected data against our backend Shopify/Stripe transaction records for at least 3 days 5. Document the final setup and provide a maintenance runbook so our team can troubleshoot Required experience: • Demonstrable CAPI and server-side tracking experience • Deep understanding of event_id deduplication mechanics • Comfort with tag managers, GTM, CAPI Gateways, or direct APIs • Experience reconciling ad platform data against source-of-truth databases Hourly: $10–$30/hr Duration: 1–3 months Hours: Less than 30/week This is time-sensitive — we want someone who can start this week. In your application, please include: • 2–3 sentence summary of your diagnostic approach • A past project example where you fixed conversion over-counting (before/after metrics if possible) • Your earliest start date and Phase 1 turnaround estimate • Rate and availability confirmation

Instructor Note

This brief is based on a real Upwork posting from 2026. The client has a working Shopify store with an active Meta ad spend. Their tracking is already installed — the problem is that it's broken. This is fundamentally different from Module 1 where you built tracking from zero. Here you are a diagnostic specialist walking into someone else's implementation.

Part 2 — Client Requirements Analysis

What the client said vs. what it actually means technically.

Requirement 1: "Meta is showing roughly 5x the real numbers"

CLIENT SAID: Meta reports 5x actual ROAS, sometimes 60x on individual days.
TECH REALITY: A clean 2x multiplier (Meta reports double) usually means one duplication source. 5x with variance (sometimes 60x) suggests multiple overlapping problems — likely at least two duplicate Pixel/CAPI sources plus a firing-condition bug (Purchase on non-purchase pages). The 60x days may coincide with page reloads or SPA re-render storms.

Requirement 2: "Dual firing — browser Pixel and CAPI without event_id deduplication"

CLIENT SAID: Both Pixel and CAPI are sending Purchase events without deduplication.
TECH REALITY: Correct hypothesis. On Shopify, the native Facebook & Instagram channel app sends both browser Pixel events and server-side CAPI events with matching event_id values. If the store ALSO has a manual Pixel in theme.liquid, a GTM container with a Meta tag, or a third-party app (Elevar, Trackify, Triple Whale) firing its own events, those create a second event stream with different event_id values. Meta cannot deduplicate across mismatched IDs, so it counts both.

Requirement 3: "Repeated firing per single transaction"

CLIENT SAID: Page reloads, SPA re-renders, or navigation cause the same purchase to fire multiple times.
TECH REALITY: On Shopify's checkout, the order status page (thank-you page) can be revisited by the customer. If a Purchase event fires on every page load of that URL rather than once per order, reloads inflate the count. Shopify's Customer Events system fires checkout_completed once per order, but legacy code in Additional Scripts or theme.liquid may fire on every page load of the /thank_you route. SPA re-renders are less common on Shopify (it's not a pure SPA), but some themes and headless storefronts do re-render.

Requirement 4: "Purchase events firing on non-purchase actions"

CLIENT SAID: Page views on the order status page may trigger Purchase events.
TECH REALITY: A common misconfiguration. GTM triggers often use URL-based rules like "Page URL contains /thank_you" instead of proper Shopify Customer Event subscriptions. The order status page is also used for order lookup (existing customers checking their order), which fires the same URL trigger without any new purchase. Additionally, some GTM setups fire on "All Pages" with a Purchase tag that should be restricted to the order confirmation.

Requirement 5: "Incorrect or multiplied value parameters"

CLIENT SAID: Event payloads may have wrong values.
TECH REALITY: Multiple sources: Shopify reports in the store's currency while Stripe may report in cents (needing ÷100). If multiple Pixel/CAPI sources fire with different value calculations, the reported total is additive. A store with a $50 order might report: $50 (native app) + $50 (GTM Pixel) + $5000 (GTM CAPI with cents not divided) = $5,100 for one $50 order. That alone is a 102x inflation on value.

Requirement 6: "Audit across tag manager (GTM), site code, and server-side"

CLIENT SAID: Check GTM, theme code, and server-side configuration.
TECH REALITY: On a typical Shopify store, there are 5 places where Meta tracking can be installed independently, and the client may not even know all of them exist. You must check all 5:
1. Facebook & Instagram channel app (native)
2. theme.liquid (manual code)
3. GTM container (tags)
4. Settings → Checkout → Additional Scripts
5. Third-party Shopify apps (Customer Events)

Requirement 7: "Validate corrected data against backend for at least 3 days"

CLIENT SAID: Compare fixed Meta data to Shopify/Stripe records.
TECH REALITY: This is the professional standard. After fixing the tracking, you pull Shopify Orders export and Stripe payouts for the same period, then compare total purchase count and total revenue against what Meta Events Manager reports. A clean implementation should show Meta purchase count within 5–15% of actual orders (never exactly matching due to ad blockers, consent banners, and attribution windows). The 3-day window lets you catch intermittent issues.

Part 3 — Client Problem Statement

Structure the mess before touching anything.

Current State

The client has a Shopify store running Meta ad campaigns. Tracking is installed — Pixel fires, CAPI events arrive in Events Manager — but the data is dramatically wrong. Meta reports 5x the actual purchase volume and revenue. The client cannot make ad spend decisions because ROAS numbers are meaningless. They're flying blind on a live ad budget.

The Information Gap

The client does not know which of the 5 possible installation points is causing the duplication. They suspect "dual firing" but haven't audited systematically. They may have installed the native Meta app, then hired a developer who added GTM tracking, then installed a third-party attribution app — each adding its own Pixel/CAPI layer without removing the previous one. Nobody documented what was installed or when.

Why This Matters

Every day of inflated data is a day of wrong optimization decisions. Meta's algorithm is learning from garbage signal — it thinks 5x more people are converting than actually are, so it targets broader and shallower instead of finding the real converters. The client is burning money while the tracking is broken.

Desired State

After your work:

• Exactly one Purchase event fires per actual order (within the 5–15% tolerance for ad blockers/consent).

• Purchase values match Shopify order totals (same currency, same amount).

• Browser Pixel and CAPI events are properly deduplicated via matching event_id.

• Meta ROAS aligns with actual Shopify/Stripe revenue within a reasonable margin.

• A single, documented tracking architecture with one clear source of truth.

• A runbook the client's team can use to verify tracking health going forward.

Part 4 — Client Objectives

What the client actually needs from you, organized by category.

Diagnostic Objectives

Identify every source of Meta events on the store. Map which Pixel IDs are firing, from which installation points, on which pages. Determine the exact mechanism causing over-counting.

Remediation Objectives

Remove or disable all duplicate event sources. Ensure exactly one browser-side and one server-side event path, both using the same event_id for deduplication. Fix value parameters. Fix firing conditions.

Validation Objectives

Prove the fix works with data. Compare Meta events to Shopify orders over 3+ days. Show before/after screenshots. Demonstrate that Meta purchase count matches reality.

Documentation Objectives

Write a runbook the client can follow. Include: what is installed, where, how to check it's working, what to do when it breaks, and a monthly health check procedure.

Commercial Constraints

Budget is $10–$30/hr. This is a diagnostic-heavy job — most of the time is investigation, not building. Phase 1 (diagnosis) should be fast. The client is time-sensitive because every day of bad data costs them ad spend.

Part 5 — Technical Objectives

The 15 things you must be able to do after completing this module.

#Objective
1Set up a Shopify development store for tracking experiments
2Install the native Facebook & Instagram channel app and configure data sharing to "Maximum"
3Use Meta Pixel Helper to audit which Pixel IDs fire on every page of the store
4Inspect theme.liquid for hardcoded fbq('init') blocks
5Audit a GTM container for Meta Pixel/CAPI tags
6Inspect Shopify Settings → Checkout → Additional Scripts for legacy tracking code
7Audit Shopify Customer Events (Settings → Customer Events) for third-party pixel installations
8Explain Shopify's Web Pixels API: strict sandbox (app pixels) vs. lax sandbox (custom pixels)
9Map all discovered event sources into a single diagnostic matrix
10Deliberately create a duplicate-event scenario and verify it in Events Manager
11Remove duplicate sources and verify single-fire behavior
12Verify event_id deduplication between browser Pixel and CAPI events
13Reconcile Meta Events Manager data against Shopify Orders export
14Write a tracking health runbook for client handover
15Explain why reported Meta purchases should be within 5–15% of actual orders, not exactly matching

Part 6 — Shopify Tracking Architecture

Where Meta events can come from on a Shopify store.

The Five Installation Points

On a Shopify store, Meta tracking code can be installed in five independent locations. Each one can fire events to Meta without knowing the others exist. This is the root cause of most duplication problems — tracking was installed at different times by different people, and nobody removed the old installation when the new one went in.

#LocationWhat It DoesWho Usually Installs ItHow to Check
1 Facebook & Instagram channel app Official Meta integration. Installs both browser Pixel and server-side CAPI. Handles event_id deduplication automatically. Sets data sharing to "Standard," "Enhanced," or "Maximum." Store owner following Shopify's setup wizard Shopify Admin → Sales Channels → Facebook & Instagram → Settings → Data Sharing
2 theme.liquid Manual fbq('init', 'PIXEL_ID') code pasted into the theme's layout file. Fires browser-side events on every page load. No CAPI, no automatic deduplication. A developer hired before the native app existed, or one who didn't know the app was already installed Shopify Admin → Online Store → Themes → Edit Code → search for fbq
3 Google Tag Manager GTM container with one or more Meta Pixel tags (and possibly a server-side GTM forwarding CAPI events). Fires based on GTM triggers, completely independent of Shopify's native tracking. A marketing agency or PPC specialist View page source → search for GTM- container IDs. Open GTM → filter tags by "Facebook" or "Meta"
4 Additional Scripts (checkout) Code in Settings → Checkout → Additional Scripts textarea. Fires on the order status page (thank-you page). This was the standard way to track purchases before Customer Events existed. An older developer, a tutorial from 2020–2022, or a Shopify support article Shopify Admin → Settings → Checkout → scroll to "Order status page" → Additional Scripts
5 Third-party apps (Customer Events) Apps like Elevar, Trackify, Triple Whale, or upsell apps (ReConvert, Zipify) that install their own pixels via Shopify's Customer Events system. May inject their own event_id values. The store owner or a marketing agency trying to improve attribution Shopify Admin → Settings → Customer Events. Also check each app's settings for "Pixel" or "Meta" configuration.

The Fundamental Problem

Each installation point is independent. The native app doesn't know about the GTM tag. The GTM tag doesn't know about the code in theme.liquid. The third-party app doesn't know about any of them. So a single purchase can fire:

Purchase (native app, Pixel) — event_id: "shopify-abc123"
Purchase (native app, CAPI) — event_id: "shopify-abc123" ← deduplicates with above
Purchase (theme.liquid) — no event_id
Purchase (GTM Pixel tag) — event_id: "gtm-xyz789"
Purchase (GTM sGTM CAPI) — event_id: "gtm-xyz789" ← deduplicates with above
Purchase (third-party app) — event_id: "elevar-def456"

Meta receives 4 distinct Purchase events for 1 order. The native app's pair deduplicates (same event_id), and the GTM pair deduplicates (same event_id), but the 4 remaining distinct events are all counted. Purchase value is multiplied by 4.

Shopify's Web Pixels API & Customer Events

Since Shopify moved checkout to Checkout Extensibility (2024–2025), the old method of pasting JavaScript into checkout.liquid no longer works for most stores. Instead, Shopify publishes Customer Events — a standardized set of tracking events that any app or custom pixel can subscribe to. These events fire inside a sandboxed iframe, which means pixel code cannot access the main page's DOM, cookies (except via browser.cookie.get()), or other scripts.

Customer EventWhen It FiresMaps To (Meta)
page_viewedAny storefront page loadPageView
product_viewedProduct detail pageViewContent
product_added_to_cartAdd to Cart clickAddToCart
checkout_startedCheckout beginsInitiateCheckout
payment_info_submittedPayment details enteredAddPaymentInfo
checkout_completedOrder placed (fires once per order)Purchase

Key Insight: checkout_completed fires once per order

Shopify's Customer Events system guarantees that checkout_completed fires exactly once per completed order. This is the correct event to subscribe to for Purchase tracking. Code that instead fires on "page URL contains /thank_you" will fire on every page load of that URL — including reloads, back-button navigation, and order status lookups by existing customers.

Sandbox Restrictions

ActionAllowed?How
Read cookies (_fbp, _fbc)Yesawait browser.cookie.get('_fbp')
Read document.cookie directlyNoReturns undefined — silent failure, no error
Access main page DOMNoSandbox isolation
Send HTTP requestsYesfetch() with keepalive: true
Use navigator.sendBeaconYesFor guaranteed delivery on page unload
Access checkout data (order ID, total, line items)YesVia event payload: event.data.checkout

The Silent Cookie Failure

If code uses document.cookie instead of browser.cookie.get('_fbp') inside a Web Pixels sandbox, it returns undefined silently. The code doesn't crash — it just sends events with no _fbp or _fbc values. This means identity matching fails silently, and Event Match Quality drops without any error to alert you. This is one of the most common bugs in custom Shopify Pixel implementations.

Part 7 — Laboratory Environment

Set up your sandbox for Shopify tracking experiments.

Tool Inventory

ToolPurposeCost
Shopify Partner AccountCreate free development storesFree
Shopify Development StoreFull Shopify store without paying for a planFree (via Partner account)
Meta Business SuiteBusiness Manager, Pixel/Dataset, Events ManagerFree
Meta Pixel Helper (Chrome extension)Audit which Pixel IDs fire on each pageFree
Google Tag ManagerSimulate a GTM-based tracking installationFree
Chrome DevTools (Network tab)Inspect actual HTTP requests to facebook.com/trFree
Incognito/Private browsingTest without cached cookies or extensions interferingFree

Free-Sandbox Limitation: Facebook & Instagram Channel App

Shopify development stores only allow free and partner-approved apps. The official Facebook & Instagram channel app is free and should be installable on a development store. If installation fails, use Shopify's 3-day free trial + $1/month for 3 months offer on a Basic plan instead — total cost under $5 for 3 months of a full store with all apps available.

Alternative: Even without the native app, you can still complete 80% of this lab by manually installing tracking code in theme.liquid, GTM, and Additional Scripts — which is what you'll be auditing on client stores anyway.

Part 8 — Complete Implementation

12 layers. Each one builds a skill you need for real Shopify diagnostic work.

LAYER 1

Create the Shopify Development Store

OBJECTIVE

Set up a free Shopify store you can break and fix without consequences.

WHY IT EXISTS

You cannot practice diagnostic work on a client's live store. You need a sandbox where you can install tracking in all 5 locations, create deliberate duplication, and verify fixes — all without risking a client's real ad data.

CONCEPTS

Shopify Partner Program — a free program that gives developers and agencies access to development tools, including unlimited development stores. A development store is a full Shopify store with no time limit, but it cannot process real payments or be accessed publicly without a password.

EXACT STEPS
  1. Create a Shopify Partner account. Go to partners.shopify.com. Sign up with your email. No credit card required. This gives you access to the Partner Dashboard.
  2. Create a development store. In the Partner Dashboard, go to Stores → Add Store → "Create development store." Choose "Create a store to test and build" (not "Create a store for a client"). Store name: tracking-lab. Store URL will be: tracking-lab.myshopify.com.
  3. Add a test product. In the store admin, go to Products → Add Product.
    Title: "Test Product"
    Price: $49.99
    Status: Active
    Add a placeholder image (any image).
    Save.
  4. Enable Bogus Gateway for test orders. Settings → Payments → scroll to "Manual payment methods" or use the Bogus Gateway.
    For the Bogus Gateway: In "Payments" search for the test gateway option. Enter gateway name Bogus Gateway.
    This lets you create test orders without real payment processing.
  5. Create a test order. Visit your store's storefront (preview), add the Test Product to cart, proceed to checkout, enter test customer details:
    Name: Lab TestUser
    Email: labtestuser@example.com
    Address: 123 Test Street, New York, NY 10001
    Payment: Use the Bogus Gateway (enter "1" in the card number field to simulate a successful transaction).
    Complete the order. Note the order number and the thank-you page URL.
  6. Verify the order. In Shopify Admin → Orders, confirm your test order appears with the correct product and price.
ACCEPTANCE CRITERIA
LAYER 2

Install Meta Pixel Helper & Baseline Audit

OBJECTIVE

Install the diagnostic tools and establish what fires on a clean store (nothing, yet).

WHY IT EXISTS

Before you install any tracking, you need to know what the "zero state" looks like. This is your baseline. Every event you see after this point was added by a specific installation step, and you can trace it back.

EXACT STEPS
  1. Install Meta Pixel Helper. Go to the Chrome Web Store, search "Meta Pixel Helper," install the extension. Pin it to your toolbar.
  2. Baseline audit. Open your development store in an Incognito window. Visit:
    • Homepage
    • Product page
    • Cart page
    • Checkout (start but don't complete)
    On each page, click the Pixel Helper icon. Record what you see (should be: no pixels detected).
  3. Open Chrome DevTools. On any page, press F12 → Network tab → filter by facebook.com/tr.
    You should see zero requests. This confirms no Meta tracking is installed.
  4. Document the baseline. Take a screenshot of Pixel Helper showing "No pixels found" on the homepage. This is your "before" state.
ACCEPTANCE CRITERIA
LAYER 3

Install the Native Meta App (Installation Point #1)

OBJECTIVE

Install the official Facebook & Instagram channel app. This is the correct, Shopify-recommended way to install Meta tracking.

WHY IT EXISTS

On real client stores, this app is almost always installed. You need to understand what it does automatically so you can recognize when other installations conflict with it.

CONCEPTS

Data Sharing Levels:

LevelWhat It SendsCAPI?
StandardBrowser Pixel events only (PageView, ViewContent, AddToCart, Purchase)No
EnhancedBrowser Pixel + hashed customer data (email, phone) appended to eventsNo
MaximumBrowser Pixel + server-side CAPI events with automatic event_id deduplicationYes
EXACT STEPS
  1. Install the app. In your Shopify Admin, go to Sales Channels → "+" → search "Facebook & Instagram" → Add. (If this fails on a development store, see the Free-Sandbox Limitation note in Part 7.)
  2. Connect your Meta account. Follow the OAuth flow. Connect to your Meta Business Manager, select your Pixel/Dataset (the same one from Module 1 if you have one, or create a new one).
  3. Set data sharing to Maximum. In the Facebook & Instagram app settings → Data Sharing → select "Maximum." This enables CAPI.
  4. Verify with Pixel Helper. Open your store in Incognito. Visit:
    • Homepage → Pixel Helper should show PageView
    • Product page → ViewContent
    • Add to Cart → AddToCart
    • Complete a test order → Purchase on the thank-you page
    Record each Pixel ID shown. This is your "native app Pixel ID."
  5. Verify in Events Manager. Go to Meta Events Manager → your Dataset. Under "Overview," you should see the events arriving. Check that both "Browser" and "Server" sources are listed for Purchase (indicating both Pixel and CAPI are active).
  6. Record the Pixel ID. Write down the Pixel ID the native app is using. You will need this to identify conflicts later.

What the Native App Handles Automatically

• Browser Pixel installation (injected into the storefront via Shopify's Web Pixels API)

• Server-side CAPI events (sent from Shopify's servers, not the browser)

• Matching event_id between browser and server events (deduplication)

• SHA-256 hashing of customer PII (email, phone)

• Correct event timing and value parameters

If this app is the only thing installed, tracking works correctly out of the box.

ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. What is the difference between "Standard" and "Maximum" data sharing?

2. If a store has data sharing set to "Standard," is CAPI active?

3. Who generates the event_id for deduplication — the browser Pixel code or Shopify's server?

LAYER 4

Create Duplicate Source #1: theme.liquid (Installation Point #2)

OBJECTIVE

Deliberately install a second, conflicting Meta Pixel in theme.liquid. This simulates the most common duplication scenario (~30% of cases).

WHY IT EXISTS

On real client stores, a developer often pasted Meta Pixel code into theme.liquid in 2019–2022, before the native app existed or before the store owner installed it. When the native app was later added, nobody removed the old code. Both now fire independently. You need to create this scenario, see its effects, and then fix it.

EXACT STEPS
  1. Go to theme code editor. Shopify Admin → Online Store → Themes → your active theme → "..." → Edit Code.
  2. Open theme.liquid (under Layout).
  3. Paste the following code just before the closing </head> tag. Use a different Pixel ID than the native app (create a second Pixel in Meta Events Manager, or use the same one — even the same ID without matching event_id will cause duplication):
    <!-- DELIBERATELY ADDED FOR LAB: DUPLICATE PIXEL -->
    <script>
      !function(f,b,e,v,n,t,s)
      {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
      n.callMethod.apply(n,arguments):n.queue.push(arguments)};
      if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
      n.queue=[];t=b.createElement(e);t.async=!0;
      t.src=v;s=b.getElementsByTagName(e)[0];
      s.parentNode.insertBefore(t,s)}(window, document,'script',
      'https://connect.facebook.net/en_US/fbevents.js');
      fbq('init', 'YOUR_SECOND_PIXEL_ID');
      fbq('track', 'PageView');
    </script>
    Replace YOUR_SECOND_PIXEL_ID with an actual Pixel ID (either a second Pixel you created, or the same Pixel ID as the native app).
  4. Save the theme file.
  5. Verify the duplication. Open your store in Incognito. Visit the homepage. Open Pixel Helper:
    • If you used a different Pixel ID: You should see TWO Pixel IDs, each firing PageView.
    • If you used the same Pixel ID: You should see ONE Pixel ID but with PageView firing TWICE.
    Either way, this is a duplication problem.
  6. Complete a test order. Go through the full purchase flow. On the thank-you page, check Pixel Helper:
    • The native app fires Purchase (with CAPI in the background).
    • The theme.liquid code does NOT fire Purchase (it only has fbq('track', 'PageView')). But it fires an extra PageView on every page, including the thank-you page.
    • If you also added fbq('track', 'Purchase', ...) to the theme.liquid code, then Purchase fires twice on the browser side with different event_id values.

What You Just Created

You now have a store with TWO independent Pixel installations. The native app handles deduplication between its own browser + server events. But the theme.liquid code generates events with NO event_id at all — raw fbq('track', ...) calls don't include one. Meta cannot deduplicate events that have no event_id. Even if both use the same Pixel ID, the events are counted separately.

STUDENT QUESTIONS

1. If both installations use the same Pixel ID, does Meta automatically deduplicate them?

2. What happens if the theme.liquid code uses a different Pixel ID than the native app?

3. Why does the theme.liquid Pixel code not include an event_id?

LAYER 5

Create Duplicate Source #2: GTM (Installation Point #3)

OBJECTIVE

Install a Google Tag Manager container with a Meta Pixel tag. This simulates the most common duplication scenario (~40% of cases).

WHY IT EXISTS

Agencies and PPC specialists love GTM. They install a GTM container, add a Meta Pixel tag, and configure triggers. When they leave, the container stays. The store owner later installs the native Meta app (or another agency does). Now GTM and the native app both fire. Nobody audits the GTM container because "it's managed by the agency."

EXACT STEPS
  1. Create a GTM account and container. Go to tagmanager.google.com. Create an account and a Web container. Name it "Tracking Lab."
  2. Install GTM on Shopify. Copy the GTM container snippet. In Shopify, go to Online Store → Themes → Edit Code → theme.liquid. Paste the GTM <script> snippet right after the opening <head> tag. Paste the <noscript> snippet right after the opening <body> tag. Save.
  3. Create a Meta Pixel tag in GTM. In GTM:
    • Tags → New → Tag Type: "Custom HTML"
    • Paste the same fbq initialization and PageView code as Layer 4, using a Pixel ID (the same or different).
    • Trigger: "All Pages"
    • Name: "Meta Pixel — All Pages"
  4. Create a Purchase tag in GTM.
    • Tags → New → Tag Type: "Custom HTML"
    • Code:
    <script>
      fbq('track', 'Purchase', {
        value: {{Purchase Value}},
        currency: 'USD'
      });
    </script>

    • Trigger: Create a new trigger. Type: "Page View." Fire on: "Some Page Views." Condition: "Page URL contains thank_you"
    • Name: "Meta Purchase — Thank You Page"
    (For now, use a hardcoded value or GTM variable. The exact value extraction is not the point of this exercise — the duplication is.)
  5. Publish the GTM container. Submit → Publish.
  6. Verify the triple installation. Open your store in Incognito. Visit the homepage. Pixel Helper should now show events from THREE sources: native app, theme.liquid, and GTM.
    Complete a test order. On the thank-you page, count how many Purchase events fire.

The GTM Trigger Problem

The GTM Purchase trigger uses "Page URL contains thank_you." This fires on every page load of that URL — including:

• Customer reloading the thank-you page

• Customer using browser back button and navigating forward again

• Customer checking their order status (which uses the same URL)

Shopify's native checkout_completed event fires once per order. The GTM URL-based trigger fires on every page load. This is a compounding duplication on top of the Pixel-source duplication.

STUDENT QUESTIONS

1. How many independent Purchase events can fire on a single order with all three installations active?

2. Why is "Page URL contains thank_you" a dangerous GTM trigger for Purchase?

3. How would you find the GTM container ID on a client's store without GTM admin access?

LAYER 6

Create Duplicate Source #3: Additional Scripts (Installation Point #4)

OBJECTIVE

Add legacy purchase tracking code in Shopify's Additional Scripts field. This simulates the ~15% of duplication cases caused by forgotten checkout scripts.

WHY IT EXISTS

Before Shopify's Customer Events system existed (pre-2023), the standard way to track purchases was to paste JavaScript into Settings → Checkout → Additional Scripts. Thousands of tutorials and Shopify support articles recommended this approach. The code is still there on many stores — invisible unless you know to look for it.

EXACT STEPS
  1. Go to Additional Scripts. Shopify Admin → Settings → Checkout → scroll down to "Order status page" section → "Additional Scripts" textarea.
  2. Paste legacy purchase tracking code:
    <script>
      // DELIBERATELY ADDED FOR LAB: LEGACY CHECKOUT SCRIPT
      if (typeof fbq !== 'undefined') {
        fbq('track', 'Purchase', {
          value: {{ checkout.total_price | money_without_currency }},
          currency: '{{ checkout.currency }}'
        });
      }
    </script>
    This uses Shopify's Liquid template variables to pull the actual order value.
  3. Save.
  4. Complete another test order. On the thank-you page, count the total Purchase events in Pixel Helper. You should now see Purchase firing from: native app, GTM tag, AND this Additional Scripts code. (The theme.liquid code fires PageView everywhere but may not fire Purchase unless you added it.)

Why This Is Especially Insidious

Additional Scripts are hidden deep in Shopify's settings. There is no visual indicator that code exists there. A store owner who hires you to "fix tracking" may not even know this field exists. You must check it on every audit.

ACCEPTANCE CRITERIA
LAYER 7

Build the Diagnostic Matrix

OBJECTIVE

Map every Meta event source on the store into a single diagnostic document. This is the deliverable you hand to the client as "Phase 1: Diagnosis."

WHY IT EXISTS

You cannot fix what you haven't mapped. A freelancer who says "I found a duplicate Pixel" and removes one thing may miss two other sources. The matrix forces a systematic audit of all 5 installation points before you touch anything.

EXACT STEPS
  1. Create the diagnostic matrix. Use a spreadsheet or table with these columns:
    Installation PointLocationPixel IDEvents FiredHas event_id?CAPI?StatusAction
    1. Native AppSales Channels → FB & IG[your ID]PageView, ViewContent, AddToCart, PurchaseYes (automatic)Yes (Maximum)KEEPThis is the correct installation
    2. theme.liquidLayout/theme.liquid, line ~45[your ID or different]PageView (all pages)NoNoREMOVEDelete the script block
    3. GTMGTM container GTM-XXXXXX[your ID or different]PageView (all pages), Purchase (thank_you URL)NoNoREMOVEPause or delete Meta tags in GTM
    4. Additional ScriptsSettings → CheckoutSame as #2 (uses existing fbq)Purchase (thank-you page)NoNoREMOVEDelete the script
    5. Third-party appsSettings → Customer EventsN/ANone foundN/AN/AN/ANo action
  2. Count the damage. For one test Purchase:
    • Native app: 1 Purchase (browser) + 1 Purchase (CAPI) = 2 events, deduplicated to 1 by matching event_id
    • GTM: 1 Purchase (browser, no event_id) = counted separately
    • Additional Scripts: 1 Purchase (browser, no event_id) = counted separately
    • Total: Meta sees 3 distinct Purchase events for 1 actual order
    • If the customer reloads the thank-you page: GTM and Additional Scripts fire again, native app does not. Total could be 5+ events.

This Is Your Phase 1 Deliverable

On a real Upwork job, this matrix IS the diagnosis. You present it to the client with: "I found 3 independent sources of Meta tracking on your store. Here's where each one is, what it does, and which ones need to be removed." The client sees you've been thorough. This is how you justify your rate.

LAYER 8

Fix: Remove Duplicate Sources

OBJECTIVE

Remove all duplicate tracking sources, leaving only the native app. Verify single-fire behavior.

WHY IT EXISTS

The fix is conceptually simple: remove everything except the one correct installation. But on a real client store, you must do it carefully — document what you're removing, take backups, and verify after each removal.

EXACT STEPS
  1. Duplicate your theme as a backup. Online Store → Themes → "..." → Duplicate. This gives you a rollback point.
  2. Remove the theme.liquid code. Edit Code → theme.liquid → find the fbq script block you added in Layer 4 → delete the entire block (from the comment to the closing </script> tag). Save.
    Verify: Visit homepage in Incognito. Pixel Helper should show ONE fewer Pixel source.
  3. Pause/remove the GTM Meta tags. Open GTM → Tags → find "Meta Pixel — All Pages" and "Meta Purchase — Thank You Page" → Pause both tags (don't delete yet, in case you need to reference them).
    Publish a new GTM container version.
    Verify: Pixel Helper should now show only the native app's events.
  4. Remove the Additional Scripts code. Settings → Checkout → Additional Scripts → delete the Purchase tracking code you added in Layer 6. Save.
    Verify: Complete a test order. On the thank-you page, Pixel Helper should show exactly ONE Purchase event from the native app.
  5. Final verification. Complete one more test order. Check:
    • Pixel Helper: exactly 1 Purchase event on the thank-you page
    • Events Manager: 1 Purchase (browser) + 1 Purchase (server), deduplicated to 1 event
    • No PageView duplicates on any page
    • Reload the thank-you page — Purchase should NOT fire again
ACCEPTANCE CRITERIA
STUDENT QUESTIONS

1. Why do you pause GTM tags rather than delete them immediately?

2. Why duplicate the theme before editing?

3. After removing all duplicates, should Meta's reported purchases go up, down, or stay the same?

4. The client will see their "ROAS drop" after your fix. How do you explain this to them?

LAYER 9

Reconciliation: Meta vs. Shopify Orders

OBJECTIVE

Compare Meta Events Manager data against Shopify's order records. This is the validation step that proves your fix worked.

WHY IT EXISTS

The client said: "Validate corrected data against our backend Shopify/Stripe transaction records for at least 3 days." This is how a professional proves the fix. You don't just say "I removed the duplicates" — you show the numbers match.

CONCEPTS

Why it won't be exactly 100% match:

FactorEffectTypical Impact
Ad blockersBlock browser Pixel, but CAPI still sendsBrowser events 10–30% lower than actual
Consent bannersSuppress tracking before consentVaries by region (EU: significant)
Safari ITPBlocks third-party cookies, limits first-partyReduces _fbc cookie availability
CAPI with Maximum sharingServer-side events bypass ad blockersShould capture 85–95% of actual orders
Attribution windowMeta attributes purchases to ad clicks within 7/1 day windowsOrganic purchases not counted as conversions
EXACT STEPS
  1. Create 5 test orders on your development store over a period of time (e.g., 5 orders spread across 1–2 hours). Use different test customer details for each.
  2. Export Shopify orders. Orders → Export → CSV. Note the order count and total revenue.
  3. Check Meta Events Manager. Go to Events Manager → your Dataset → Purchase event. Filter by the same time period. Note the event count.
  4. Build the reconciliation table:
    MetricShopify OrdersMeta Events ManagerRatioVerdict
    Purchase count551.0Clean (in test environment, no ad blockers)
    Total revenue$249.95$249.951.0Values match
  5. Repeat with duplicates active (optional, for comparison). Re-enable one duplicate source, create 3 more orders, and compare again. You should see Meta reporting 6+ events for 3 orders.

On a Real Client Store

You would run this comparison over 3–7 days of real traffic, not test orders. The ratio should settle between 0.85 and 1.0 (Meta reports 85–100% of actual orders). Below 0.85 means tracking is under-reporting (possible consent/blocking issue). Above 1.0 means duplication still exists. Above 1.5 means significant duplication. The client's starting ratio was ~5.0.

LAYER 10

Event Match Quality Deep Dive

OBJECTIVE

Understand and improve the Event Match Quality (EMQ) score on the store's events. Fixing duplication is half the job — the other half is making sure the remaining events carry enough identity data for Meta to match them to users.

WHY IT EXISTS

The client brief mentions "demonstrable CAPI and server-side tracking experience." A freelancer who removes duplicates but ignores EMQ has only done half the work. Low EMQ means Meta can't attribute conversions to ad clicks, which means the client's campaign optimization suffers even with accurate event counts.

CONCEPTS

Recall from Module 1: EMQ scores each event on a 1–10 scale based on the identity parameters it carries.

ParameterApproximate EMQ ImpactShopify Native App Sends It?
em (hashed email)+4 pointsYes (with Maximum sharing)
ph (hashed phone)+3 pointsYes (if customer provides phone)
fbc (click ID)+3 pointsYes (if customer arrived via Meta ad)
fbp (browser ID)+2 pointsYes (browser Pixel sets this cookie)
fn, ln (name)+1 point eachYes (with Maximum sharing)
external_id+1 pointDepends on configuration
client_ip_address+1 pointYes (CAPI sends this)
client_user_agent+1 pointYes (CAPI sends this)
EXACT STEPS
  1. Check EMQ in Events Manager. Go to Events Manager → your Dataset → click on the Purchase event → "Event Match Quality" tab. Note the score and which parameters are contributing.
  2. Identify gaps. Common gaps on Shopify:
    fbc missing: Customer didn't arrive via Meta ad (common in test environment)
    ph missing: Customer didn't provide phone at checkout
    external_id not sent: Native app may not send this by default
  3. Document the EMQ state. Create a table showing each parameter, whether it's being sent, and what would need to change to include it.

Important Context for the Client

EMQ is only meaningful for CAPI events (server-side). Browser Pixel events don't have an EMQ score because they match via cookies. The native Shopify app with "Maximum" sharing typically achieves EMQ 6–8 out of 10 for logged-in customers, and 3–5 for guest checkouts. Improving beyond this requires custom implementation (adding external_id, capturing phone more aggressively, etc.) — which is out of scope for a $10–$30/hr diagnostic fix job, but worth mentioning to the client as a future improvement.

LAYER 11

Debugging Lab: Shopify-Specific Failures

OBJECTIVE

Encounter and diagnose the specific failure modes you'll see on real Shopify stores.

Failure 1: Purchase fires on order status page revisit

Create the failure: Re-enable the GTM Purchase tag with the "URL contains thank_you" trigger. Complete a test order. Then close the browser, reopen it, and visit the same thank-you URL (copy it from your order confirmation email or browser history).

Symptom: Purchase fires again. Events Manager shows 2 Purchase events for 1 order.

Root cause: URL-based triggers fire on every page load. The order status page is reachable after the initial purchase.

Fix: Use Shopify's checkout_completed Customer Event instead of URL matching. Or, if GTM must stay, use a GTM variable that checks for a "purchase_tracked" cookie and only fires once.

Lesson: URL-based triggers are never safe for purchase tracking on Shopify. Always use the event-based approach.

Failure 2: theme.liquid Pixel uses document.cookie for _fbp

Create the failure: In a custom pixel (Settings → Customer Events → Add Custom Pixel), write code that tries to read _fbp using document.cookie instead of browser.cookie.get('_fbp').

Symptom: No error. The code runs. But _fbp is undefined. The event fires to Meta with no browser ID.

Where to inspect: Add console.log('fbp:', document.cookie) to the custom pixel — it logs empty string. Use console.log('fbp:', await browser.cookie.get('_fbp')) instead — it returns the actual value.

Fix: Always use browser.cookie.get() inside Shopify's Web Pixels sandbox.

Lesson: Silent failures are the most dangerous. The tracking "works" but sends incomplete data. Only an EMQ audit reveals the problem.

Failure 3: Missing keepalive on checkout_completed fetch

Create the failure: In a custom pixel, subscribe to checkout_completed and send a CAPI event via fetch() WITHOUT keepalive: true.

Symptom: Most purchases track correctly, but 3–7% silently fail. The customer completes checkout and navigates away (or the browser starts unloading the page) before the fetch completes.

Root cause: Without keepalive: true, the browser can cancel the request when the page unloads. Checkout completion often triggers a redirect, which starts unloading.

Fix: Always use fetch(url, { method: 'POST', keepalive: true, body: ... }) or navigator.sendBeacon(url, body) for events that fire during page transitions.

Lesson: This failure is intermittent and invisible. You'll only catch it by comparing Meta event counts to Shopify order counts over time. A 5% drop in tracked purchases is enough to hurt campaign optimization.

Failure 4: Two Pixel IDs, same store, different ad accounts

Create the failure: Install the native app with Pixel A (owned by the store's Business Manager). Add a theme.liquid Pixel with Pixel B (owned by an agency's Business Manager that ran campaigns last year).

Symptom: Pixel Helper shows TWO different Pixel IDs. Events go to two different Event Managers. The current ad campaigns (using Pixel A) see correct data. But the old Pixel B is still receiving events, and if anyone accidentally uses Pixel B for a campaign, they get data that includes all traffic but is completely disconnected from the store's actual conversion history.

Fix: Identify which Pixel ID is currently used for ad campaigns. Remove all others.

Lesson: Always ask the client: "Which Pixel ID are your current ads using?" Then verify it matches what's installed on the store.

Failure 5: Value in cents vs. dollars

Create the failure: In a GTM or custom pixel Purchase event, map the value from Shopify's checkout data without checking the format. Some data sources report in cents (5000 for $50.00), others in dollars (50.00).

Symptom: Events Manager shows purchases with values 100x higher than actual. A $50 order reports as $5,000.

Fix: Always verify the unit of the value field. Shopify's Liquid {{ checkout.total_price }} reports in the store's subunit (cents). {{ checkout.total_price | money_without_currency }} reports in dollars. The Customer Events API event.data.checkout.totalPrice.amount reports as a decimal string ("50.00").

Lesson: This is the value-inflation component of the client's "5x ROAS" problem. A single value error multiplied across all purchases dramatically inflates reported revenue.

Failure 6: Native app CAPI stops working after token expiry

Create the failure: Disconnect and reconnect the Facebook & Instagram app, or revoke the app's permissions in Meta Business Settings.

Symptom: Browser Pixel events continue (they don't need a token). CAPI events stop. Events Manager shows events from "Browser" only, no "Server" source. Purchase count appears to drop because CAPI was catching events that ad blockers prevented the browser from sending.

Fix: Reconnect the app. In Facebook & Instagram channel settings, verify the connection is active. In Events Manager, verify both Browser and Server sources are receiving events.

Lesson: "Tracking stopped working" on Shopify often means CAPI stopped, not the Pixel. Always check for both sources in Events Manager.

LAYER 12

Client Handover: The Tracking Health Runbook

OBJECTIVE

Write the maintenance runbook the client requested. This is the final deliverable.

WHY IT EXISTS

The client brief says: "Document the final setup and provide a maintenance runbook so our team can troubleshoot." A freelancer who fixes the problem but doesn't document it will get hired again when it breaks. A freelancer who documents it earns the client's trust and a 5-star review. The review matters more than the repeat work.

RUNBOOK TEMPLATE

Write a document with these sections:

1. Current Tracking Architecture

Single source: Facebook & Instagram channel app, data sharing set to "Maximum." Pixel ID: [ID]. No other tracking installations on the store.

2. What Was Removed (and why)

theme.liquid manual Pixel code (lines X–Y): Causing duplicate PageView events. Removed [date].

• GTM container GTM-XXXXXX, tags "Meta Pixel — All Pages" and "Meta Purchase": Causing duplicate Purchase events with URL-based trigger. Tags paused [date].

• Additional Scripts Purchase code: Legacy checkout tracking causing duplicate Purchase. Removed [date].

3. Monthly Health Check (5 minutes)

• Open Meta Events Manager. Check that both "Browser" and "Server" sources are listed for Purchase. If only "Browser" appears, the CAPI connection may have broken — reconnect the Facebook & Instagram app.

• Compare last 7 days: Meta Purchase count vs. Shopify Orders count. Ratio should be 0.85–1.0. Above 1.0 = possible duplication returned. Below 0.85 = possible CAPI/consent issue.

• Open Pixel Helper on your store. Verify only ONE Pixel ID fires. If you see a second ID, someone installed something new.

4. What to Do When Tracking Breaks

• "ROAS suddenly doubled overnight" → Check for new app installations, theme changes, or GTM updates that re-introduced duplicate tracking.

• "Purchase events dropped to zero" → Check Facebook & Instagram app connection. Check if data sharing was changed from "Maximum."

• "Events show in Pixel Helper but not in Events Manager" → Check that the Pixel ID in Pixel Helper matches the Dataset in Events Manager. They might be looking at different Pixels.

5. Rules for Future Changes

• Do NOT add Meta Pixel code to theme.liquid. The native app handles this.

• Do NOT add Purchase tracking to Additional Scripts. The native app handles this.

• If you install a new marketing app, check Settings → Customer Events to verify it's not injecting its own Meta Pixel.

• If an agency needs GTM for other platforms (Google Ads, TikTok), that's fine — but NO Meta tags in GTM.

ACCEPTANCE CRITERIA

Part 9 — Student Deliverables

Complete all of these before considering this module finished.

Part 10 — Client Definition of Done

What success looks like from the client's perspective.

The system is "done" when:

Part 11 — Final Exam

Answer these without looking at the guide. If you cannot answer all 15, go back and review.

01. Name all 5 places where Meta tracking code can be installed on a Shopify store.
02. What does the "Maximum" data sharing setting do that "Standard" does not?
03. Why does the native Facebook & Instagram app's deduplication work, but a manual fbq('track', 'Purchase') in theme.liquid does not deduplicate with it?
04. A client says Meta reports 2x their actual purchases. What is the most likely cause?
05. A client says Meta reports 5x their actual purchases. What does the "5x" tell you about the number of duplicate sources?
06. What is Shopify's Web Pixels API sandbox, and why does it matter for cookie access?
07. What happens when you use document.cookie inside a Shopify custom pixel instead of browser.cookie.get()?
08. Why is "Page URL contains thank_you" a dangerous GTM trigger for Purchase events?
09. What is the expected ratio of Meta Purchase events to actual Shopify orders on a properly configured store with live traffic?
10. A store has the native Meta app AND a third-party app (Elevar) both sending Purchase events. Both use the same Pixel ID. Both include event_id. Will Meta deduplicate them? Why or why not?
11. After you fix tracking, the client's reported ROAS drops from 5x to 1.8x. They panic. What do you tell them?
12. What is Event Match Quality, and why does it matter even after you've fixed duplication?
13. Where is the Additional Scripts field in Shopify, and why is it the most commonly missed duplication source?
14. What should a monthly tracking health check include?
15. Write your Upwork application response for this job: a 2–3 sentence diagnostic approach summary and a Phase 1 turnaround estimate.

Diagnostic Standard

You have not completed this module if you can only remove duplicate Pixels. You must be able to reason through the full diagnostic chain:

WHAT IS THE SYMPTOM? → WHAT DATA CONFIRMS IT? → WHERE ARE ALL THE INSTALLATION POINTS? → WHICH ONES ARE DUPLICATES? → DO THEY HAVE EVENT_IDs? → DO THE IDs MATCH? → WHAT IS THE FIX? → HOW DO I VERIFY? → HOW DO I PROVE IT TO THE CLIENT? → HOW DO I PREVENT IT FROM COMING BACK?

— End of Module 2 —

MARKET RESEARCH Upwork Market Intelligence

Automation Niches & Market Penetration

Factual breakdown of automation sub-markets on Upwork. Data sourced from Upwork's official reports, hiring pages, and third-party market analyses.

Platform Context

Upwork's AI segment reached $300M+ annualized GSV (gross services volume) with 50% year-over-year growth. Within that, the AI Integration & Automation sub-segment grew over 90% YoY. The platform has 784,000 active clients as of Q1 2026, with GSV per client rising 5% to $5,138 despite flat total GSV. AI-enabled freelancers earn approximately 40% more per hour than those who don't use AI.

The reply-rate signal

Upwork doesn't publish job volumes by category. The best proxy for market health is reply rate — the percentage of proposals that get a client response. Platform mean is 7.45%. Categories above that have more demand than supply; below it, the reverse. Automation niches sit across both sides of that line.

YoY Growth: Automation-Adjacent Skills

From Upwork's official 2026 In-Demand Skills report (measured by completed job earnings, Feb 2026 release).

AI Video Gen/Edit
+329%
AI Integration
+178%
AI Data Annotation
+154%
E-commerce Mgmt
+130%
AI Image Gen/Edit
+95%
AI Chatbot Dev
+71%
Display Advertising
+26%
Scripting & Auto.
+23%

Note: "Scripting & Automation" was a fastest-growing skill in the 2025 report's Coding & Web Development category. By 2026, AI-specific skills overtook it in growth rate, though base volume for scripting/automation remains higher.

The Eight Automation Niches

1. Workflow Automation

HIGH DEMAND

Zapier, Make.com, n8n workflow builds, migrations, and maintenance.

8,838Zapier client reviews
5,129n8n client reviews
$12–$90/hrUpwork rate range
$300–$10K+Project pricing

2. Conversion Tracking

HIGH DEMAND

Meta Pixel/CAPI, GTM, GA4, server-side tagging, pixel audits.

8,034GTM client reviews
$20–$150/hrUpwork rate range
$200–$5KProject pricing
595Top freelancer's jobs

3. CRM Automation

HIGH DEMAND

HubSpot, Salesforce, Airtable — implementation, workflows, RevOps.

$25–$350/hrRate range (entry → principal)
$5K–$200KProject pricing
23%Salesforce global CRM share
$25/hrHubSpot Upwork median

4. AI Chatbot Dev

FAST GROWING

Conversational AI, customer support bots, AI agent builders.

+71%YoY demand growth
$9.6B → $41BGlobal market (2025→2033)
19.6%Market CAGR
$50–$150/hrTypical rate

5. AI Integration

FASTEST GROWING

Connecting AI models (OpenAI, Claude, etc.) into business tools and workflows.

+178%YoY demand growth
$40–$180/hrRate range
90%+AI auto. sub-segment growth

6. E-commerce Tracking

HIGH DEMAND

Shopify/WooCommerce pixel setups, server-side tracking, purchase event debugging.

+130%E-commerce mgmt growth
$200–$3KE-com tracking projects
$2K–$5KsGTM setup projects

7. AI Video/Image

FASTEST GROWING

AI video generation/editing, AI image generation, creative automation pipelines.

+329%Video YoY growth
+95%Image YoY growth

8. Data Annotation/AI Ops

FAST GROWING

AI data labeling, knowledge representation, model fine-tuning support.

+154%YoY demand growth
7.21%Reply rate (below avg)

Rate Comparison Across Niches

Niche Entry Rate Mid-Level Senior/Expert Typical Project
Workflow Auto. (Zapier/Make/n8n) $12–$30/hr $40–$80/hr $100–$180/hr $300–$10K
Conversion Tracking (GTM/CAPI/GA4) $20–$40/hr $50–$80/hr $100–$150/hr $200–$5K
CRM — HubSpot $25–$40/hr $55–$100/hr $140–$275/hr $5K–$70K
CRM — Salesforce $30–$55/hr $80–$135/hr $150–$350/hr $25K–$200K
AI Chatbot Dev $25–$50/hr $50–$100/hr $100–$180/hr $1K–$15K
AI Integration $30–$50/hr $50–$100/hr $100–$180+/hr $1K–$25K
n8n (standalone) $10–$25/hr $40–$80/hr $80–$150/hr $300–$15K
sGTM / Server-Side $30–$50/hr $60–$100/hr $120–$200/hr $2K–$5K

Rate data compiled from Upwork hiring pages (Aug 2026), Ciphernutz pricing guide, and Jobbers CRM freelancing guide. Upwork median rates skew lower than direct-client rates — the gap is widest in CRM consulting, where Upwork's HubSpot median ($25/hr) sits far below direct-client rates ($100–$200+/hr).

Saturation vs. Opportunity

Niche Competition Level Reply Rate Signal Barrier to Entry Verdict
Workflow Auto. High — large freelancer pool Above average for specialists; below for generalists Low — Zapier/Make are drag-and-drop CROWDED BUT ACTIVE
Conversion Tracking Moderate — technical barrier filters Strong — constant demand from ad spend Medium — requires Pixel/CAPI/GTM knowledge PROMISING
CRM (HubSpot) Moderate-High Strong at mid-level; saturated at entry Medium — requires HubSpot certification PROMISING
CRM (Salesforce) High — mature market Strong for certified specialists High — Salesforce certs + Apex/SOQL HIGH CEILING
AI Chatbot Dev Growing rapidly — still room Below platform mean (7.21% vs 7.45%) Medium — API skills + conversational design GROWING
AI Integration Low — demand outpacing supply Strong — 178% growth, nascent freelancer pool Medium-High — API + ML + business context MOST PROMISING
E-commerce Tracking Moderate Strong — tied to Shopify/WooCommerce volume Medium — platform-specific knowledge PROMISING
sGTM / Server-Side Low — specialist skill Strong — iOS privacy driving adoption High — cloud infra + data layer + consent MOST PROMISING

Most Promising Niches — Ranked

"Promising" here means: growing demand, lower freelancer saturation relative to job volume, and a rate ceiling that rewards skill depth.

1. AI Integration & Automation

Why: +178% YoY growth, the AI sub-segment alone is $300M+ annualized on Upwork and growing 90%+. Demand is outpacing supply — businesses want AI connected to their existing tools (CRMs, spreadsheets, workflows), not just chatbots. Few freelancers can bridge the gap between an API key and a production-grade integration. Senior rates reach $180+/hr.

Risk: Tooling is evolving fast. What's manual today may be one-click tomorrow. Skills built on a specific framework can depreciate quickly.

2. Server-Side Tracking (sGTM / CAPI)

Why: iOS privacy changes, cookie deprecation, and consent regulations are structurally forcing businesses from browser-side to server-side tracking. Projects run $2K–$5K for setup alone, with ongoing maintenance contracts. The knowledge barrier (cloud infrastructure + data layer design + consent frameworks + platform APIs) keeps the freelancer pool small. Top GTM freelancers on Upwork have completed 595 jobs and earned $100K+ on the platform.

Risk: Platforms are building native sGTM alternatives (Shopify's Web Pixels API, Meta's native CAPI integrations). Some of the setup work will get commoditized.

3. Conversion Tracking & Attribution (Meta/Google/TikTok)

Why: Every business running paid ads needs tracking. It breaks constantly — platform updates, consent changes, developer errors, duplicate installations. GTM has 8,034 client reviews on Upwork, indicating massive throughput. The "repair" sub-niche (fixing broken tracking) has higher conversion rates and less competition than new-build work. Entry at $200 fixed-price jobs, scaling to $5K for full audits and sGTM implementations.

Risk: Entry-level pixel installs are a commodity ($50–$100 jobs). You must move up to audits, CAPI, and server-side work to earn meaningful rates.

4. CRM Automation & RevOps

Why: Highest absolute rate ceiling of any automation niche. Salesforce CPQ implementations go to $200K. RevOps consulting reaches $250–$350/hr at the senior level. HubSpot implementations run $5K–$70K. The gap between Upwork marketplace rates (median $25/hr for HubSpot) and direct-client rates ($100–$200+/hr) is the widest in automation — meaning room to position above the floor.

Risk: Entry barrier is real. Salesforce requires certifications + Apex/SOQL. HubSpot is easier to enter but the entry-level market is crowded. Project timelines are long (weeks to months), and scope creep is common.

5. Workflow Automation (Zapier/Make/n8n)

Why: Largest volume — 8,838 Zapier client reviews alone. Low entry barrier means fast start. n8n migration work is growing as businesses move from Zapier's rising pricing ($29.99/mo+ for multi-step) to self-hosted alternatives. The niche that overlaps most with every other niche on this list.

Risk: Most crowded category. Pure Zapier builds are increasingly commoditized. You need a specialization on top of the platform skill (e.g., "Zapier + CRM" or "n8n + AI agents") to differentiate.

6. AI Chatbot Development

Why: +71% YoY growth, $9.6B global market heading to $41B by 2033 (19.6% CAGR). Businesses want customer-facing bots, internal knowledge bots, and support automation.

Risk: Reply rate is 7.21% — just below platform mean, suggesting freelancer supply is catching up to demand. No-code chatbot builders are lowering the floor. Differentiation requires deeper NLP/RAG/agent skills.

Key Strategic Facts

FactSource
AI-enabled freelancers earn 40% more per hour across all categoriesUpwork 2026 In-Demand Skills Report
Top AI skills demand grew 109% YoY (vs 23% for general high-demand skills)Upwork 2026 In-Demand Skills Report
84% of freelancers regularly use AI tools (up from 41% in 2023)Upwork / SelfEmployed.com analysis
Automation repair jobs convert better than new-build jobsRemoteAITools market analysis
Web Development reply rate: 5.80% (saturated); Lead Generation: 14.38% (undersupplied)GigRadar Upwork Market Report 2026 (133K proposals)
Speed-to-bid matters: reply rate drops from 8.99% to 6.91% after 5 minutesGigRadar Upwork Market Report 2026
77% of business leaders say AI is increasing demand for specialized freelancersUpwork 2026 In-Demand Skills Report
Upwork's variable fee structure charges 15% for commodity categories, 5–10% for specialized onesGigRadar Upwork Market Report 2026
CRM consulting has the widest gap between Upwork marketplace rates and direct-client ratesJobbers CRM Freelancing Guide

Where Your Course Covers

Niche Covered By Market Signal
Conversion Tracking (Meta CAPI, browser Pixel) Module 1 — Meta CAPI Lab PROMISING
E-commerce Tracking (Shopify diagnostics) Module 2 — Shopify Diagnostic PROMISING
GTM / sGTM pipelines Module 3 (planned) MOST PROMISING
Multi-platform tracking (Google, TikTok, LinkedIn) Module 4 (planned) PROMISING
iOS / Consent / Optimization Module 5 (planned) MOST PROMISING
Workflow Automation (n8n/Zapier) Modules 1 & 2 (as tooling) CROWDED BUT ACTIVE
CRM & RevOps Track B (planned) PROMISING
AI Integration Track C — Module 1 MOST PROMISING
AI Chatbot Dev Not yet covered GROWING

— End of Market Research —

MODULE 1 AI Integration & Automation Track

AI Integration Laboratory

A complete, hands-on student laboratory based on real Upwork AI integration projects.

Covers OpenAI & Claude APIs, n8n AI workflows, HubSpot CRM webhooks, Slack bots, and production prompt engineering. 16 implementation layers. Three live AI use cases delivered to a single SaaS client.

How to use this laboratory: Work through each Part and Layer in sequence. Every layer builds on the one before it. You will call real LLM APIs, build real n8n workflows, and process real data. By the end, you will have built and delivered three production AI integrations for a B2B SaaS client — and be able to scope, price, and pitch this work on Upwork.

What you will build in this module

#DeliverableLayers
1AI lead enrichment & scoring pipeline (HubSpot → n8n → LLM → HubSpot)Layers 7–9
2Support ticket triage & AI draft-response system (Webhook → n8n → LLM → Slack)Layers 10–12
3Internal knowledge-base Q&A bot (Slack → n8n → document retrieval → LLM → Slack)Layers 13–15
4Client handover package with documentation, runbook, and maintenance proposalLayer 16

Part 1 — Raw Client Brief

Read this exactly as a freelancer would see it on Upwork. This is the actual posting language.

AI Integration for SaaS Operations — Connect LLMs to Our Business Tools We're a B2B SaaS company (ClearMetrics — analytics platform for mid-market e-commerce). 35 employees, Series A funded. We use HubSpot CRM, Slack, and Google Workspace internally. THE PROBLEM Our sales team spends 2-3 hours/day manually researching new inbound leads — googling the company, checking LinkedIn, figuring out if they're a fit for our ICP. It's slow and inconsistent. Our support team gets 40-60 tickets/day through our help widget. Right now a human reads every ticket, categorizes it, and writes a first response. About 60% of tickets are repeat questions answered in our docs. Our team constantly asks the same internal questions — "What's our refund policy?", "How do I set up SSO for a client?", "Where's the onboarding checklist?" — and the answers are scattered across Google Docs, Notion, and Slack threads. WHAT WE NEED 1. AI Lead Enrichment When a new contact is created in HubSpot (form submission or manual entry), automatically: - Research the company (website, industry, size, recent funding) - Score them against our ICP (mid-market e-commerce, $5M-$100M revenue, Shopify Plus or custom platform) - Write a 2-3 sentence summary for the sales rep - Update the HubSpot contact with the enrichment data 2. Support Ticket Triage When a ticket comes in through our webhook: - Categorize it (bug, feature request, billing, how-to, account access) - Assign urgency (critical/high/medium/low) - Draft a response using our help docs as context - Post the categorization + draft to a Slack channel for the support rep to review and send We do NOT want auto-replies to customers. A human always reviews before sending. 3. Internal Knowledge Bot A Slack bot that team members can ask questions to. It should: - Search our internal docs (we'll provide ~50 Google Docs and a Notion export) - Answer with the relevant info and cite which document it came from - Say "I don't know" when it genuinely doesn't have the answer TOOLS WE USE - HubSpot CRM (Professional tier) - Slack (Business+) - Google Workspace - Notion (we can export to markdown) - We're open to n8n or Make.com for the middleware — prefer n8n if you're comfortable with it since we want to self-host eventually BUDGET & TIMELINE Budget: $2,000-$3,500 fixed price Timeline: 2-3 weeks Ongoing: Open to a monthly maintenance retainer after delivery ($300-500/mo) TO APPLY 1. Share a link to a previous AI integration you've built (n8n or Make workflow that calls OpenAI/Claude) 2. Your estimated API cost for us per month (we process ~200 leads and ~1,500 tickets monthly) 3. Which LLM would you recommend and why 4. Your fixed price and timeline We'll do a 30-min call with shortlisted candidates before hiring. Please don't apply with a generic proposal.

End of raw client brief. Everything above is the client's exact language.

Part 2 — Client Requirements Analysis

Translate the client's business language into technical requirements. Challenge every assumption.

This client is more sophisticated than most Upwork clients — they know their tools, they've defined an ICP, and they understand that AI shouldn't auto-reply to customers. That's a good sign. It also means your proposal needs to demonstrate technical depth, not just enthusiasm.

Requirement 1: "AI Lead Enrichment"

CLIENT SAID"When a new contact is created in HubSpot... automatically research the company."
INTERPRETATIONThey want an automation triggered by a HubSpot contact creation event. The LLM should research the lead's company and return structured data.
TECH QUESTIONWhere does the company data come from? The LLM's training data is stale — it doesn't know recent funding rounds or current employee counts.
TECH REALITYPure LLM-based "research" is unreliable for facts like company size or funding. You have two options: (1) scrape the company's website and feed the page content to the LLM for summarization, or (2) use a data enrichment API (Clearbit, Apollo, or similar) for structured data and use the LLM only for the ICP scoring and summary. Option 2 is more reliable. For this lab, we'll do a hybrid: scrape the company website with an HTTP request, then feed that content + any available HubSpot data to the LLM for scoring and summarization.
RECOMMENDATIONSet expectations with the client: LLMs are excellent at scoring and summarizing data they're given, but they are not reliable research tools for factual company data. Propose the hybrid approach.

Requirement 2: "Support Ticket Triage"

CLIENT SAID"Categorize it... assign urgency... draft a response using our help docs."
INTERPRETATIONA two-stage AI pipeline: (1) classify the ticket, (2) generate a draft response using relevant docs as context. This is a lightweight RAG pattern.
TECH QUESTIONHow do we get "our help docs as context" into the prompt? 50+ documents won't fit in a single prompt.
TECH REALITYFull RAG with vector embeddings is overkill for ~50 documents. A simpler approach: (1) use the LLM's classification output to select relevant doc sections via keyword matching, (2) inject those sections into the draft-response prompt. This is "poor man's RAG" and works surprisingly well for a bounded knowledge base. If the client's docs grow past ~200 pages, you can upgrade to proper vector search later — that's a separate project (and a separate invoice).
RECOMMENDATIONStart simple. Keyword-based retrieval + LLM drafting. The client said "help docs as context" — they don't need a vector database. They need it to work reliably for 5 categories of tickets.

Requirement 3: "Internal Knowledge Bot"

CLIENT SAID"A Slack bot that team members can ask questions to."
INTERPRETATIONA Slack integration that receives messages, searches internal docs, and replies with cited answers.
TECH QUESTIONHow does the bot receive messages? Slack's Events API via webhook, or a slash command?
TECH REALITYTwo approaches: (1) a Slack app with Events API that triggers an n8n webhook when the bot is mentioned, or (2) a Slack slash command (/ask) that hits an n8n webhook. The mention-based approach feels more natural but requires a Slack app with bot token scopes. The slash command is simpler to set up but less conversational. For a first version, a slash command is fastest to deliver and easiest for the client to maintain.
RECOMMENDATIONBuild with Slack slash command for v1. Propose mention-based interaction as a v2 enhancement (upsell opportunity).

Budget & Pricing Analysis

Pricing this project

The client's budget is $2,000–$3,500. Let's break down the actual work:

DeliverableEstimated HoursImplied Rate
Lead enrichment pipeline8–12 hrs$2,800 fixed
≈ $70–$93/hr effective
Support ticket triage8–10 hrs
Knowledge bot6–8 hrs
Documentation & handover4–6 hrs

Total: 26–36 hours. At $2,800 fixed price, your effective rate is $78–$108/hr. This is within the mid-level range for AI integration work on Upwork ($50–$100/hr). The monthly retainer ($300–$500/mo for ongoing maintenance) is a recurring revenue stream — always accept these.

API Cost Estimate for the Client

The client asked you to estimate their monthly API costs. This demonstrates your technical depth and builds trust.

Use CaseMonthly VolumeModelAvg Tokens/CallMonthly Cost
Lead enrichment200 leadsGPT-4.1-mini~2,000 in / ~500 out~$0.32
Ticket triage + draft1,500 ticketsGPT-4.1-mini~3,000 in / ~800 out~$3.72
Knowledge bot~500 queriesGPT-4.1-mini~4,000 in / ~600 out~$1.28
Estimated monthly total~$5.32

The cost conversation is a selling point

Most clients expect AI API costs to be hundreds of dollars per month. When you show them it's under $10/mo for their volume using GPT-4.1-mini ($0.40/M input, $1.60/M output), you immediately build credibility and remove a objection. If they need higher quality for the knowledge bot, Claude Haiku 3.5 ($0.80/$4.00) or GPT-4.1 ($2.00/$8.00) are still under $30/mo at their volume. Always lead with the cheapest model that works, then offer upgrades.

Part 3 — Crafting the Upwork Proposal

Your proposal is your first deliverable. It must demonstrate competence, not just claim it.

The client said: "Please don't apply with a generic proposal." They also asked for four specific things. Address every single one.

Sample Proposal

Hi [Client name],

I've built AI integrations with n8n for three SaaS companies this year — lead scoring with HubSpot webhooks, support triage pipelines, and Slack-based knowledge bots. Here's a recent one: [link to portfolio piece or Loom walkthrough].

Your API costs will be ~$5-10/month. At 200 leads + 1,500 tickets + ~500 bot queries, GPT-4.1-mini handles all three use cases for under $6/mo. If you want higher accuracy for the knowledge bot, Claude Haiku at ~$4/mo is the upgrade path — still under $15/mo total.

Model recommendation: GPT-4.1-mini for lead scoring and ticket triage (fast, cheap, reliable for structured classification). For the knowledge bot, I'd test both GPT-4.1-mini and Claude Haiku 3.5 during development and let the quality of answers decide. Both excel at document Q&A with citations.

My approach for the three use cases:

1. Lead enrichment: HubSpot webhook → n8n → scrape company website → LLM scores against your ICP criteria → writes summary + score back to HubSpot custom properties. No manual research needed.

2. Ticket triage: Your webhook → n8n → LLM classifies (category + urgency) → keyword-matches relevant help docs → LLM drafts response with doc context → posts to Slack for human review. Never auto-sends to customers.

3. Knowledge bot: Slack slash command → n8n webhook → searches your indexed docs → LLM answers with source citations → replies in Slack thread. Says "I don't know" when it should.

Fixed price: $2,800. Timeline: 2 weeks. I'll deliver in milestones: lead enrichment first (days 1-4), ticket triage second (days 5-9), knowledge bot third (days 10-13), documentation on day 14. Happy to discuss the monthly retainer after delivery.

I'm available for a 30-min call this week. What works for you?

Why this proposal works

Leads with cost: "$5-10/month" immediately differentiates you from applicants who don't understand API pricing. Names specific models: Shows you know the landscape, not just "we'll use AI." Architecture in plain English: The three numbered paragraphs mirror the client's three requirements — they can see you understood every piece. Milestone-based delivery: Reduces the client's risk — they see progress weekly, not a black box for 3 weeks. Defers the retainer conversation: Smart — don't negotiate recurring revenue before you've proven value.

Part 4 — Architecture & Tool Selection

Map the full system before writing a single workflow. Every integration decision has cost and maintenance implications.

System Architecture

                    ┌─────────────────────────┐
                    │     TRIGGER SOURCES      │
                    │                          │
                    │  HubSpot Webhook ─────┐  │
                    │  Support Widget ──────┤  │
                    │  Slack Command ───────┤  │
                    └───────────────────────┼──┘
                                            │
                                            ▼
                    ┌─────────────────────────┐
                    │     n8n MIDDLEWARE       │
                    │                          │
                    │  ┌── Receive trigger     │
                    │  ├── Fetch context data  │
                    │  ├── Build prompt        │
                    │  ├── Call LLM API        │
                    │  ├── Parse response      │
                    │  └── Route output        │
                    └───────────┬──────────────┘
                                │
               ┌────────────────┼────────────────┐
               ▼                ▼                ▼
    ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
    │   HubSpot    │  │    Slack     │  │   Logging    │
    │  (update     │  │  (post msg  │  │  (Google     │
    │   contact)   │  │   to chan)  │  │   Sheets)    │
    └──────────────┘  └──────────────┘  └──────────────┘

LLM API Selection Matrix

ModelInput / 1M tokensOutput / 1M tokensBest ForLatency
GPT-4.1-nano$0.10$0.40Simple classification only~200ms
GPT-4.1-mini$0.40$1.60Classification + drafting (our default)~400ms
GPT-4.1$2.00$8.00Complex reasoning, long documents~800ms
Claude Haiku 3.5$0.80$4.00Document Q&A with nuanced citations~300ms
Claude Sonnet 4$3.00$15.00High-stakes drafting, edge cases~1s

The model selection rule for client work

Start with the cheapest model that produces acceptable output. For this project: GPT-4.1-mini handles lead scoring, ticket classification, and basic drafting at <$6/mo. Only upgrade if the client tests the outputs and finds quality insufficient. Never start with GPT-4.1 or Claude Sonnet "just in case" — the client will see the API bill and question whether you optimized costs.

Why n8n (not Make, not Zapier)

Factorn8nMake.comZapier
AI node supportNative OpenAI + HTTP Request for any APIOpenAI module + HTTPChatGPT integration (limited)
Self-hostingYes — Docker, $0/moNoNo
Per-execution cost$0 self-hosted$0.0033/operation (Team)$0.01–$0.05/task
Complex branchingFull JS code nodes, IF/SwitchRouters, limited codePaths (limited)
HTTP flexibilityFull request builder, auth headersHTTP moduleWebhooks by Zapier (basic)
Client preference"prefer n8n... want to self-host eventually"

The client explicitly prefers n8n with a path to self-hosting. This is a common pattern for Series A SaaS companies — they want to own their infrastructure and avoid per-execution pricing as volumes grow. At 1,500 tickets/month on Zapier, that's $75–$150/mo in Zapier costs alone. n8n cloud is ~$24/mo; self-hosted is $0.

Tool Stack Summary

Trigger layer
HubSpot webhooks, HTTP webhooks, Slack slash commands
Middleware
n8n (cloud for development, self-hosted for production)
AI layer
OpenAI API (GPT-4.1-mini primary) via HTTP Request node
Output layer
HubSpot API (contact updates), Slack API (messages), Google Sheets (logging)

Part 5 — LLM API Fundamentals

Before building workflows, understand how to talk to an LLM programmatically. This is the skill that separates you from no-code freelancers.

LAYER 01

OpenAI Chat Completions API

OBJECTIVE

Understand the anatomy of an OpenAI API call and make your first successful request.

THE HTTP REQUEST

Every OpenAI API call is an HTTP POST to a single endpoint. No SDK required — any tool that can make HTTP requests (n8n, curl, Postman) can call the API.

POST https://api.openai.com/v1/chat/completions

Headers:
  Content-Type: application/json
  Authorization: Bearer sk-proj-YOUR_API_KEY

Body:
{
  "model": "gpt-4.1-mini",
  "messages": [
    {
      "role": "system",
      "content": "You are a lead scoring assistant for a B2B SaaS company."
    },
    {
      "role": "user",
      "content": "Score this lead: Company is 'TechNova', found at technova.io, they sell analytics dashboards to mid-market retailers."
    }
  ],
  "temperature": 0.2,
  "max_tokens": 500
}
KEY PARAMETERS
ParameterWhat it doesFor client work
modelWhich model to useStart with gpt-4.1-mini — cheapest that works well
messagesThe conversation history. Array of role+content objectsAlways include a system message defining the task
temperatureRandomness. 0 = deterministic, 2 = creative chaosUse 0.1–0.3 for classification and scoring. Use 0.5–0.7 for drafting responses
max_tokensMaximum output lengthSet a reasonable cap. Lead summaries: 200. Draft replies: 500. Don't leave unlimited
response_formatForce JSON output{"type": "json_object"} — critical for structured data. The model returns valid JSON
THE RESPONSE
{
  "id": "chatcmpl-abc123",
  "model": "gpt-4.1-mini-2025-07-18",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"score\": 72, \"fit\": \"partial\", \"summary\": \"TechNova sells analytics dashboards to mid-market retailers. Partial ICP match — right market segment (mid-market) but wrong vertical (retail, not e-commerce). Worth a discovery call if they serve Shopify Plus merchants.\"}"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 89,
    "completion_tokens": 67,
    "total_tokens": 156
  }
}

Always check usage

The usage object tells you exactly how many tokens the call consumed. At GPT-4.1-mini rates ($0.40/M input, $1.60/M output), this call cost $0.000036 + $0.000107 = $0.000143. Under one hundredth of a cent. Log these numbers for every call in production — token costs are how you prove to the client their bill is reasonable.

EXACT STEPS
  1. Create an OpenAI account. Go to platform.openai.com. Sign up and add a payment method (you'll need ~$1 of credit for this entire lab).
  2. Generate an API key. Go to API Keys → Create new secret key → name it "AI Integration Lab" → copy it immediately (you can't see it again). Store it securely.
  3. Make a test call. In your terminal, run:
curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-proj-YOUR_KEY" \
  -d '{
    "model": "gpt-4.1-mini",
    "messages": [
      {"role": "system", "content": "Reply with valid JSON only."},
      {"role": "user", "content": "Score this company as an ICP fit for a B2B e-commerce analytics SaaS: Acme Corp, 50 employees, Shopify Plus, $20M revenue."}
    ],
    "temperature": 0.2,
    "response_format": {"type": "json_object"}
  }'
  1. Verify the response. You should get a JSON response with the model's scoring. If you get a 401 error, your API key is wrong. If you get a 429 error, you've hit a rate limit — wait a minute and retry.
  2. Check your usage. Go to platform.openai.com/usage to see the API call logged with token counts and cost.
LAYER 02

Claude Messages API

OBJECTIVE

Learn the Claude API as an alternative. Many clients prefer Anthropic or want you to benchmark both providers.

THE HTTP REQUEST
POST https://api.anthropic.com/v1/messages

Headers:
  Content-Type: application/json
  x-api-key: sk-ant-YOUR_KEY
  anthropic-version: 2023-06-01

Body:
{
  "model": "claude-haiku-3-5-20241022",
  "max_tokens": 500,
  "system": "You are a lead scoring assistant for a B2B SaaS company. Always respond in valid JSON.",
  "messages": [
    {
      "role": "user",
      "content": "Score this lead: Company is 'ShopFlow', found at shopflow.com, they build inventory management for Shopify Plus stores. 80 employees, $15M ARR."
    }
  ]
}
KEY DIFFERENCES FROM OPENAI
FeatureOpenAIAnthropic/Claude
Auth headerAuthorization: Bearer sk-...x-api-key: sk-ant-...
System promptInside messages array as role "system"Top-level "system" field (separate from messages)
JSON moderesponse_format: {"type": "json_object"}No native JSON mode — instruct in system prompt + parse output
API versionIn URL path (/v1/)In header (anthropic-version)
Response pathchoices[0].message.contentcontent[0].text
Max tokensOptional (has default)Required — call fails without it
EXACT STEPS
  1. Create an Anthropic account. Go to console.anthropic.com. Sign up and add billing.
  2. Generate an API key. Settings → API Keys → Create Key → copy it.
  3. Make a test call.
curl https://api.anthropic.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk-ant-YOUR_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-haiku-3-5-20241022",
    "max_tokens": 500,
    "system": "You are a lead scoring assistant. Always respond in valid JSON with keys: score (0-100), fit (strong/partial/weak), summary (2-3 sentences).",
    "messages": [
      {"role": "user", "content": "Score: ShopFlow, shopflow.com, inventory management for Shopify Plus, 80 employees, $15M ARR."}
    ]
  }'
LAYER 03

Prompt Engineering for Business Tasks

OBJECTIVE

Write production-grade prompts that return consistent, structured, actionable outputs. This is the single most important skill in AI integration — the difference between a demo that impresses and a system that works in production.

THE FOUR RULES OF BUSINESS PROMPTS

Rule 1: Define the output format explicitly. Never let the model decide how to format its response. Specify exact JSON keys, field types, and value constraints.

// BAD — the model invents its own format every time
"Score this lead and tell me if they're a good fit."

// GOOD — deterministic, parseable output
"Analyze this lead against our ICP. Return valid JSON with exactly these keys:
{
  \"score\": integer 0-100,
  \"fit\": \"strong\" | \"partial\" | \"weak\",
  \"icp_match\": {
    \"market_segment\": boolean,   // mid-market ($5M-$100M revenue)
    \"vertical\": boolean,          // e-commerce
    \"platform\": boolean           // Shopify Plus or custom
  },
  \"summary\": string (2-3 sentences, actionable for sales rep),
  \"next_action\": \"call\" | \"email_sequence\" | \"disqualify\"
}"

Rule 2: Give the model the ICP criteria, not just the lead data. The model doesn't know your client's ICP unless you tell it. Include the scoring rubric in the system prompt.

SYSTEM PROMPT:
"You are a lead scoring assistant for ClearMetrics, a B2B analytics platform.

Our Ideal Customer Profile (ICP):
- Market: Mid-market e-commerce companies
- Revenue: $5M-$100M annually
- Platform: Shopify Plus or custom-built e-commerce
- Pain point: Need better analytics/attribution for their ad spend
- Decision maker: VP Marketing, CMO, or Head of Growth

Scoring rubric:
- 80-100: Strong ICP match on 4-5 criteria. Prioritize for immediate outreach.
- 50-79: Partial match. Worth a discovery call to explore fit.
- 0-49: Weak match. Add to nurture sequence, don't prioritize.

Always respond in valid JSON. Never include text outside the JSON object."

Rule 3: Include few-shot examples for consistent quality. Show the model what a good output looks like. This is especially important for tone and judgment calls.

USER PROMPT:
"Here is a lead to score:

Company: StyleHub
Website content: 'StyleHub is a DTC fashion brand selling through Shopify Plus. We did $22M in revenue last year and are scaling our paid social spend.'

Example of a correct scoring output:
{
  \"score\": 88,
  \"fit\": \"strong\",
  \"icp_match\": {\"market_segment\": true, \"vertical\": true, \"platform\": true},
  \"summary\": \"StyleHub is a $22M DTC fashion brand on Shopify Plus actively scaling paid social. Strong ICP match — they need attribution analytics as they increase ad spend.\",
  \"next_action\": \"call\"
}

Now score the following lead:
Company: DataPipe
Website content: 'DataPipe provides ETL pipelines for enterprise data teams. Fortune 500 clients. Annual contracts from $200K.'"

Rule 4: Handle edge cases in the prompt, not in your code. Tell the model what to do when data is missing, ambiguous, or outside its knowledge.

"If the company website is unreachable or returns no useful content:
- Set score to 0
- Set fit to \"unknown\"
- Set summary to \"Unable to access company website. Manual research required.\"
- Set next_action to \"email_sequence\"

If the company is clearly not in e-commerce (e.g., healthcare, government, fintech):
- Score based on any partial ICP overlap
- Always explain WHY in the summary"

Temperature settings for production

0.0–0.2: Classification tasks, scoring, categorization. You want the same input to produce the same output every time.
0.3–0.5: Summarization and structured drafting. Some variety is acceptable but the output should stay professional.
0.5–0.7: Draft email/message responses. You want natural language variation so responses don't feel robotic.
Never above 0.7 for client work. High temperatures produce creative but unreliable output. Save that for brainstorming, not production systems.

Part 6 — n8n Setup & AI Workflow Foundations

Set up your n8n environment and learn the nodes you'll use in every AI integration.

LAYER 04

n8n Environment Setup

OBJECTIVE

Get n8n running and configure credentials for OpenAI, HubSpot, and Slack.

EXACT STEPS
  1. Sign up for n8n cloud. Go to app.n8n.cloud → Start free trial. The free tier gives you 2,500 executions/month — plenty for development. Production will use self-hosted, but cloud is faster for building.
  2. Create OpenAI credentials. In n8n: Settings → Credentials → Add Credential → search "OpenAI" → paste your API key → Save. Test it by clicking "Test Connection."
  3. Create HubSpot credentials. In n8n: Add Credential → search "HubSpot" → choose "OAuth2" or "API Key."
    • For development: Use a HubSpot private app token. In HubSpot: Settings → Integrations → Private Apps → Create → name it "n8n AI Integration" → Scopes: select crm.objects.contacts.read, crm.objects.contacts.write, crm.schemas.contacts.read → Create App → copy the token.
    • Paste the token in n8n's HubSpot credential as the API key.
  4. Create Slack credentials. In n8n: Add Credential → search "Slack" → choose "OAuth2." You'll need a Slack app:
    • Go to api.slack.com/apps → Create New App → From Scratch → name it "ClearMetrics AI Bot" → select your workspace.
    • OAuth & Permissions → Bot Token Scopes: add chat:write, commands, channels:read.
    • Install to Workspace → copy the Bot User OAuth Token (xoxb-...).
    • Paste in n8n's Slack credential.
  5. Test each credential. Create a blank workflow → add each node (OpenAI, HubSpot, Slack) → test connection. All three should show green.

Security: Never hardcode API keys in workflows

Always use n8n's credential store. When you export a workflow to share with the client, credentials are automatically stripped. If you hardcode a key in an HTTP Request node URL or body, it will be visible to anyone who opens the workflow. This is a career-ending mistake on Upwork — if a client's API key leaks because of your workflow, you're liable.

LAYER 05

The HTTP Request Node for LLM Calls

OBJECTIVE

Learn to call any LLM API from n8n using the HTTP Request node. This is more flexible than the native OpenAI node and works with any provider.

WHY HTTP REQUEST INSTEAD OF THE OPENAI NODE

n8n has a native OpenAI node, but the HTTP Request node is better for client work because: (1) it works with any LLM provider (OpenAI, Anthropic, local models) without switching nodes, (2) you can see and control the exact request being sent, (3) it's easier to debug, and (4) it matches the API documentation directly.

N8N HTTP REQUEST CONFIGURATION
FieldValue
MethodPOST
URLhttps://api.openai.com/v1/chat/completions
AuthenticationGeneric Credential Type → Header Auth
Header NameAuthorization
Header ValueBearer sk-proj-YOUR_KEY (from credential)
Send BodyYes → JSON → specify body below
Response FormatJSON
// Body for the HTTP Request node:
{
  "model": "gpt-4.1-mini",
  "messages": [
    {
      "role": "system",
      "content": "{{ $json.systemPrompt }}"
    },
    {
      "role": "user",
      "content": "{{ $json.userPrompt }}"
    }
  ],
  "temperature": 0.2,
  "max_tokens": 500,
  "response_format": {"type": "json_object"}
}
EXTRACTING THE RESPONSE

The LLM's response is nested inside the HTTP response body. In the next n8n node, access it with:

// The raw text response:
{{ $json.choices[0].message.content }}

// If you used JSON mode, parse it:
{{ JSON.parse($json.choices[0].message.content) }}

// Access a specific field from the JSON:
{{ JSON.parse($json.choices[0].message.content).score }}

// Token usage (for logging):
{{ $json.usage.total_tokens }}
ERROR HANDLING IN N8N

LLM API calls fail. Rate limits (429), server errors (500), timeouts, malformed responses. In n8n, add error handling:

  1. Select the HTTP Request node → Settings → On Error: Continue (using error output).
  2. This creates a second output path (red) that fires when the request fails.
  3. Connect the error output to a Slack notification node that alerts you (or the client) when a call fails.
  4. For rate limits specifically, add an IF node that checks $json.statusCode === 429, then a Wait node (30 seconds), then retry the HTTP Request.
LAYER 06

Webhook Triggers & Data Routing

OBJECTIVE

Set up the entry points for all three use cases. Each workflow starts with a trigger — understanding webhook architecture is essential.

THE THREE TRIGGERS

Trigger 1: HubSpot Contact Created Webhook

HubSpot can send a webhook when a contact is created or updated. In HubSpot: Automation → Workflows → Create Workflow → trigger "Contact enrollment" → "Contact is created" → Action: "Send webhook" → URL: your n8n webhook URL.

// HubSpot sends this payload:
{
  "objectId": 12345,
  "properties": {
    "email": "jane@shopflow.com",
    "firstname": "Jane",
    "lastname": "Smith",
    "company": "ShopFlow",
    "website": "https://shopflow.com"
  },
  "subscriptionType": "contact.creation"
}

Trigger 2: Support Ticket Webhook (Generic)

The client uses a custom support widget that sends HTTP POST requests. This is the simplest trigger — a raw webhook.

// Support widget sends:
{
  "ticket_id": "TK-4521",
  "subject": "Can't export CSV from dashboard",
  "body": "Hi, I'm trying to export my Q3 data as CSV but the button is greyed out. I'm on the Pro plan. Browser: Chrome 126. This is urgent — I need this for a board meeting tomorrow.",
  "customer_email": "mike@retailco.com",
  "customer_name": "Mike Chen",
  "plan": "pro",
  "submitted_at": "2026-08-15T14:32:00Z"
}

Trigger 3: Slack Slash Command

When a user types /ask How do I set up SSO for a client? in Slack, Slack sends a POST to your n8n webhook.

// Slack sends (form-encoded, not JSON):
{
  "command": "/ask",
  "text": "How do I set up SSO for a client?",
  "user_id": "U0ABC123",
  "user_name": "sarah.kim",
  "channel_id": "C0DEF456",
  "response_url": "https://hooks.slack.com/commands/..."
}
N8N WEBHOOK NODE SETUP
  1. In n8n, create three separate workflows. In each, add a Webhook node as the trigger.
  2. Set HTTP Method to POST.
  3. For HubSpot and support webhooks: Response Mode → "When Last Node Finishes" (HubSpot expects a 200 response). For Slack: Response Mode → "Immediately" (Slack requires a response within 3 seconds).
  4. Copy each webhook's URL. You'll need these when configuring HubSpot workflows, the support widget, and the Slack app.
  5. For the Slack slash command: In your Slack app settings → Slash Commands → Create New Command → Command: /ask → Request URL: paste your n8n webhook URL → Short Description: "Ask the AI knowledge bot a question" → Save.

Part 7 — Use Case 1: AI Lead Enrichment & Scoring

Build the first deliverable: an automated pipeline that researches, scores, and enriches every new HubSpot contact.

LAYER 07

Lead Enrichment Workflow

OBJECTIVE

Build the complete n8n workflow: HubSpot webhook → fetch company website → LLM scores the lead → write enrichment data back to HubSpot.

WORKFLOW ARCHITECTURE
┌─────────────┐    ┌──────────────┐    ┌─────────────┐    ┌──────────────┐    ┌──────────────┐
│  Webhook     │───▶│ HubSpot:     │───▶│ HTTP Req:   │───▶│ HTTP Req:    │───▶│ HubSpot:     │
│  (trigger)   │    │ Get Contact  │    │ Fetch URL   │    │ OpenAI API   │    │ Update       │
│              │    │ (full data)  │    │ (company    │    │ (score +     │    │ Contact      │
│              │    │              │    │  website)   │    │  summarize)  │    │ (write back) │
└─────────────┘    └──────────────┘    └─────────────┘    └──────────────┘    └──────────────┘
                                              │                                      │
                                              ▼                                      ▼
                                       ┌─────────────┐                      ┌──────────────┐
                                       │ IF: website  │                      │ Google Sheet: │
                                       │ reachable?   │                      │ Log result   │
                                       └─────────────┘                      └──────────────┘
EXACT STEPS

Node 1: Webhook (trigger)

  1. This is the webhook you created in Layer 06. It receives HubSpot's contact creation event.
  2. The incoming data includes objectId (HubSpot contact ID) and basic properties.

Node 2: HubSpot — Get Contact

  1. Add a HubSpot node → Operation: Get Contact → Contact ID: {{ $json.body.objectId }}.
  2. Under "Additional Fields," request: email, firstname, lastname, company, website, phone, jobtitle.
  3. This fetches the full contact record since the webhook payload only includes basic fields.

Node 3: HTTP Request — Fetch Company Website

  1. Add an HTTP Request node → Method: GET → URL: {{ $json.properties.website }}.
  2. Set Options → Response Format: Text (we want the raw HTML).
  3. Set Options → Timeout: 10000 (10 seconds — don't wait forever for a slow site).
  4. Enable error handling (Continue on Error) — the site might be unreachable.

Node 4: Code Node — Extract Text from HTML

  1. Add a Code node between the HTTP Request and the LLM call.
  2. This strips HTML tags and extracts readable text. The LLM can't process raw HTML efficiently.
// Code node — extract text from HTML
const html = $input.first().json.data || '';
const text = html
  .replace(/<script[\s\S]*?<\/script>/gi, '')
  .replace(/<style[\s\S]*?<\/style>/gi, '')
  .replace(/<[^>]+>/g, ' ')
  .replace(/\s+/g, ' ')
  .trim()
  .substring(0, 3000); // Cap at ~3000 chars to control token usage

return [{ json: { websiteText: text, contact: $input.first().json } }];

Node 5: HTTP Request — OpenAI API Call

  1. Add an HTTP Request node (as configured in Layer 05).
  2. System prompt:
You are a lead scoring assistant for ClearMetrics, a B2B analytics platform for mid-market e-commerce.

ICP CRITERIA:
- Market segment: Mid-market e-commerce ($5M-$100M revenue)
- Platform: Shopify Plus, BigCommerce, or custom e-commerce
- Pain point: Needs better analytics, attribution, or data visibility
- Decision maker titles: VP Marketing, CMO, Head of Growth, Director of E-commerce

SCORING RUBRIC:
- 80-100: Strong match on 3+ criteria. Immediate outreach.
- 50-79: Partial match. Worth discovery call.
- 20-49: Weak match. Nurture sequence.
- 0-19: Not a fit. Disqualify.

Respond ONLY in valid JSON with these exact keys:
{
  "score": integer 0-100,
  "fit": "strong" | "partial" | "weak" | "disqualify",
  "summary": "2-3 sentence summary for the sales rep. Be specific about why.",
  "industry": "detected industry",
  "estimated_revenue": "best estimate or 'unknown'",
  "next_action": "immediate_call" | "discovery_email" | "nurture" | "disqualify"
}

If website content is empty or unavailable, set score to 0 and fit to "unknown".
  1. User prompt: Score this lead:\n\nName: {{ $json.contact.properties.firstname }} {{ $json.contact.properties.lastname }}\nCompany: {{ $json.contact.properties.company }}\nTitle: {{ $json.contact.properties.jobtitle }}\nEmail: {{ $json.contact.properties.email }}\n\nCompany website content:\n{{ $json.websiteText }}
  2. Set temperature to 0.2, max_tokens to 400, response_format to JSON mode.

Node 6: HubSpot — Update Contact

  1. Add a HubSpot node → Operation: Update Contact → Contact ID: {{ $('Webhook').first().json.body.objectId }}.
  2. Map the LLM's JSON response to HubSpot custom properties:

Create custom properties in HubSpot first

Before this node works, you must create these custom properties in HubSpot: Settings → Properties → Create Property (in the Contact object):

  • ai_lead_score — Number field (0-100)
  • ai_fit_level — Single-line text
  • ai_summary — Multi-line text
  • ai_next_action — Single-line text
  • ai_enriched_at — Date field
// Property mapping in the HubSpot Update node:
ai_lead_score:  {{ JSON.parse($json.choices[0].message.content).score }}
ai_fit_level:   {{ JSON.parse($json.choices[0].message.content).fit }}
ai_summary:     {{ JSON.parse($json.choices[0].message.content).summary }}
ai_next_action: {{ JSON.parse($json.choices[0].message.content).next_action }}
ai_enriched_at: {{ new Date().toISOString() }}
LAYER 08

Lead Scoring Calibration & Edge Cases

OBJECTIVE

Test the pipeline with diverse lead types, calibrate scoring accuracy, and handle failure modes.

TEST CASES
Test LeadExpected ScoreWhy
Shopify Plus store, $30M revenue, VP Marketing85–95Perfect ICP match
Custom e-commerce, $8M revenue, CEO70–85Right vertical, smaller, CEO not ideal buyer
B2B SaaS company, $50M revenue15–30Right size, wrong vertical
Freelance consultant, no website0–10Not a company, no data
Competitor analytics platform0–5Disqualify — competitor
Website unreachable (404 or timeout)0Graceful degradation, flag for manual review
CALIBRATION PROCESS
  1. Create 10 test contacts in HubSpot covering the cases above.
  2. Trigger the webhook for each (or run the workflow manually with test data).
  3. Review every LLM response. Check: Is the score reasonable? Is the summary actionable? Does the next_action match?
  4. If scores are consistently too high or too low, adjust the scoring rubric in the system prompt. Add examples if the model misunderstands a criterion.
  5. Log results in a spreadsheet: Lead | Expected Score | Actual Score | Correct? This is your QA artifact for the client handover.
LAYER 09

Logging, Monitoring & Cost Tracking

OBJECTIVE

Add production-grade logging so the client can see what's happening and you can debug problems remotely.

GOOGLE SHEETS LOG

Add a Google Sheets node at the end of the workflow that appends a row for every lead processed:

ColumnValue
Timestamp{{ new Date().toISOString() }}
Contact ID{{ $('Webhook').first().json.body.objectId }}
Company{{ $('HubSpot').first().json.properties.company }}
Score{{ JSON.parse($('OpenAI').first().json.choices[0].message.content).score }}
Fit{{ JSON.parse($('OpenAI').first().json.choices[0].message.content).fit }}
Tokens Used{{ $('OpenAI').first().json.usage.total_tokens }}
API Cost{{ (($('OpenAI').first().json.usage.prompt_tokens * 0.4 + $('OpenAI').first().json.usage.completion_tokens * 1.6) / 1000000).toFixed(6) }}
Statussuccess or error

Why logging matters for Upwork work

The Google Sheet becomes your proof of work. When the client asks "Is the AI working?" you can show them a live spreadsheet with every lead processed, every score, every cost. It also lets you demonstrate ROI: "Your AI scored 200 leads this month for $0.32 in API costs, saving your team ~40 hours of manual research." That's how you justify the maintenance retainer.

Part 8 — Use Case 2: Support Ticket Triage & AI Draft Responses

Build the second deliverable: automated ticket classification and response drafting with human review.

LAYER 10

Ticket Classification Pipeline

OBJECTIVE

Build the first stage of the triage system: receive a ticket, classify it by category and urgency, and route it.

WORKFLOW ARCHITECTURE
┌─────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Webhook     │───▶│ HTTP Req:    │───▶│ Code Node:   │───▶│ Slack:       │
│  (ticket     │    │ OpenAI API   │    │ Format msg   │    │ Post to      │
│   incoming)  │    │ (classify)   │    │ for Slack    │    │ #support-ai  │
└─────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
CLASSIFICATION SYSTEM PROMPT
You are a support ticket classifier for ClearMetrics, a B2B analytics SaaS.

CATEGORIES (pick exactly one):
- bug: Software defect, broken feature, error message, crash
- feature_request: New functionality, enhancement, integration request
- billing: Invoice, payment, plan change, refund, pricing question
- how_to: Usage question, setup help, configuration, best practices
- account_access: Login issues, SSO, permissions, password reset, MFA

URGENCY LEVELS:
- critical: Production system down, data loss, security issue, revenue impact
- high: Major feature broken, blocking customer's workflow, time-sensitive
- medium: Inconvenience, workaround exists, not time-sensitive
- low: Nice-to-have, general question, feedback

RULES:
- If the customer mentions "urgent", "ASAP", "board meeting", "demo tomorrow", or similar time pressure → upgrade urgency by one level
- If the customer is on Enterprise plan → default urgency to at least "medium"
- Bug + data-related → always "critical" (potential data integrity issue)

Respond ONLY in valid JSON:
{
  "category": string,
  "urgency": string,
  "confidence": float 0-1,
  "reasoning": "one sentence explaining classification",
  "key_issue": "the core problem in 10 words or less"
}
SLACK MESSAGE FORMAT

The Code node formats the classification into a readable Slack message:

const ticket = $('Webhook').first().json.body;
const classification = JSON.parse($('OpenAI').first().json.choices[0].message.content);

const urgencyEmoji = {
  critical: '🔴',
  high: '🟠',
  medium: '🟡',
  low: '🟢'
};

const message = `${urgencyEmoji[classification.urgency]} *${classification.urgency.toUpperCase()}* — ${classification.category}

*Ticket:* ${ticket.ticket_id}
*From:* ${ticket.customer_name} (${ticket.customer_email}) — ${ticket.plan} plan
*Subject:* ${ticket.subject}
*Key issue:* ${classification.key_issue}

> ${ticket.body.substring(0, 500)}

_AI confidence: ${(classification.confidence * 100).toFixed(0)}% — ${classification.reasoning}_`;

return [{ json: { text: message, ticket, classification } }];
LAYER 11

AI Draft Response with Document Context

OBJECTIVE

Add the second stage: retrieve relevant help docs and draft a response for the support rep to review.

DOCUMENT PREPARATION

Before the AI can draft responses, it needs access to the client's help documentation. We use a simple but effective approach: pre-process docs into a searchable format.

  1. Collect the client's help docs. Ask for exports in markdown or plain text. Most help desks (Intercom, Zendesk, HelpScout) can export articles as HTML or markdown.
  2. Create a document index. A JSON file that maps keywords to document chunks:
// docs-index.json — stored in n8n as a static file or a Code node
[
  {
    "id": "csv-export",
    "title": "How to Export Data as CSV",
    "keywords": ["export", "csv", "download", "data", "report"],
    "content": "To export your dashboard data as CSV:\n1. Navigate to the dashboard\n2. Click the '...' menu in the top-right\n3. Select 'Export as CSV'\n\nNote: CSV exports are available on Pro and Enterprise plans. Free plans can export up to 100 rows. If the export button is greyed out, check that your date range is selected and the dashboard has finished loading."
  },
  {
    "id": "sso-setup",
    "title": "Setting Up SSO for Your Organization",
    "keywords": ["sso", "single sign-on", "saml", "okta", "azure ad", "login"],
    "content": "SSO is available on Enterprise plans. To configure:\n1. Go to Settings → Security → SSO\n2. Select your identity provider (Okta, Azure AD, OneLogin)\n3. Enter your SAML endpoint URL and certificate\n4. Map user attributes..."
  }
  // ... more docs
]

Document retrieval logic (Code node):

// Simple keyword-based retrieval
const ticket = $('Webhook').first().json.body;
const classification = $('Classification').first().json;
const docsIndex = [/* your docs-index.json loaded here */];

// Extract keywords from ticket subject + body
const ticketText = (ticket.subject + ' ' + ticket.body).toLowerCase();

// Score each doc by keyword overlap
const scored = docsIndex.map(doc => {
  const matches = doc.keywords.filter(kw => ticketText.includes(kw));
  return { ...doc, matchCount: matches.length };
});

// Take top 3 most relevant docs
const relevant = scored
  .filter(d => d.matchCount > 0)
  .sort((a, b) => b.matchCount - a.matchCount)
  .slice(0, 3);

const context = relevant.map(d => `--- ${d.title} ---\n${d.content}`).join('\n\n');

return [{ json: { relevantDocs: context, ticket, classification } }];
DRAFT RESPONSE SYSTEM PROMPT
You are a support agent for ClearMetrics writing a draft reply to a customer ticket.

RULES:
- Be helpful, professional, and concise
- Reference specific steps from the documentation when available
- If the docs don't cover this issue, acknowledge that and suggest next steps
- Never make up features or capabilities not in the docs
- Never promise timelines for fixes or features
- Sign off as "ClearMetrics Support" (the human rep will personalize before sending)
- Keep responses under 200 words

DOCUMENTATION CONTEXT:
{{ $json.relevantDocs }}

TICKET CLASSIFICATION:
Category: {{ $json.classification.category }}
Urgency: {{ $json.classification.urgency }}

Respond in plain text (not JSON). Write the email body only — no subject line.

The human-in-the-loop principle

This system drafts responses — it never sends them automatically. The draft appears in Slack where a support rep reviews, edits, and sends. This is not optional. The client explicitly said "we do NOT want auto-replies." Even if they hadn't, auto-sending AI-drafted responses to customers is a business risk you should never take in a v1 delivery. Position the human review step as a feature, not a limitation.

LAYER 12

Complete Triage Workflow Assembly

OBJECTIVE

Wire the classification and drafting stages together into a single workflow with the complete Slack output.

FINAL SLACK MESSAGE FORMAT
const ticket = $('Webhook').first().json.body;
const classification = JSON.parse($('Classify').first().json.choices[0].message.content);
const draftResponse = $('Draft').first().json.choices[0].message.content;

const urgencyEmoji = { critical: '🔴', high: '🟠', medium: '🟡', low: '🟢' };

const message = `${urgencyEmoji[classification.urgency]} *NEW TICKET — ${classification.urgency.toUpperCase()}*

*ID:* ${ticket.ticket_id} | *Category:* ${classification.category} | *Plan:* ${ticket.plan}
*From:* ${ticket.customer_name} <${ticket.customer_email}>
*Subject:* ${ticket.subject}

> ${ticket.body}

---

📝 *AI Draft Response:*

${draftResponse}

---
_Classification confidence: ${(classification.confidence * 100).toFixed(0)}% | AI reasoning: ${classification.reasoning}_
_⚠️ Review and edit before sending to customer_`;

return [{ json: { channel: '#support-ai', text: message } }];

Part 9 — Use Case 3: Internal Knowledge Base Q&A Bot

Build the third deliverable: a Slack bot that answers team questions using internal documentation.

LAYER 13

Document Ingestion & Indexing

OBJECTIVE

Process the client's ~50 internal documents into a searchable format that the LLM can use as context.

DOCUMENT PROCESSING PIPELINE
  1. Collect documents from the client. Ask them to export their Google Docs as markdown (File → Download → Markdown) and their Notion workspace as markdown (Settings → Export → Markdown & CSV).
  2. Clean and chunk each document. Split long documents into sections of ~500–1000 words. Each chunk should be self-contained — it should make sense without the other chunks.
  3. Build the knowledge index. A structured JSON file with metadata:
// knowledge-base.json
[
  {
    "id": "refund-policy-001",
    "source_doc": "Company Policies — Refund and Cancellation",
    "section": "Refund Eligibility",
    "keywords": ["refund", "cancel", "money back", "billing", "charge"],
    "content": "ClearMetrics offers full refunds within 14 days of purchase for annual plans. Monthly plans can be cancelled at any time with no refund for the remaining period. To process a refund: 1. Customer submits request via support. 2. Support verifies the purchase date. 3. If within 14 days, process through Stripe dashboard. Partial refunds are handled case-by-case by the billing team."
  },
  {
    "id": "sso-setup-001",
    "source_doc": "IT Handbook — SSO Configuration",
    "section": "Setting Up SSO for a New Client",
    "keywords": ["sso", "saml", "okta", "azure", "single sign-on", "client onboarding"],
    "content": "To set up SSO for a new Enterprise client: 1. Confirm client is on Enterprise plan. 2. Navigate to Admin → Organizations → [Client Name] → Security. 3. Enable SAML SSO. 4. Provide the client with our SAML metadata URL: https://app.clearmetrics.io/saml/metadata. 5. Client provides their IdP metadata XML. 6. Upload the XML and test with a single user. 7. Enable 'Require SSO' only after successful test."
  }
  // ... 48 more chunks
]

Why keyword search instead of vector embeddings?

For ~50 documents, keyword matching is fast, free, debuggable, and accurate enough. Vector search adds: an embedding API cost ($0.02/M tokens with text-embedding-3-small), a vector database (Pinecone, Qdrant), and complexity the client must maintain. Save that for v2 if the knowledge base grows past 200+ documents. For now, simple wins. You can always upsell the vector upgrade later — that's a separate $1,000–$2,000 project.

LAYER 14

Slack Bot Workflow

OBJECTIVE

Build the Q&A workflow: Slack slash command → search knowledge base → LLM generates answer with citations → reply in Slack.

WORKFLOW ARCHITECTURE
┌─────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Webhook     │───▶│ Code Node:   │───▶│ HTTP Req:    │───▶│ Code Node:   │───▶│ HTTP Req:    │
│  (Slack      │    │ Search docs  │    │ OpenAI API   │    │ Format       │    │ Slack API    │
│   /ask cmd)  │    │ (keyword     │    │ (answer with │    │ Slack reply  │    │ (post msg)   │
│              │    │  matching)   │    │  citations)  │    │              │    │              │
└─────────────┘    └──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
KNOWLEDGE BOT SYSTEM PROMPT
You are ClearMetrics' internal knowledge assistant. Team members ask you questions about company policies, processes, and product features.

RULES:
1. Answer ONLY from the documentation provided below. Never make up information.
2. If the answer is in the docs, respond clearly and cite the source document.
3. If the answer is NOT in the docs, say: "I don't have information about that in my knowledge base. You might want to ask [relevant team] or check [relevant system]."
4. Be concise — Slack messages should be scannable, not essays.
5. Use bullet points for multi-step processes.
6. Always end with the source: "📄 Source: [document name]"

DOCUMENTATION:
{{ $json.relevantDocs }}

If no relevant documents were found for this query, respond: "I couldn't find any documentation related to your question. Try rephrasing, or ask in #general for help from the team."
SLACK RESPONSE

Post the response back to the user using Slack's response_url (provided in the slash command payload):

// Final HTTP Request node — respond to Slack
// Method: POST
// URL: {{ $('Webhook').first().json.body.response_url }}
// Body:
{
  "response_type": "in_channel",
  "text": "{{ $json.answer }}",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "💬 *{{ $('Webhook').first().json.body.user_name }}* asked: _{{ $('Webhook').first().json.body.text }}_"
      }
    },
    {
      "type": "divider"
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "{{ $json.answer }}"
      }
    }
  ]
}
LAYER 15

Bot Testing & "I Don't Know" Calibration

OBJECTIVE

Ensure the bot answers correctly when it should and says "I don't know" when it should. This is where most AI integrations fail — they hallucinate answers instead of admitting ignorance.

TEST MATRIX
QueryExpected BehaviorPass/Fail
"What's our refund policy?"Answers from refund-policy doc, cites source
"How do I set up SSO for a client?"Step-by-step from SSO doc, cites source
"What's the CEO's phone number?""I don't have that information" (not in docs)
"What's the meaning of life?""I don't have that information" (out of scope)
"Can I get a refund after 30 days?"References 14-day policy, explains limitation
"How do I set up SSO with OneLogin?"References SSO doc but notes OneLogin is supported, adapts steps
"What's our competitor's pricing?""I don't have that information" (not in docs, sensitive)

The hallucination test

Ask the bot a question that sounds like it could be in the docs but isn't. If the bot invents an answer instead of saying "I don't know," your prompt needs stronger guardrails. Add this to the system prompt: "If you are even slightly unsure whether the information is in the provided documentation, say 'I don't know.' It is always better to admit ignorance than to provide incorrect information."

Part 10 — Error Handling & Production Hardening

The difference between a demo and a production system is how it handles failure.

COMMON FAILURE MODES
FailureCauseImpactHandling
API rate limit (429)Too many requests/minWorkflow failsWait 30s → retry → if 2nd fail, alert Slack
API server error (500)OpenAI outageWorkflow failsRetry once → if fail, log + alert → manual processing
Malformed JSON responseModel didn't follow formatParse error in next nodeTry/catch in Code node → fallback to raw text
HubSpot webhook timeoutn8n slow to respondHubSpot retries (duplicates)Deduplication by objectId in first node
Company website unreachableDomain down, firewallNo enrichment dataScore 0, flag for manual review
Token limit exceededVery long website/ticketTruncated or rejectedTruncate input to 3000 chars before sending
API key expired/revokedKey rotation, billing issueAll calls failAlert owner immediately via email
N8N ERROR HANDLING PATTERN
// In any Code node that parses LLM output:
try {
  const result = JSON.parse($input.first().json.choices[0].message.content);
  return [{ json: { ...result, status: 'success' } }];
} catch (e) {
  // LLM didn't return valid JSON — use raw text as fallback
  return [{ json: {
    score: 0,
    fit: 'error',
    summary: $input.first().json.choices[0].message.content,
    next_action: 'manual_review',
    status: 'parse_error',
    error: e.message
  }}];
}

Part 11 — Client Handover & Documentation

Your deliverable isn't working code. It's a system the client's team can operate without you.

LAYER 16

Handover Package

DELIVERABLES CHECKLIST
RUNBOOK TEMPLATE
CLEARMETRICS AI INTEGRATION — RUNBOOK

1. SYSTEM HEALTH CHECK (weekly)
   □ Open Google Sheet log — check for any "error" status rows
   □ Open n8n → Executions → filter by "Error" — investigate any failures
   □ Check OpenAI usage dashboard — verify costs are within expected range

2. COMMON ISSUES
   Issue: Lead enrichment stopped working
   → Check: Is the HubSpot workflow still active? (Automation → Workflows)
   → Check: Is the n8n webhook URL still correct?
   → Check: Is the OpenAI API key still valid? (platform.openai.com/api-keys)

   Issue: Slack bot not responding
   → Check: Is the n8n workflow active?
   → Check: Is the Slack app still installed? (api.slack.com/apps)
   → Check: Does /ask show an error in Slack? → Check n8n execution log

   Issue: Ticket triage classifying wrong category
   → Review recent tickets in the log sheet
   → Adjust the system prompt categories or add examples
   → Test with 5 sample tickets after changes

3. UPDATING THE KNOWLEDGE BASE
   → Add new doc: Create a new entry in knowledge-base.json
   → Include: id, source_doc name, section, keywords, content
   → Test: Ask the bot a question the new doc should answer
   → Verify: Bot cites the new document correctly

4. MONTHLY COST CHECK
   → OpenAI: platform.openai.com/usage → current month → compare to estimate
   → n8n: Check execution count → compare to plan limits
   → Expected: ~$5-10/mo OpenAI, ~$24/mo n8n cloud

5. ESCALATION
   For issues you can't resolve: [Your name] — [your email]
   Response time: Within 24 hours on business days

Part 12 — Upwork Delivery & Career Positioning

Close the project, secure the review, and position yourself for the next one.

MILESTONE DELIVERY MESSAGE

Hi [Client name],

All three AI integrations are live and tested. Here's what's been delivered:

1. Lead Enrichment (live)
Every new HubSpot contact is automatically scored and enriched. I've processed your last 10 test leads — results are in the HubSpot custom properties (ai_lead_score, ai_summary) and the logging spreadsheet. Average processing time: 3 seconds per lead. API cost for 200 leads/month: ~$0.32.

2. Ticket Triage (live)
Tickets are classified and draft responses appear in #support-ai within 4 seconds. I've run 15 test tickets covering all 5 categories — classification accuracy is 93% (14/15 correct). The one miss was a billing question phrased as a bug report — I've tuned the prompt to handle that edge case.

3. Knowledge Bot (live)
Type /ask in any Slack channel to query your 50 internal docs. Tested with 20 questions — 18 correct answers with proper citations, 2 correctly said "I don't know." Zero hallucinations.

📎 Attached: Architecture diagram, runbook, prompt documentation, n8n workflow exports (JSON).
🎥 Loom walkthrough (18 min): [link]

Happy to do a 30-min handover call to walk your team through everything. After that, I'm available for the monthly maintenance retainer we discussed ($400/mo) — that covers prompt tuning, knowledge base updates, monitoring, and priority bug fixes.

GETTING THE 5-STAR REVIEW

The review request

After the client confirms everything is working (give them 3-5 days to test), send: "Great to hear it's working well! If you have a moment, a review on Upwork would really help — it's the main way new clients find me for this kind of work. No pressure at all."

Do not ask before they've confirmed satisfaction. Do not ask more than once. If they leave a 5-star review, thank them briefly. Your Upwork profile with verified AI integration reviews at $2,800 is worth more than any certification.

PORTFOLIO POSITIONING

After this project, your Upwork profile should highlight:

Skill TagEvidence
AI Integration"Connected OpenAI APIs to HubSpot CRM and Slack for a B2B SaaS — automated lead scoring, support triage, and internal Q&A"
n8n Workflow Automation"Built 3 production n8n workflows processing 2,000+ monthly events"
OpenAI API / LLM Integration"Production prompt engineering for classification, scoring, and document Q&A"
HubSpot CRM Automation"Custom properties, webhook integrations, automated lead enrichment"
PRICING LADDER FOR AI INTEGRATION WORK
Project TypeScopePrice RangeTimeline
Single AI featureOne workflow (e.g., lead scoring only)$800–$2,0003–5 days
Multi-use-case integration2–3 AI workflows + documentation$2,000–$5,0002–3 weeks
Full AI ops setupMultiple workflows + RAG + monitoring + training$5,000–$15,0004–8 weeks
Enterprise AI platformCustom AI system with vector search, fine-tuning, multi-tenant$15,000–$50,000+2–6 months
Monthly retainerMaintenance, prompt tuning, KB updates, monitoring$300–$1,500/moOngoing

The upsell path from this project

This $2,800 project opens three natural follow-ups:
1. Vector search upgrade ($1,500–$3,000): When the knowledge base grows past 200 docs, migrate from keyword search to embeddings + Pinecone/Qdrant. Same workflow architecture, better retrieval.
2. n8n self-hosting ($500–$1,000): Migrate from n8n cloud to their own Docker instance. Saves $24/mo and gives them full control.
3. Additional use cases ($1,000–$3,000 each): AI-generated onboarding emails, churn prediction, meeting note summarization. Every SaaS team has 5 more things they'd automate once they see the first three working.

— End of AI Integration Module —

Cost, Tools & Career Path

Everything you need to know before starting. Total investment, what you'll work with, and exactly how each module changes your Upwork positioning.

Total Cost Commitment

Every tool and service across all three modules. No hidden costs.

Tool / ServiceUsed InCostNotes
Shopify & E-Commerce
Shopify Partner AccountM2FreeUnlimited dev stores, no card required
Shopify Development StoreM2FreeFull store, no time limit
Shopify Basic Plan (fallback)M2$1–$3Only if FB app won't install on dev store. $1/mo for 3 months.
Meta & Advertising
Meta Business SuiteM1, M2FreeBusiness Manager, Pixel, Events Manager
Meta Pixel Helper (Chrome)M1, M2FreeBrowser extension for auditing
Google Tag ManagerM2FreeContainer for simulating GTM-based installs
Automation Platforms
n8n (self-hosted)M1, C1FreeDocker or Node.js install. Unlimited executions.
n8n Cloud (alternative)M1, C1Free trial14-day trial, no card. 2,500 executions/mo.
ZapierM1Free tier100 tasks/mo on free plan — enough for learning
ngrok / Cloudflare TunnelM1, C1FreeWebhook tunneling for self-hosted n8n
SaaS & CRM
TypeformM1Free tier10 responses/mo on free plan
CalendlyM1Free tier1 event type, unlimited bookings
Stripe (test mode)M1FreeTest mode with fake card numbers, no real charges
HubSpot CRMC1FreeFree CRM includes contacts, deals, API access
SlackC1FreeFree workspace for bot integration
Google SheetsM1, C1FreeLogging, cost tracking, data pipeline
AI / LLM APIs
OpenAI API (GPT-4.1-mini)C1~$1Pay-as-you-go. Entire lab costs ~$1 in tokens.
Anthropic API (Claude Haiku)C1~$1Pay-as-you-go. Knowledge bot use case.
Dev Tools (already on your computer)
Chrome DevToolsM1, M2FreeNetwork tab for inspecting HTTP requests
Incognito / Private BrowsingM1, M2FreeClean-cookie testing
Text editor (any)AllFreeVS Code, Sublime, Notepad++ — anything
Total course cost $2 – $5

The $2–$5 is LLM API credits for the AI module. If the Facebook app installs on your free dev store (it usually does), the rest is $0. No subscriptions, no monthly fees, no credit card holds.

Current Upwork Market — Where You're Entering

The automation engineering landscape on Upwork as of August 2026.

CategoryUpwork DemandEntry RateSenior RateSupply
n8n Experts5,129 client reviews$15–$25/hr$80–$150/hrGrowing, still undersupplied
API Integration23,514 client reviews$20–$35/hr$55–$85/hrLarge pool, competitive
AI Automation Engineers+178% YoY growth$35–$45/hr$100–$180+/hrDemand outpacing supply
Workflow Automation (Zapier/Make)8,838+ client reviews$10–$20/hr$40–$80/hrLargest pool, low barrier
The positioning advantage. Freelancers who combine n8n + API skills + AI integration earn 34% more per hour than those listing individual tools. The most common mistake on Upwork is positioning as "n8n expert" or "Zapier developer" — tool-based positioning traps you in the entry-rate pool. Each module in this course moves you toward outcome-based positioning: "I fix your broken tracking," "I build your conversion pipeline," "I integrate AI into your existing tools." Clients pay for results, not tool names.

What Each Module Produces & Your New Positioning

Each lab gives you a new deliverable skill and unlocks a higher-paying market tier.

Module 2 — Shopify Tracking Diagnostic $10–$30/hr

What you produce: A complete Shopify tracking audit — diagnostic matrix mapping all 5 installation points, identification of duplicate/conflicting Pixels, fix implementation, and a clean handover document the client can use after you leave.

Tools mastered: Shopify Partner + dev stores, Meta Pixel Helper, Chrome DevTools (Network tab), GTM container auditing, theme.liquid code inspection, Events Manager verification.

Upwork service you can sell: "Shopify Meta Pixel & CAPI Audit — find and fix duplicate tracking, inflated ROAS, and event deduplication failures."

Before

"I know some Shopify and Meta ads"

No portfolio, no deliverable template

Competing on price in general VA pool

After

Can walk into any Shopify store and produce a professional audit

Diagnostic matrix template ready to reuse

Positioned as a specialist, not a generalist

Module 1 — Meta CAPI Automation Lab $300–$400 fixed

What you produce: A complete B2B lead-gen tracking system built from scratch — Typeform forms, Calendly scheduling, Stripe payment webhooks, five custom conversion events sent to Meta via both Pixel and Conversions API, with n8n/Zapier automations handling the server-side pipeline.

Tools mastered: n8n (self-hosted + cloud), Zapier, Meta Conversions API, webhook architecture, SHA-256 hashing, event deduplication, Typeform, Calendly, Stripe test mode, Google Sheets as a data layer.

Upwork service you can sell: "Meta CAPI implementation — I build your full conversion tracking pipeline from form fill to purchase, with server-side events and automated deduplication."

Key unlock: You now know n8n and Zapier — the two most in-demand automation platforms on Upwork. This skill transfers to every automation job, not just tracking.

Before (after M2)

Can audit and fix existing systems

Hourly diagnostic work, $10–$30/hr

No automation or build experience

After

Can build complete systems from scratch

Fixed-price projects at $300–$400 each

n8n + Zapier skills open the entire automation market

Module C1 — AI Integration Laboratory $2,000–$5,000 fixed

What you produce: Three production AI integrations for a SaaS client — an AI lead enrichment pipeline (HubSpot + LLM scoring), an automated support ticket triage system (webhook + LLM classification + Slack), and an internal knowledge base Q&A bot (Slack command + document retrieval + cited answers). Complete with error handling, cost tracking, runbook, and client handover package.

Tools mastered: OpenAI API (GPT-4.1-mini), Claude API (Haiku), n8n AI workflows, HubSpot CRM API, Slack Bot API, webhook architecture, prompt engineering, structured JSON output, Google Sheets logging, production error handling.

Upwork service you can sell: "AI integration for SaaS — I connect LLMs to your existing CRM, support tools, and Slack with production-grade workflows, error handling, and documentation your team can maintain."

Key unlock: AI integration is the fastest-growing category on Upwork (+178% YoY). Freelancers who bridge the gap between an API key and a production system command the highest rates. You're no longer selling hours — you're selling fixed-price projects with retainers.

Before (after M1)

Can build automation workflows

$300–$400 fixed-price tracking projects

No AI or LLM experience

After

Can deliver $2,000–$5,000 AI integration projects

Retainer-eligible: $300–$500/mo ongoing maintenance

Top 5% positioning in fastest-growing Upwork category

Combined Earning Potential

ServicePricing ModelRateJobs/MonthMonthly Revenue
Shopify tracking auditsHourly$20–$30/hr2–3$400–$900
Meta CAPI buildsFixed price$300–$4002–3$600–$1,200
AI integrationsFixed + retainer$2,000–$5,0001$2,000–$5,000
Retainer clients (AI)Monthly$300–$5002–4$600–$2,000
Total (part-time freelancing)$3,600–$9,100/mo
The stack effect. Most Upwork freelancers list one skill. You walk in with three distinct services at three price points. Lower-tier work (audits) generates quick reviews and Job Success Score. Mid-tier work (CAPI builds) pays the bills. High-tier work (AI integrations) builds your reputation and attracts retainer clients. The audit work also feeds the pipeline — every broken Shopify store you fix is a client who may need a CAPI rebuild or an AI integration next.

Rate data sourced from Upwork hiring pages (Aug 2026), Ciphernutz n8n pricing guide, RemoteAITools market analysis, and Upwork's official hourly rate guide. Actual earnings depend on your profile, reviews, proposal quality, and market conditions.

Part-Time Study Plan

All three labs in recommended learning order, paced for busy professionals doing 1.5–2 hours on weekday evenings and 2–3 hours on weekends.

~60 hrs Total effort
12 wks Duration
5–6 hrs/wk Weekly pace
3 labs Modules
48 Layers
Why this order? Each module's skills feed the next. Start by diagnosing a broken tracking setup — you learn how tracking actually works before building anything. Then build a complete system from scratch, picking up n8n/Zapier automation along the way. Finally, apply those automation skills to the highest-value work: AI integrations.
Diagnose & Fix Build from Scratch Integrate AI
M2 — Shopify Tracking Diagnostic 11 parts · 12 layers · ~15 hrs

Start here — learn how tracking works by walking into a broken system, auditing it, and fixing it.

Wk 1 Client Brief & Shopify Architecture ~5 hrs
Mon P1P2 Read client brief, requirements analysis — your first client archetype 1.5 hr
Wed P3P4P5 Problem statement, client objectives, technical objectives 1.5 hr
Sat P6 Shopify's 5 tracking installation points, how duplicates happen, data flow 2 hrs
Wk 2 Audit & Fix Implementation ~5.5 hrs
Tue P7P8 Lab setup, begin systematic audit (Layers 1–4) 2 hrs
Thu P8 Remove duplicate tracking, reconcile data sources (Layers 5–8) 1.5 hr
Sun P8 Complete fix implementation, verification testing (Layers 9–12) 2 hrs
Wk 3 Deliverables, Handover & Final Exam ~4.5 hrs
Mon P9P10 Student deliverables checklist, client definition of done 1.5 hr
Wed P11 Final exam — produce full diagnostic report + clean handover doc 2 hrs
Sat Review module — solidify understanding of tracking fundamentals for M1 1 hr
You can deliver Shopify tracking audits at $10–$30/hr on Upwork

M1 — Meta CAPI Automation Lab 12 parts · 20 layers · ~25 hrs

Now build a complete tracking system from scratch — you already know how tracking works from M2.

Wk 4 Client Brief & Problem Framing ~5 hrs
Mon P1P2 Read client brief, requirements analysis — different archetype from M2 1.5 hr
Wed P3P4 Problem statement, client objectives, craft Upwork proposal 1.5 hr
Sat P5P6 Technical objectives, review client's requested architecture vs. reality 2 hrs
Wk 5 Architecture & Lab Setup ~6 hrs
Mon P7 Architecture review, correct the client's design, map real data flows 1.5 hr
Wed P8 Lab environment: set up Meta Pixel, Business Manager, test accounts 1.5 hr
Sat P9 Begin implementation: Typeform, Calendly, Pixel install (Layers 1–4) 3 hrs
Wk 6 Conversions API & Zapier/n8n Automations ~6.5 hrs
Tue P9 CAPI setup, Zap 1–2: Schedule + CallAttended events (Layers 5–8) 2 hrs
Thu P9 Zap 3–4: QualifiedLead + Purchase events, Google Sheet tracker (Layers 9–13) 1.5 hr
Sun P9 Meta Events Manager config, custom conversions, n8n alternatives (Layers 14–17) 3 hrs
Wk 7 Testing, Deliverables & Final Exam ~5.5 hrs
Mon P9 End-to-end verification, Event Match Quality, deduplication (Layers 18–20) 2 hrs
Wed P10P11 Student deliverables checklist, client definition of done 1.5 hr
Sat P12 Final exam — full architecture map + handover doc from raw brief 2 hrs
You can deliver $300–$400 Meta CAPI projects — and you now know n8n/Zapier

C1 — AI Integration Laboratory 12 parts · 16 layers · ~20 hrs

The capstone — apply your n8n skills from M1 to the highest-value automation work: LLM integrations.

Wk 8 Client Brief, Proposal & Architecture ~5 hrs
Mon P1P2 Read SaaS client brief, requirements analysis, API cost estimation 1.5 hr
Wed P3 Craft Upwork proposal — lead with cost data, name specific models 1 hr
Sat P4 System architecture, LLM selection matrix, n8n vs Make vs Zapier 2.5 hrs
Wk 9 LLM APIs & n8n AI Foundations ~5.5 hrs
Mon P5 OpenAI + Claude API first calls, prompt engineering rules (Layers 1–3) 2 hrs
Thu P6 n8n AI credentials, HTTP Request node for LLM calls (Layers 4–5) 1.5 hr
Sat P6 Webhook triggers for all 3 use cases, data routing (Layer 6) 2 hrs
Wk 10 Lead Enrichment Pipeline ~6 hrs
Tue P7 Build enrichment workflow: webhook → fetch → LLM → HubSpot (Layer 7) 2 hrs
Thu P7 Calibrate scoring with 10 test leads, handle edge cases (Layer 8) 1.5 hr
Sun P7 Add Google Sheets logging + cost tracking (Layer 9) 2.5 hrs
Deliverable 1 — AI lead enrichment pipeline is live and logged
Wk 11 Ticket Triage & Knowledge Bot ~5.5 hrs
Mon P8 Ticket classification + Slack output, doc retrieval (Layers 10–11) 2 hrs
Wed P8 AI draft responses, wire complete triage flow (Layer 12) 1.5 hr
Sun P9 Knowledge bot: doc ingestion, Slack /ask command, citations (Layers 13–14) 2 hrs
Deliverables 2 & 3 — triage system and knowledge bot both live
Wk 12 Harden, Document & Deliver ~3.5 hrs
Mon P9P10 "I don't know" testing, error handling for all 3 workflows (Layer 15) 1.5 hr
Wed P11 Handover package: runbook, prompt docs, architecture diagram (Layer 16) 1.5 hr
Fri P12 Delivery message, pricing ladder, upsell paths, portfolio positioning 0.5 hr
Course complete — ready for tracking audits, CAPI builds, and $2,800 AI projects
Flexible, not rigid. The days shown are suggestions — swap any session to a day that works. What matters is ~5–6 hours per week. Each week ends with real, working deliverables. If you miss a session, double up on the weekend. If you're faster, move to the next week early.
By week 12 you can pitch three distinct services: Shopify tracking audits ($10–$30/hr), Meta CAPI builds ($300–$400 fixed), and AI integrations ($2,000–$5,000 fixed + $300–$500/mo retainer). Combined earning potential: $4,000–$8,000/mo freelancing part-time.