Particles UIDOCSDESIGN DECISION INFRASTRUCTUREStudioCLIMCP
Getting startedStudioTokensToken architectureThemesChanges & governanceBranches & reviewsDesign documentationFigma pluginCLIConnect AI agentsWebhooks & CI/CDSecurityPrivacyTerms
Docs / Reference / Webhooks & CI/CD

Webhooks & CI/CD

When a designer publishes a release, Particles can push the regenerated token files straight into your codebase as a pull request. Webhooks fire a signed event; your CI pulls the immutable release snapshot and opens the PR. Webhooks and webhook secrets are a Team plan and above feature.

The pipeline#

terminal
Designer publishes release 1.4.0
        │
        ▼  release.published  (HMAC-signed, retried)
   your CI / webhook receiver
        │  GET /v1/projects/:id/releases/1.4.0/export?format=style-dictionary
        ▼  (the frozen release snapshot — deterministic)
   regenerate token files → open a PR in your repo

Events#

A webhook subscribes to one or more event types. release.published is the one that drives CI/CD; the others power notifications and custom automations.

EventFires when
release.publishedA semver release snapshot is published — drives token-file CI/CD.
token.created / token.updated / token.deletedA token changes on a branch.
token_request.opened / approved / merged / rejectedA Token Request changes state.
branch.created / branch.mergedA branch is created or merged.

Create a webhook#

In Studio, go to Settings → Webhooks, pick a project, enter your receiver URL, and select the events. Each webhook gets a signing secret used to verify deliveries. You can Test a webhook, pause it, and inspect its recent deliveries (status, response code, attempt) from the same screen.

Delivery & payload#

Deliveries are POST requests with these headers:

HeaderValue
X-Particles-EventThe event type, e.g. release.published
X-Particles-Signaturesha256=<hmac> — HMAC-SHA256 over `timestamp + "." + body`
X-Particles-TimestampUnix seconds — reject if too far from now (replay protection)
X-Particles-DeliveryUnique delivery id — use as an idempotency key
release.published payload
{
  "event": "release.published",
  "projectId": "018fde70-…",
  "actor": "018fde70-…",
  "timestamp": "2026-06-11T10:00:00.000Z",
  "data": {
    "releaseId": "018fde70-…",
    "version": "1.4.0",
    "branch": "main",
    "notes": "Refresh brand palette",
    "tokenCount": 142,
    "exports": {
      "tailwind-v4":      ".../v1/projects/018fde70-…/releases/1.4.0/export?format=tailwind-v4",
      "style-dictionary": ".../v1/projects/018fde70-…/releases/1.4.0/export?format=style-dictionary",
      "dtcg":             ".../v1/projects/018fde70-…/releases/1.4.0/export?format=dtcg"
    }
  }
}

Verify the signature before trusting a delivery:

verify.ts
import { createHmac } from 'node:crypto'

function verify(secret: string, timestamp: string, rawBody: string, signature: string) {
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex')
  // constant-time compare in production
  return expected === signature
}
i

Deliveries that fail (timeout, 5xx, or 429) are retried automatically with backoff (≈30s → 2m → 10m → 30m). A 4xx other than 429 is treated as a permanent rejection and not retried. You can also redeliver any past attempt from the delivery log.

Webhook secrets (the CI credential)#

CI needs to call back into Particles to pull the release export. It uses a webhook secret — the only machine credential. It is deliberately narrow:

PropertyBehaviour
Read-onlyAuthenticates GET read routes only — it can never modify tokens, branches, or releases.
Project-scopedLimited to an allowlist of projects (or all projects). A read for a project outside the list is rejected.
Revocable / deletableRevoke stops it immediately (kept for audit); delete removes it entirely.
Shown onceThe plaintext whsec_… is displayed only at creation time — store it in your CI secrets.

Create one in Settings → Webhook Secrets (org admin), choose its project scope, and copy the whsec_… value once. Present it as a bearer token:

terminal
curl -H "Authorization: Bearer $PARTICLES_WEBHOOK_SECRET" \
  "https://api.particles-ui.com/v1/projects/$PROJECT_ID/releases/1.4.0/export?format=style-dictionary"

