Skip to content

Jira ↔ Priority.vote sync walkthrough

This guide builds a one-way Jira → Priority.vote sync end-to-end: starting from a fresh Priority.vote account, ending with a script that creates a Priority.vote initiative for each Jira issue and keeps the two in sync when the Jira side changes.

The script is written in Python for illustration; the API is language-agnostic and any HTTP client works.

Prerequisites

  • A Priority.vote account with at least one backlog you own.
  • A Jira project and a personal token for it (this guide assumes you can already call the Jira REST API).
  • Python 3.10+, httpx, and uuid available in your environment.

Step 1 — Create the PAT

Follow Personal access tokens:

  • Name: jira-sync
  • Scopes: write:initiatives
  • Restrict to: the single backlog you want Jira to write into
  • Expiry: 1 year

Copy the value and put it in a secret store (PRIORITY_VOTE_PAT for this guide).

Step 2 — Identify the backlog id

Pull your backlogs with the token and pick the right one. Output is paginated; pass ?per_page=100 if you have more than 50.

GET /api/v1/backlogs HTTP/1.1
Authorization: Bearer pv_pat_…

Save the id in an env var (PRIORITY_VOTE_BACKLOG_ID).

Step 3 — Sync loop

import httpx
import os
import time

BASE = "https://priority.vote/api/v1"
PAT = os.environ["PRIORITY_VOTE_PAT"]
BACKLOG_ID = os.environ["PRIORITY_VOTE_BACKLOG_ID"]
HEADERS = {"Authorization": f"Bearer {PAT}", "Accept": "application/json"}
SOURCE = "jira"

def upsert_initiative(issue):
    """Look up by external_ref; PATCH if exists, POST if not."""
    lookup = httpx.get(
        f"{BASE}/backlogs/{BACKLOG_ID}/initiatives",
        params={"external_source": SOURCE, "external_ref": issue["key"]},
        headers=HEADERS,
        timeout=10,
    )
    payload = {
        "title": issue["fields"]["summary"],
        "description": issue["fields"].get("description") or None,
        "effort": issue["fields"].get("customfield_effort", 1),
        "external_source": SOURCE,
        "external_ref": issue["key"],
    }
    idem = f"jira-sync-{issue['key']}-{issue['fields']['updated']}"
    headers = {**HEADERS, "Idempotency-Key": idem}

    if lookup.status_code == 404:
        r = httpx.post(
            f"{BASE}/backlogs/{BACKLOG_ID}/initiatives",
            json=payload,
            headers=headers,
            timeout=10,
        )
    else:
        existing = lookup.json()
        r = httpx.patch(
            f"{BASE}/backlogs/{BACKLOG_ID}/initiatives/{existing['id']}",
            json=payload,
            headers=headers,
            timeout=10,
        )

    handle(r)

def handle(r):
    if r.status_code in (200, 201):
        return
    if r.status_code == 429:
        wait = int(r.headers.get("Retry-After", "5"))
        print(f"rate limited; sleeping {wait}s")
        time.sleep(wait)
        return
    if r.status_code == 422:
        body = r.json()
        print(f"validation error: {body}")
        return
    if r.status_code in (401, 403):
        raise SystemExit(f"auth problem: {r.status_code} {r.text} — check PAT")
    if r.status_code == 409:
        print(f"conflict (likely external_ref race) — will retry next tick")
        return
    raise RuntimeError(f"unexpected {r.status_code}: {r.text}")

Call upsert_initiative for each Jira issue you want to mirror. Run the script every few minutes (cron, systemd-timer, GitLab schedule — your choice).

Step 4 — Handle the corner cases

  • Issue archived in Jira. Decide your policy: archive in Priority.vote too (PATCH status: "archived") or leave alone.
  • Effort missing in Jira. Default to 1. Or pull from a custom field.
  • Jira issue renamed (same key). The external_ref stays stable — your PATCH on title simply updates the existing initiative.
  • Jira issue key changes (rare). Treat as a delete + create.

What the logbook will show

Every PATCH and POST writes a logbook entry tagged via 'jira-sync' token. You can audit exactly which initiative changes came from the adapter vs which came from a human stakeholder.

Where to go next

  • Two-way sync (push Priority.vote changes back to Jira) — read General integration for the polling and error-handling baselines, then mirror this guide in reverse.
  • Other sources: the same pattern works for any tracker. Pick an external_source string and the API will treat it as a separate uniqueness namespace per backlog. New sources must be added to the server registry in a TDR before they are accepted.