Invalid Percent Encoding in UTM Parameters: What Breaks and Why

UTMGuard Team
8 min readURL Syntax

"Our campaign URLs looked perfect. But Google Analytics showed garbled data like 'summer%2Gsale' and 'email%XYnewsletter.' Turns out our URL encoding tool was broken, generating invalid percent codes that browsers couldn't decode."

James Wilson, a marketing technologist at a SaaS company, spent two days debugging why 40% of their campaign traffic was showing corrupted parameter values. The culprit: malformed percent encoding.

What is Percent Encoding?

Percent encoding (also called URL encoding) is the method for representing special characters in URLs using the % symbol followed by two hexadecimal digits.

Valid percent encoding:

Space → %20
! → %21
" → %22
# → %23
$ → %24
% → %25

The rule: % followed by exactly two hexadecimal digits (0-9, A-F)

What Makes Percent Encoding Invalid?

Invalid Pattern 1: Non-Hexadecimal Characters

Valid hex digits: 0 1 2 3 4 5 6 7 8 9 A B C D E F

Invalid examples:

❌ %2G (G is not hex)
❌ %ZZ (Z is not hex)
❌ %XY (X and Y are not hex)
❌ %1K (K is not hex)
❌ %M5 (M is not hex)

What happens: Browser cannot decode, shows literal characters or throws error

🚨 Not sure what's breaking your tracking?

Run a free 60-second audit to check all 40+ ways UTM tracking can fail.

Scan Your Campaigns Free

✓ No credit card ✓ See results instantly

Invalid Pattern 2: Incomplete Encoding

Missing digits:

❌ %2 (only one digit instead of two)
❌ % (percent sign alone)
❌ %A (only one digit)

What happens: Browser treats as malformed, unpredictable behavior

Invalid Pattern 3: Truncated Sequences

Cut-off encoding at end of value:

❌ utm_campaign=summer-sale%2
   (Encoding started but not finished)

❌ utm_source=email%
   (Percent with no following digits)

Invalid Pattern 4: Wrong Case for Reserved Percent

Lowercase hex digits in some contexts:

⚠️ %2f vs %2F
Generally works but not technically valid per RFC 3986
Should use uppercase: %2F

Real-World Failure Examples

Case 1: The Malformed Comma Encoding

Intended campaign: "2024, Summer Sale"

Someone tried to encode the comma:

❌ WRONG:
utm_campaign=2024%2G summer sale
(G is not hexadecimal)

Browser sees: "2024%2G summer sale" (literal characters, not decoded)

Correct:

✅ RIGHT:
utm_campaign=2024-summer-sale
(No encoding needed, use hyphens)

Or if you must encode:
utm_campaign=2024%2C%20summer%20sale
(Comma = %2C, Space = %20, both valid hex)

Case 2: The Truncated Encoding Error

Generated URL (automated tool bug):

❌ BROKEN:
https://example.com?utm_source=email&utm_campaign=spring%2

What happened:
- Tool started encoding (%)
- Crashed or truncated before finishing
- Left incomplete %2 sequence

Browser behavior:

  • Chrome: Shows "spring%2" literally
  • Firefox: May show "spring%" or error
  • Safari: Unpredictable
  • Analytics: Records gibberish

Impact:

  • 2,847 sessions tracked with malformed campaign name
  • Unable to attribute $12,600 in revenue
  • Campaign appears as multiple different names in reports

Case 3: The Copy-Paste Encoding Corruption

Original URL:

utm_campaign=black-friday-sale

After copy-paste through multiple tools:

❌ CORRUPTED:
utm_campaign=black%XYfriday%ZZsale

Why it happened:
1. Pasted into Word document (auto-formatting applied)
2. Copied from Word to email (encoding changed)
3. Email client "helped" by trying to encode
4. Result: Invalid percent sequences

😰 Is this your only tracking issue?

This is just 1 of 40+ ways UTM tracking breaks. Most marketing teams have 8-12 critical issues they don't know about.

• 94% of sites have UTM errors

• Average: $8,400/month in wasted ad spend

• Fix time: 15 minutes with our report

✓ Connects directly to GA4 (read-only, secure)

