Sanity Webhook to GitHub Actions: Fix 422 Error with event_type Parameter

6 replies
Last updated: Nov 30, 2025
Hey there, I’m trying to setup Sanity webhook API to trigger Github actions dispatch workflow. However I’m running into below issue. I already had a test via Curl before setting up sanity webhook. Can anyone suggest how this can be achieved?
{
id: "atm-xxxxx",
projectId: "xxx",
inProgress: false,
duration: 226,
createdAt: "2022-02-xxxxx",
updatedAt: "2022-02-xxxx",
messageId: "msg-xxxxxxxxxx",
hookId: "xxxxxxxxx",
isFailure: true,
failureReason: "other",
resultCode: 422,
resultBody: "{"message":"Invalid request.\n\n\"_createdAt\", \"_id\", \"_rev\", \"_type\", \"_updatedAt\", \"header\", \"slug\", \"title\" are not permitted keys.\n\"event_type\" wasn't supplied.","documentation_url":"<https://docs.github.com/rest/reference/repos#create-a-repository-dispatch-event>"}"
}
AI Update

I can see the issue! GitHub's repository dispatch API requires a specific payload structure, but Sanity webhooks by default send the entire document with all its Sanity-specific fields like _id, _type, _createdAt, etc. GitHub is rejecting this because it only expects event_type (required) and optionally client_payload.

Here's how to fix this:

Configure Your Sanity Webhook with a Custom Projection

When setting up your webhook in Sanity (either via the Studio or API), you need to use a projection to transform the payload into the format GitHub expects:

  1. Go to your Sanity project's webhook settings (Manage → API → Webhooks)
  2. Create or edit your webhook with these settings:
    • URL: https://api.github.com/repos/{owner}/{repo}/dispatches
    • Dataset: Choose your dataset
    • HTTP method: POST
    • HTTP Headers:
      • Authorization: Bearer YOUR_GITHUB_PAT (Personal Access Token with repo scope)
      • Accept: application/vnd.github.v3+json
    • Projection: This is the key part - use something like:
{
  "event_type": "sanity-webhook",
  "client_payload": {
    "documentId": _id,
    "documentType": _type,
    "slug": slug.current,
    "title": title
  }
}

The projection uses GROQ syntax to reshape your document data into the exact structure GitHub expects. The event_type field is required by GitHub, and client_payload can contain whatever custom data you want to pass to your GitHub Actions workflow.

In Your GitHub Actions Workflow

You can then access this data in your workflow:

on:
  repository_dispatch:
    types: [sanity-webhook]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Use payload data
        run: |
          echo "Document ID: ${{ github.event.client_payload.documentId }}"
          echo "Title: ${{ github.event.client_payload.title }}"

If you need more control over the payload transformation or want to add conditional logic, consider using Sanity Functions instead of direct webhooks. Functions are the modern, recommended approach for reacting to content changes - they're serverless compute modules that run within Sanity with native integration, automatic scaling, and better security.

Functions can listen to document changes and make custom HTTP requests with complete control over the payload format:

// sanity.blueprint.ts
import {defineBlueprint} from 'sanity'

export default defineBlueprint({
  functions: [
    {
      name: 'trigger-github-action',
      runtime: 'nodejs22',
      handler: './functions/triggerGithub',
      events: {
        documentChanges: {
          filter: '_type == "post"'
        }
      }
    }
  ]
})

Then in your function handler, you have full control to format the GitHub API request exactly as needed, handle errors, add retry logic, etc.

The webhook projection approach should solve your immediate issue though - just make sure your GitHub PAT has the correct repo scope and the projection transforms your document into the event_type + client_payload structure GitHub expects!

Show original thread
6 replies

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?