Skip to content

Inbound Task Sync

Inbound Task Sync is the lightweight authenticated webhook that lets external task tools push work into a TruePPM project — without TruePPM having to host an OAuth handshake or maintain a connector. You mint a token, your external system POSTs to /projects/{id}/task-sync/, and the task lands in the project’s backlog ready for the PM to schedule.

It’s deliberately import-only: status changes you make in TruePPM do not flow back to the external source. Designate one source of truth for status before setup — see Source of truth below. Two-way sync with conflict resolution is on the Enterprise roadmap.

This feature closes ADR-0065 Gap 3 and is detailed in ADR-0068.

  • Authenticated POST /api/v1/projects/{id}/task-sync/ endpoint
  • Project-scoped API tokens (Authorization: Bearer tppm_<64-hex>)
  • Idempotent upsert by (project, source, external_id) — re-pushes update; they don’t duplicate
  • Default status map (todoNOT_STARTED, in_progressIN_PROGRESS, doneCOMPLETE, plus common synonyms) with per-token override
  • Assignee resolution by email; unresolved emails parked in a per-link queue
  • Parent attach via parent_external_id — preserves Jira epic → story hierarchy
  • Per-project rate limit (100 req/min steady state, 1000 req/min during the first 60 minutes after token creation — the backfill window for migrating existing data)
  • Append-only audit log readable by every project member
  • No web UI for token management — tokens are minted and revoked via API (see Mint a token). A token-management screen in project settings is on the roadmap.
  • No write-back to the external source — TruePPM is downstream. Status, name, and assignee changes you make in TruePPM stay in TruePPM.
  • No sprint binding from the payload — every inbound task lands in the project backlog (status=BACKLOG, sprint=null). The PM places it into a sprint via the normal sprint-planning surface.
  • No OAuth handshake or HMAC signature verification — authentication is the bearer token only. If you need stronger guarantees (signed payloads, SSO-gated token issuance) those are Enterprise.
Terminal window
curl -X POST "https://your-truppm/api/v1/projects/${PROJECT_ID}/api-tokens/" \
-H "Authorization: Bearer ${YOUR_JWT}" \
-H "Content-Type: application/json" \
-d '{"name": "Jira Production"}'

The response contains the raw token in the token field. Copy it now — it is not retrievable later. Subsequent reads of this endpoint return the prefix (the first 8 hex characters) only.

To configure a custom status mapping for a source whose vocabulary doesn’t match the default:

Terminal window
curl -X POST "https://your-truppm/api/v1/projects/${PROJECT_ID}/api-tokens/" \
-H "Authorization: Bearer ${YOUR_JWT}" \
-d '{
"name": "Linear",
"status_map": {
"started": "IN_PROGRESS",
"in_review": "REVIEW",
"shipped": "COMPLETE"
}
}'

status_map is immutable after mint by design — changing it requires minting a new token and revoking the old one (which appears in the audit log so the team sees it).

Terminal window
curl -X POST "https://your-truppm/api/v1/projects/${PROJECT_ID}/task-sync/" \
-H "Authorization: Bearer ${TPPM_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"source": "jira",
"external_id": "PROJ-123",
"name": "Add CSV export to the report dashboard",
"description": "Customers asked for...",
"status": "in_progress",
"assignee_email": "priya@example.com",
"story_points": 3,
"external_url": "https://your.atlassian.net/browse/PROJ-123",
"parent_external_id": "PROJ-1"
}'

Response on first push:

{
"task_id": "f8c0...",
"short_id": "00000027",
"created": true,
"assignee_resolved": true
}

Same payload re-pushed: created: false, same task_id. The task name, description, status, and story points are updated; the assignee is not overwritten if it was previously resolved (this prevents a compromised token from silently rewriting human ownership decisions).

Terminal window
curl -X DELETE "https://your-truppm/api/v1/projects/${PROJECT_ID}/api-tokens/${TOKEN_ID}/" \
-H "Authorization: Bearer ${YOUR_JWT}"

Revocation is immediate and append-only — the revoke event appears in the audit log for every project member to see.

FieldRequiredDescription
sourceyesLowercase identifier of the external tool (jira, linear, github, or your own [a-z][a-z0-9_]{0,31} value)
external_idyesThe external system’s identifier — PROJ-123, LIN-42, #7
namerecommendedTask title. Defaults to external_id if omitted
descriptionnoFree-form text (stored in TruePPM’s notes field)
statusnoExternal status string, translated via the token’s status_map
assignee_emailnoIf the email matches a current project member, the assignee is set; otherwise parked in pending_assignee_email
story_pointsnoInteger 0–999
external_urlnoCanonical URL of the external task (used by future UI surfaces)
parent_external_idnoThe external system’s parent identifier. If a previous push with that external_id exists for the same source, the new task is attached as a subtask under it. Cross-source parents are rejected.