✓ Scans 90 days of data in 2 minutes

✓ Prioritizes issues by revenue impact

✓ Shows exact sessions affected

Get Your Free Audit Report

How to Detect Invalid Percent Encoding

Visual Inspection

Red flags to look for:

❌ %2G, %XY, %ZZ (non-hex characters)
❌ Trailing % at end of value
❌ %A, %2, %F (single digit after %)
❌ Multiple %% characters
❌ % followed by space

JavaScript Detection

function detectInvalidPercentEncoding(url) {
  const issues = [];
 
  try {
    const urlObj = new URL(url);
    const params = urlObj.searchParams;
 
    ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
      const value = params.get(param);
      if (!value) return;
 
      // Pattern 1: % followed by non-hex characters
      const invalidHex = value.match(/%[^0-9A-Fa-f]/g);
      if (invalidHex) {
        issues.push({
          parameter: param,
          issue: 'Invalid hex characters after %',
          examples: invalidHex,
          severity: 'CRITICAL'
        });
      }
 
      // Pattern 2: % followed by only one digit
      const incompleteEncoding = value.match(/%[0-9A-Fa-f](?![0-9A-Fa-f])/g);
      if (incompleteEncoding) {
        issues.push({
          parameter: param,
          issue: 'Incomplete percent encoding (single digit)',
          examples: incompleteEncoding,
          severity: 'CRITICAL'
        });
      }
 
      // Pattern 3: % at end of string
      if (value.endsWith('%')) {
        issues.push({
          parameter: param,
          issue: 'Percent sign at end of value',
          severity: 'CRITICAL'
        });
      }
 
      // Pattern 4: % followed by space
      if (value.includes('% ')) {
        issues.push({
          parameter: param,
          issue: 'Percent followed by space',
          severity: 'CRITICAL'
        });
      }
 
      // Pattern 5: Multiple consecutive %
      if (value.includes('%%')) {
        issues.push({
          parameter: param,
          issue: 'Multiple consecutive percent signs',
          severity: 'WARNING'
        });
      }
    });
 
  } catch (e) {
    issues.push({
      parameter: 'URL',
      issue: 'Malformed URL',
      error: e.message,
      severity: 'CRITICAL'
    });
  }
 
  return {
    valid: issues.length === 0,
    issues: issues
  };
}
 
// Usage
const testURLs = [
  'https://example.com?utm_campaign=sale%2Gspecial',
  'https://example.com?utm_source=email%',
  'https://example.com?utm_medium=social%2',
  'https://example.com?utm_campaign=summer-sale'  // This one is valid
];
 
testURLs.forEach(url => {
  const result = detectInvalidPercentEncoding(url);
  console.log(`URL: ${"{"}{"{"}url{"}"}{"}"}}`);
  console.log(`Valid: ${result.valid}`);
  if (!result.valid) {
    console.log('Issues:', result.issues);
  }
  console.log('---');
});

Browser Console Test

Quick manual check:

// Paste this in browser console with your URL
const testUrl = 'https://example.com?utm_campaign=sale%2Gspecial';
 
try {
  const urlObj = new URL(testUrl);
  const campaign = urlObj.searchParams.get('utm_campaign');
  console.log('Campaign value:', campaign);
 
  // Try to decode explicitly
  try {
    const decoded = decodeURIComponent(campaign);
    console.log('Decoded:', decoded);
  } catch (e) {
    console.error('Decoding failed:', e.message);
    console.log('⚠️ INVALID PERCENT ENCODING DETECTED');
  }
} catch (e) {
  console.error('Invalid URL:', e.message);
}

Valid Percent Encoding Reference

Common Characters Properly Encoded

CharacterNameHex ValuePercent Encoded
SpaceSpace20%20
!Exclamation21%21
"Quote22%22
#Hash23%23
$Dollar24%24
%Percent25%25
&Ampersand26%26
'Apostrophe27%27
(Left Paren28%28
)Right Paren29%29
*Asterisk2A%2A
+Plus2B%2B
,Comma2C%2C
/Slash2F%2F
:Colon3A%3A
;Semicolon3B%3B
=Equals3D%3D
?Question3F%3F
@At40%40

