How an Indeed XML Feed and Google for Jobs JSON-LD Actually Work
The two main distribution channels
When you post a job on Indeed or Google for Jobs, you have two options:
- Post directly — log in to Indeed.com, fill out a form, click publish
- Syndicate via feed — provide your own job listing in a machine-readable format (XML or JSON-LD); the job board pulls from your feed and indexes it
Direct posting is easy but limited. Syndication is a bit more technical but gives you precise control: you can update a job's status, salary, or requirements in your system, and it reflects on Indeed within hours without re-entering data.
Most serious hiring teams use feeds.
The Indeed XML format
Indeed accepts a proprietary XML feed. Here's the structure:
<?xml version="1.0" encoding="UTF-8"?>
<source>
<publisher>Your Company Name</publisher>
<publisherurl>https://yourcompany.com</publisherurl>
<lastBuildDate>2026-05-21T10:00:00Z</lastBuildDate>
<job>
<title>Senior Backend Engineer</title>
<date>2026-05-21T08:00:00Z</date>
<repourl>https://yourcompany.com/jobs/senior-backend-engineer</repourl>
<description><![CDATA[
<p>We are hiring a senior backend engineer...</p>
<ul>
<li>5+ years Python/Go experience</li>
</ul>
]]></description>
<salary>120000</salary>
<hiringOrganization>Your Company</hiringOrganization>
<jobLocation>
<countryName>US</countryName>
<stateName>California</stateName>
<cityName>San Francisco</cityName>
</jobLocation>
<employmentType>FULL_TIME</employmentType>
<jobCategory>ENGINEER</jobCategory>
</job>
</source>
Key elements:
<title>: Job title (required)<description>: Full job description. Wrap in<![CDATA[...]]>if it contains HTML or special characters. Don't put HTML inside the CDATA — keep it clean text or add the HTML markup.<salary>: Base salary in USD. Some regions accept salary ranges, but Indeed primarily indexes single values.<jobLocation>: City, state, country. If remote, either omit or set city to "Remote."<date>: When the job was posted (ISO 8601 format). Indeed uses this to prioritize newer postings.<repourl>: Link back to your careers page for that specific job. When someone clicks "Apply," they land here.<employmentType>: FULL_TIME, PART_TIME, CONTRACT, TEMPORARY
Google for Jobs: JSON-LD
Google has a different format called JobPosting structured data in JSON-LD. This is embedded in your HTML as a <script type="application/ld+json"> tag.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "JobPosting",
"title": "Senior Backend Engineer",
"description": "We are hiring a senior backend engineer...",
"identifier": {
"@type": "PropertyValue",
"name": "Your Company",
"value": "job_123"
},
"datePosted": "2026-05-21",
"hiringOrganization": {
"@type": "Organization",
"name": "Your Company",
"sameAs": "https://yourcompany.com",
"logo": "https://yourcompany.com/logo.png"
},
"jobLocation": {
"@type": "Place",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main St",
"addressLocality": "San Francisco",
"addressRegion": "CA",
"postalCode": "94105",
"addressCountry": "US"
}
},
"baseSalary": {
"@type": "PriceSpecification",
"currency": "USD",
"minValue": 120000,
"maxValue": 160000,
"unitText": "YEAR"
},
"employmentType": "FULL_TIME",
"validThrough": "2026-06-21"
}
</script>
Critical fields for Google to index:
datePosted: Must be present and recent. Google de-ranks old postings.hiringOrganization: Company name and (optionally) logo. Used for the employer brand snippet.baseSalary: Salary range (min/max). Optional but improves CTR.validThrough: Job closing date. Google removes postings after this date.jobLocation: Full postal address or remote.
CDATA pitfalls and special characters
In XML, special characters like <, >, and & break the feed. Wrap your description in <![CDATA[...]]>:
<description><![CDATA[
We are looking for a developer with 5+ years experience.
Experience with: Java, Spring Boot, Docker.
]]></description>
Inside CDATA, you can include plain HTML:
<description><![CDATA[
<p>We are looking for a developer with 5+ years experience.</p>
<ul>
<li>Java 8+</li>
<li>Spring Boot</li>
<li>Docker</li>
</ul>
]]></description>
Without CDATA, the <p> and <li> tags break the XML parser. Indeed's feed validator will reject the entire feed.
Salary encoding
Indeed XML: Single salary value only. If you have a range, use the midpoint or the low end.
<salary>140000</salary>
Google for Jobs: Supports ranges:
"baseSalary": {
"@type": "PriceSpecification",
"currency": "USD",
"minValue": 120000,
"maxValue": 160000,
"unitText": "YEAR"
}
If your job is hourly, use:
"baseSalary": {
"@type": "PriceSpecification",
"currency": "USD",
"minValue": 45,
"maxValue": 65,
"unitText": "HOUR"
}
Update frequency and refreshes
Once you've set up a feed URL, Indeed crawls it periodically (usually daily, sometimes more frequently if jobs change often). When a job closes, remove it from the feed or set a closing date. Indeed will automatically de-index it.
Google for Jobs crawls the HTML pages where you embed JobPosting JSON-LD. Every time a candidate visits your job page, Google's crawler indexes it. If you remove the JSON-LD markup, the job disappears from Google for Jobs within days.
Validation and debugging
For Indeed XML:
- Use Indeed's Feed Validator to test your feed before connecting it
- Common errors: malformed CDATA, missing required fields, invalid date format
For Google for Jobs:
- Use Google's Structured Data Testing Tool or the newer Rich Results Test
- Check for missing datePosted, invalid date formats, or CDATA-like string encoding issues
How ClarityHire generates feeds
ClarityHire auto-generates both an Indeed XML feed at /api/jobs/feed.xml and a Google for Jobs JSON-LD template that you embed on your job pages at /careers/jobs/[job-id].
When you update a job (title, salary, description), the feeds refresh automatically. Closed jobs are removed from feeds within 2 hours. The system respects your job's visibility flag: draft jobs don't appear in feeds.
TL;DR
Indeed expects XML with CDATA-wrapped descriptions, salary, and location. Google for Jobs expects JSON-LD markup on your HTML with datePosted, validThrough, and salary ranges. Both use ISO date formats (YYYY-MM-DD or ISO 8601 timestamps). CDATA is your friend when wrapping HTML descriptions in XML. Validate before deploying. Auto-remove closed jobs from feeds. Test with Google's Rich Results Test and Indeed's validator.
Once set up, your jobs distribute to Indeed and Google without re-entry. The setup takes a day; the ongoing leverage is immense.