Skip to main content

ATS integration

Feed matched candidates into your existing ATS

InTransparency doesn't replace your ATS — it feeds it. When a high-confidence match fires, we send a signed webhook; a 30-line relay turns that into a Greenhouse prospect, a Lever opportunity, or a Workday requisition candidate.

The pattern

InTransparency fires a webhook → your tiny relay verifies the signature → your relay calls your ATS's candidates endpoint. No custom middleware, no polling, no scheduled sync. The relay runs anywhere (Vercel, Cloudflare Workers, AWS Lambda, or a FastAPI container).

  1. Create an agent key and register a webhook

    InTransparency API

    # Step 1: Create an InTransparency agent key with webhook scope
    curl -X POST https://www.in-transparency.com/api/agents/keys \
      -H "Cookie: <your session>" \
      -H "Content-Type: application/json" \
      -d '{"name":"ATS-relay","scopes":["agent:webhook"]}'
    # Response: { "key": "ita_...", ... } — save it once; never shown again
    
    # Step 2: Register your webhook endpoint with InTransparency
    curl -X POST https://www.in-transparency.com/api/agents/webhooks \
      -H "Authorization: Bearer ita_..." \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-relay.example.com/webhooks/intransparency",
        "events": ["match.created", "credential.issued"]
      }'
    # Response includes { "secret": "..." } — store as INTRANSPARENCY_WEBHOOK_SECRET
  2. Greenhouse relay (Node / TypeScript)

    Greenhouse Harvest API

    // Deno / Node webhook relay: InTransparency → Greenhouse Harvest API
    // Env: GREENHOUSE_API_KEY, GREENHOUSE_USER_ID, GREENHOUSE_JOB_ID,
    //      INTRANSPARENCY_WEBHOOK_SECRET
    import crypto from 'node:crypto'
    
    export default async function relay(req: Request): Promise<Response> {
      // 1. Verify the signature (constant-time compare)
      const body = await req.text()
      const sig = req.headers.get('X-InTransparency-Signature') || ''
      const expected = 'sha256=' + crypto
        .createHmac('sha256', process.env.INTRANSPARENCY_WEBHOOK_SECRET!)
        .update(body).digest('hex')
      const ok = sig.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
      if (!ok) return new Response('bad signature', { status: 401 })
    
      const { event, data } = JSON.parse(body)
      if (event !== 'match.created' || data.matchScore < 70) {
        return new Response('ignored', { status: 200 })
      }
    
      // 2. Create a prospect in Greenhouse. Before the student accepts contact,
      // the webhook carries no email or surname — only a first name with an
      // initial ("Giulia M."), a stable pseudonym and the evidence link. The
      // recruiter opens the link, writes to the student on InTransparency, and
      // the full contact arrives once the student says yes.
      const [firstName, ...rest] = String(data.candidateName).split(' ')
      await fetch('https://harvest.greenhouse.io/v1/candidates', {
        method: 'POST',
        headers: {
          'Authorization': 'Basic ' + Buffer.from(process.env.GREENHOUSE_API_KEY + ':').toString('base64'),
          'Content-Type': 'application/json',
          'On-Behalf-Of': process.env.GREENHOUSE_USER_ID!,
        },
        body: JSON.stringify({
          first_name: firstName,
          last_name: rest.join(' ') || data.candidatePseudonym,
          applications: [{ job_id: Number(process.env.GREENHOUSE_JOB_ID) }],
          website_addresses: [{ value: data.url, type: 'other' }],
          tags: ['InTransparency', data.decisionLabel],
          custom_fields: [
            { name_key: 'intransparency_match_id', value: data.matchId },
            { name_key: 'intransparency_match_score', value: data.matchScore },
          ],
        }),
      })
    
      return new Response('ok', { status: 200 })
    }

Other ATS platforms

  • Workday Recruiting

    Use the Recruiting Staffing Web Service (SOAP) — add a Candidate with Add_Candidate operation. Same relay pattern, different payload.

  • SmartRecruiters

    POST to /candidates with jobId and status=LEAD. Custom fields map cleanly.

  • Recruitee

    POST to /candidates with placements=[{offer_id}]. Attach evidence URL as note.

  • Teamtailor

    First-party push is live in beta — add a TeamTailor API key in recruiter settings. Building your own instead: POST to /v1/candidates with included job-applications; sourced candidates appear in the pipeline as leads.

  • Personio

    Use the Recruiting API — POST /v1/recruiting/applications. Italian HR teams already familiar with Personio can wire this up in under an hour.

  • Ashby

    First-party push is live in beta — add an Ashby API key in recruiter settings and skip the relay. Building your own instead: POST to /candidate.create, then /candidate.addApplication.