Hexadecimal Reference

Valid hex digits (0-15):

0 1 2 3 4 5 6 7 8 9 A B C D E F
(Also lowercase: a b c d e f, though uppercase preferred)

Invalid (not hex):

G H I J K L M N O P Q R S T U V W X Y Z
(Any letter beyond F)

How to Fix Invalid Percent Encoding

Fix 1: Don't Encode at All

Best solution: Avoid characters that need encoding

❌ NEEDS ENCODING:
utm_campaign=Save 50%! Limited Time

✅ NO ENCODING NEEDED:
utm_campaign=save-50-percent-limited-time

Fix 2: Use Proper Encoding Function

JavaScript:

function properlyEncodeUTM(value) {
  // First, clean to safe characters (best approach)
  const cleaned = value
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/%/g, '-percent')
    .replace(/&/g, '-and-')
    .replace(/[^a-z0-9-_]/g, '');
 
  return cleaned;
}
 
// If you MUST preserve original and encode:
function encodeUTMValue(value) {
  return encodeURIComponent(value);
}
 
// Examples
console.log(properlyEncodeUTM('Save 50%! Limited Time'));
// Output: save-50-percent-limited-time
 
console.log(encodeURIComponent('Save 50%'));
// Output: Save%2050%25 (valid, but avoid this approach)

PHP:

<?php
// Don't use raw urlencode on entire UTM value
// Instead, clean the value first
 
function cleanUTMValue($value) {
    $cleaned = strtolower($value);
    $cleaned = str_replace(' ', '-', $cleaned);
    $cleaned = str_replace('%', '-percent', $cleaned);
    $cleaned = preg_replace('/[^a-z0-9-_]/', '', $cleaned);
    return $cleaned;
}
 
echo cleanUTMValue('Save 50%! Limited Time');
// Output: save-50-percent-limited-time
?>

Python:

import re
from urllib.parse import quote
 
def clean_utm_value(value):
    """Best approach: clean to safe characters"""
    cleaned = value.lower()
    cleaned = cleaned.replace(' ', '-')
    cleaned = cleaned.replace('%', '-percent')
    cleaned = re.sub(r'[^a-z0-9-_]', '', cleaned)
    return cleaned
 
# If you must encode:
def encode_utm_value(value):
    """Use only if absolutely necessary"""
    return quote(value, safe='')
 
print(clean_utm_value('Save 50%! Limited Time'))
# Output: save-50-percent-limited-time
 
print(encode_utm_value('Save 50%'))
# Output: Save%2050%25 (valid but not recommended)

Fix 3: Validate Before Use

function validateAndFixPercentEncoding(url) {
  try {
    const urlObj = new URL(url);
    let fixed = false;
 
    ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
      const value = urlObj.searchParams.get(param);
      if (!value) return;
 
      // Check for invalid encoding
      const hasInvalidEncoding =
        /%[^0-9A-Fa-f]/.test(value) ||  // Non-hex after %
        /%[0-9A-Fa-f](?![0-9A-Fa-f])/.test(value) ||  // Single digit after %
        value.endsWith('%');  // % at end
 
      if (hasInvalidEncoding) {
        // Clean the value
        const cleaned = value
          .replace(/%[^0-9A-Fa-f]{0,2}/g, '')  // Remove invalid sequences
          .replace(/%$/, '')  // Remove trailing %
          .toLowerCase()
          .replace(/\s+/g, '-')
          .replace(/[^a-z0-9-_]/g, '');
 
        urlObj.searchParams.set(param, cleaned);
        fixed = true;
      }
    });
 
    return {
      original: url,
      fixed: urlObj.toString(),
      wasFixed: fixed
    };
 
  } catch (e) {
    return {
      original: url,
      error: e.message
    };
  }
}
 
// Usage
const problematic = 'https://example.com?utm_campaign=sale%2Gspecial&utm_source=email%';
const result = validateAndFixPercentEncoding(problematic);
 
console.log('Original:', result.original);
console.log('Fixed:', result.fixed);
console.log('Was fixed:', result.wasFixed);

Prevention Strategies

1. Never Manual Encode