Designate one tool as the source of truth for status before setup. TruePPM does not write back. If your team marks a task Done in TruePPM and the same task as In Progress in Jira, the next inbound push from Jira will set it back to In Progress.

The recommended pattern:

  • Jira / Linear / GitHub Issues is the source of truth. TruePPM aggregates work into a PM’s schedule and a contributor’s My Work view. Status edits happen in the source.
  • TruePPM PMs schedule, not status-track. PMs use the schedule to plan critical-path dates and the sprint surface to commit work; status flips happen in the source tool.
  • Contributors (Priya) stay in their tool. The whole point of inbound sync is that the contributor never has to open TruePPM directly.

If you need TruePPM to be the source of truth for status, simply don’t push status in the inbound payload — TruePPM will retain whatever status the team set via the schedule, board, or My Work surfaces.

  • Steady-state: 100 requests per minute per project.
  • Backfill window: 1000 requests per minute per project during the first 60 minutes after the token’s created_at. Designed for migrating an existing Jira project of a few thousand tickets in one go.

429 Too Many Requests includes a Retry-After: 60 header. Clients should respect it.

A 2000-ticket initial import can be chunked like this:

Terminal window
# chunk the export into 1000-row JSONL files: batch_1.jsonl, batch_2.jsonl
for batch in batch_1.jsonl batch_2.jsonl; do
while read line; do
curl -sS -X POST "${API}/projects/${PROJ}/task-sync/" \
-H "Authorization: Bearer ${TPPM_TOKEN}" \
-H "Content-Type: application/json" \
-d "$line"
done < "$batch"
sleep 60 # respect the per-minute window
done

Incremental updates after that fit comfortably in the 100/min steady-state cap.

If an inbound push references an email that doesn’t match any current project member, the task is created with assignee=null and the email is parked on the inbound link as pending_assignee_email. When that user joins the project, the next push for the same external_id will resolve the assignee automatically.

PMs can see the count of unresolved assignees on the project detail response:

Terminal window
curl "https://your-truppm/api/v1/projects/${PROJECT_ID}/" \
-H "Authorization: Bearer ${YOUR_JWT}" \
| jq '.unresolved_assignee_count'

A non-zero count usually means a new contributor still needs to be invited to the project.

Every token mint, revoke, and use is recorded in an append-only audit log:

Terminal window
curl "https://your-truppm/api/v1/projects/${PROJECT_ID}/api-token-audit/" \
-H "Authorization: Bearer ${YOUR_JWT}"
  • Every project member can read this log — sprint sovereignty matters, and the team should see when integration tokens are being used in their workspace.
  • Audit entries record the token prefix (first 8 hex chars), the actor (for minted / revoked), the source IP (for used), and a JSON detail blob describing what happened.
  • Audit rows are never deleted — compliance evidence has indefinite retention.

The following snippets cover the most common external sources. They all use the same endpoint and the same payload shape — only the source-system glue differs.

Push every issue change to TruePPM by adding a workflow:

.github/workflows/sync-to-trueppm.yml
on:
issues:
types: [opened, edited, assigned, closed, reopened]
jobs:
push:
runs-on: ubuntu-latest
steps:
- run: |
curl -X POST "${{ secrets.TPPM_API }}/projects/${{ secrets.TPPM_PROJECT }}/task-sync/" \
-H "Authorization: Bearer ${{ secrets.TPPM_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"source": "github",
"external_id": "${{ github.event.issue.number }}",
"name": "${{ github.event.issue.title }}",
"status": "${{ github.event.issue.state }}",
"assignee_email": "${{ github.event.issue.assignee.email }}",
"external_url": "${{ github.event.issue.html_url }}"
}'

Configure a webhook in Jira’s System → WebHooks that POSTs to a lightweight relay (Jira’s outbound webhook doesn’t let you set arbitrary headers, so a one-line proxy is the simplest path). The relay translates the Jira payload to TruePPM’s shape and forwards it with the bearer header. Status mapping is handled by the status_map on your TruePPM token, so the relay can pass the raw Jira status string through.

Linear’s webhooks include rich JSON. A two-line transform in a Cloudflare Worker or Vercel Function converts Linear’s data.identifier / data.title / data.state.name into TruePPM’s payload shape and POSTs it. Same status_map story.

  • The token is a 256-bit random value, hashed with SHA-256 at rest. The raw value is shown once and never retrievable. Treat it like a password.
  • The tppm_ prefix on every token is greppable by secret scanners (GitGuardian, GitHub secret scanning) — if a token leaks into a public repo, those services will catch it.
  • The token is project-scoped — an attacker who exfiltrates it cannot access other projects’ data or any cross-project surface.
  • A revoked token is rejected immediately; revocation is not eventually consistent.
  • 401 responses do not leak whether the token is malformed, unknown, revoked, or scoped to a different project — all four return the same generic body.

See ADR-0068 §Risks for the full STRIDE analysis.