Release-pinned export#

GET /v1/projects/:id/releases/:version/export?format=<format> returns the token files for an immutable release snapshot, so two CI runs of the same version produce byte-identical output. Formats: tailwind-v4, css, scss, style-dictionary, dtcg, json, ts, js.

response
{
  "format": "style-dictionary",
  "version": "1.4.0",
  "files": [{ "path": "tokens.json", "contents": "{ … }" }]
}

Export format & output path#

The export format, target directory, and filename are not part of the webhook or the secret — they live in the consuming repo's particles-ui.json (created by particles init), so each repo owns its own output. The webhook only triggers the run.

particles-ui.json
{
  "projectId": "018fde70-0000-7000-8000-000000000001",
  "format": "tailwind-v4",
  "outputDestination": "src/styles/tokens.css"
}
FieldControls
formatExport type — tailwind-v4, css, scss, style-dictionary, dtcg, json, ts, js.
outputDestinationTarget directory + filename. init derives the extension from the format.

Locally and in scheduled jobs, particles token-studio sync reads both fields and writes the file — no flags needed. In an event-driven webhook workflow, read the same file so the format and path stay defined in exactly one place (shown below).

i

Need more than one artifact (say a Tailwind CSS file and a typed TS theme)? Add a step per format with token-studio export --format <fmt> --output <path>, or pull each format URL from data.exports in the payload.

GitHub Actions example#

Point the webhook at a repository_dispatch relay (or any receiver that forwards the payload), then read particles-ui.json for the format and path, pull the release, and open a PR:

.github/workflows/sync-tokens.yml
on:
  repository_dispatch:
    types: [particles-release]   # release.published webhook
jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Format + output path are the repo's choice — read them from particles-ui.json.
      - id: cfg
        run: |
          echo "format=$(jq -r .format particles-ui.json)" >> "$GITHUB_OUTPUT"
          echo "out=$(jq -r .outputDestination particles-ui.json)" >> "$GITHUB_OUTPUT"
      - name: Pull released tokens
        env:
          WHSEC: ${{ secrets.PARTICLES_WEBHOOK_SECRET }}
          PID: ${{ github.event.client_payload.projectId }}
          VERSION: ${{ github.event.client_payload.data.version }}
        run: |
          mkdir -p "$(dirname "${{ steps.cfg.outputs.out }}")"
          curl -fsS -H "Authorization: Bearer $WHSEC" \
            "https://api.particles-ui.com/v1/projects/$PID/releases/$VERSION/export?format=${{ steps.cfg.outputs.format }}" \
            | jq -r '.files[0].contents' > "${{ steps.cfg.outputs.out }}"
      - uses: peter-evans/create-pull-request@v6
        with:
          branch: tokens/${{ github.event.client_payload.data.version }}
          title: "chore(tokens): sync Particles release ${{ github.event.client_payload.data.version }}"
          body: ${{ github.event.client_payload.data.notes }}
i

The format and output path come from particles-ui.json, not the workflow — change them with particles init and every CI run follows. The webhook is what makes this event-driven: it runs the moment a release is published, not on a schedule. For local or scheduled pulls, particles token-studio sync --version <v> reads the same config — see CLI.

Triggering the workflow on publish#

The workflow above listens for repository_dispatch. GitHub only starts a workflow from an authenticated dispatch call, so point a Particles webhook directly at GitHub's dispatch endpoint — no relay or extra infrastructure:

Webhook fieldValue
URLhttps://api.github.com/repos/<owner>/<repo>/dispatches
SecretA GitHub token (PAT) with Contents: read & write on the repo
Eventrelease.published

Create it in Studio → Settings → Webhooks. When the URL is a GitHub dispatch endpoint, Particles delivers the event in GitHub's shape — the secret is sent as the Bearer token and the payload is wrapped as { event_type: "particles-release", client_payload }, which fires the repository_dispatch workflow. Publishing a release now opens a token PR automatically.

i

The PAT is the webhook secret here, so scope it tightly (a fine-grained token limited to the one repo). In the repo, enable Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests so the workflow can open the PR.

← PREVIOUS
Connect AI agents
NEXT →
Security