Don't try to encode by hand:

❌ MANUAL ENCODING (error-prone):
Thinking: "Space is %20, so Summer Sale = Summer%20Sale"
Result: Often leads to %2G, %XY type errors

✅ USE FUNCTIONS:
encodeURIComponent('Summer Sale')
Result: Always correct

2. Use Clean Values Only

Best practice: avoid encoding entirely

// URL Builder that prevents encoding issues
function buildCleanUTMUrl(baseUrl, params) {
  const url = new URL(baseUrl);
 
  Object.keys(params).forEach(key => {
    // Clean value to not need encoding
    const clean = params[key]
      .toLowerCase()
      .replace(/\s+/g, '-')
      .replace(/%/g, '-percent')
      .replace(/&/g, '-and-')
      .replace(/[^a-z0-9-_]/g, '');
 
    url.searchParams.set(key, clean);
  });
 
  return url.toString();
}
 
// Usage
const trackedUrl = buildCleanUTMUrl('https://example.com', {
  utm_source: 'Email Newsletter',
  utm_campaign: 'Save 50%! Limited Time'
});
 
console.log(trackedUrl);
// https://example.com?utm_source=email-newsletter&utm_campaign=save-50-percent-limited-time
// No percent encoding needed!

3. Validate After Any Tool

Test all URLs from automation:

// After generating URL from any tool
function validateNoInvalidEncoding(url) {
  const pattern = /%(?:[^0-9A-Fa-f]|[0-9A-Fa-f](?![0-9A-Fa-f])|$)/;
 
  if (pattern.test(url)) {
    throw new Error('Invalid percent encoding detected in URL: ' + url);
  }
 
  return true;
}
 
// Use in your workflow
const generated = urlBuilderTool.generate(params);
validateNoInvalidEncoding(generated);  // Throws if invalid
deployurl(generated);

4. Regular Audits

Check for encoding issues monthly:

-- Google Analytics 4 BigQuery query
SELECT
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'campaign_name') as campaign,
  COUNT(*) as sessions
FROM `project.dataset.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
  AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
  AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'campaign_name') LIKE '%\%%'
GROUP BY campaign
ORDER BY sessions DESC

✅ Fixed this issue? Great! Now check the other 39...

You just fixed one tracking issue. But are your Google Ads doubling sessions? Is Facebook attribution broken? Are internal links overwriting campaigns?

Connects to GA4 (read-only, OAuth secured)

Scans 90 days of traffic in 2 minutes

Prioritizes by revenue impact

Free forever for monthly audits

Run Complete UTM Audit (Free Forever)

Join 2,847 marketers fixing their tracking daily

FAQ

Q: What if my URL has %20 for spaces? Is that invalid?

A: %20 is valid (2 and 0 are hex digits). But it's better to avoid needing encoding by using hyphens instead of spaces.

Q: Can I fix invalid encoding by double-encoding?

A: No! That makes it worse. Fix by removing the encoding and using clean characters only.

Q: Why does %2G appear in my URLs?

A: Likely a broken encoding tool that's using the wrong character set or has a bug. Stop using that tool and switch to proper URL builders.

Q: Will browsers automatically fix invalid encoding?

A: No. Different browsers handle it differently, creating inconsistent data. Some show literal characters, others throw errors.

Q: How do I know if encoding is uppercase or lowercase hex?

A: Both work technically, but RFC 3986 recommends uppercase (%2F not %2f). Most importantly: ensure both characters after % are valid hex (0-9, A-F).

Q: Can analytics tools decode invalid percent encoding?

A: No. If the encoding is malformed, analytics receives and stores the malformed value. Garbage in, garbage out.

Q: What's the most common cause of invalid encoding?

A: Broken URL builder tools, manual encoding attempts, and copy-paste corruption through multiple systems.

Q: Should I decode, clean, then re-encode URLs?

A: No. Decode, clean to safe characters (a-z, 0-9, hyphens), and don't re-encode. This prevents all encoding issues.


Stop invalid percent encoding from breaking your campaign tracking. UTMGuard automatically detects malformed encoding patterns and validates all your UTM parameters. Start your free audit today.