Skip to content

Task 26: Claim Status Sync (originally: webhook registration)

Status: ✅ DONE 2026-07-16 — via CIH polling, not webhooks (see "Design revision" below; webhooks turned out not to exist in CIH v2)

Design revision (2026-07-16, verified against CIH's OpenAPI spec)

The webhook plan below was invalidated by CIH's actual API (https://stagingcih.velocitycareerlabs.io/documentation/json):

  • CIH v2 exposes no outbound webhook configuration anywhere — the tenant create schema has no webhookUrl (extra fields are silently ignored), the issuer-service schema has none, and the depot messagingSettings.webhookUrl appears only on the read shape with no API to set it. The pre-existing webhookUrl code in velocity-registrar.service.ts and the /claim/webhook/vnf receiver were written against a capability that does not exist.
  • Claim state is exposed for polling instead: each stored credential (GET /operator/credentials/get) carries acceptedAt / rejectedAt, keyed by credentialReference — which we set to credential_offers.id at issuance.

What was built (working, verified end-to-end 2026-07-16)

ClaimSyncService (issuer/services/claim-sync.service.ts): - Cron every 5 minutes (CLAIM_SYNC_ENABLED=false disables) + manual POST /issuer/credentials/sync-claims - Pulls the tenant's credentials from CIH, matches credentialReference → local offer id, flips sent/created offers with acceptedAt to status=claimed, claimed_at=acceptedAt, claim_method=vnf_app - Live verification: offer 232b1629… flipped to claimed with the exact wallet-acceptance timestamp (16:37:11Z) — no tunnel/public URL needed - Uses the global staging tenantId like the rest of issuance — per-org resolution is Task 27

Leftovers / cleanup opportunities

  • PUBLIC_API_URL webhook registration code (velocity-tenant.service) and the /claim/webhook/vnf receiver are now dead-ends for claims — harmless (CIH ignores the extra field) but can be removed or kept for a future CIH version that adds webhooks
  • Old offers issued under a deleted tenant can never flip — mark manually or expire them

Original (superseded) webhook plan — kept for history

Created: 2026-07-16 Priority: High — last functional loose end in the issuance → claim loop Effort: ~half a day code + deploy-time config

Problem (verified 2026-07-16 during local Beta testing)

A credential was claimed successfully in the Velocity staging web wallet (shows Verified), but our credential_offers row stays at status=sent with no claimed_at. Everything that reads offer status is therefore stale:

  • Credentials dashboard status chips + "Credentials Claimed" stats
  • Candidate view (/candidates/[id]) claimed counters
  • Employee detail → Credential Offers section
  • Reminder engine (GAP-06): keeps nudging candidates who already claimed (offers stay sent, which is exactly what the reminder cron looks for)

Root cause

The claim happens wallet ↔ Velocity CIH; our backend is not in that path. We only learn of it via a webhook, and:

  1. We never register a webhook URL with CIH. The receiving endpoint already exists — POST /api/v1/claim/webhook/vnf (claim.controller.tsClaimService.processVNFWebhook) — but velocity-tenant.service.ts never passes webhookUrl when creating the tenant, so CIH has nowhere to send claim notifications. This affects every environment, not just local.
  2. Local dev is unreachable anyway — Velocity's cloud cannot call http://localhost:8000; a tunnel (ngrok etc.) is required for local end-to-end testing.

Implementation plan

  1. DONE 2026-07-16PUBLIC_API_URL + optional VNF_WEBHOOK_BEARER_TOKEN added to .env.example. Unset → registration skipped with a warning log (previous behaviour).
  2. DONE 2026-07-16velocity-tenant.service.ts now passes webhookUrl: "${PUBLIC_API_URL}/api/v1/claim/webhook/vnf" (plus bearer webhookAuth when the token is set) in the CIH create-tenant request.
  3. Existing tenants: tenants created before this change have no webhook. Check whether CIH exposes a tenant-update call; if not, the org's tenant must be re-created (retry-tenant-creation path) or updated manually via the Velocity dashboard.
  4. Verify the payload contract: processVNFWebhook was written against the documented shape but has never received a real staging call. First live webhook should be inspected (it's logged in full) and the handler adjusted if the real payload differs.
  5. Security: the webhook route is unauthenticated by design (CIH calls it). Confirm/introduce a shared-secret or signature check (ClaimService has an HMAC helper — check whether CIH signs webhooks and validate if so).

How to test locally (today)

# 1. Expose the local backend
ngrok http 8000        # → https://<id>.ngrok.app

# 2. Set the env var and restart backend
echo 'PUBLIC_API_URL=https://<id>.ngrok.app' >> app/backend/.env.development
docker compose -f docker-compose.local.yml restart backend

# 3. Re-create the org's tenant (retry tenant creation) so CIH learns the URL
# 4. Issue a credential, claim it in the staging wallet
# 5. Watch the webhook land:
docker logs -f backend_rec_instance | grep -i webhook
# 6. Confirm: credential_offers.status = claimed, claimed_at set

Acceptance criteria

  • Tenant creation registers the webhook URL when PUBLIC_API_URL is set
  • Claiming a credential flips the offer to claimed + sets claimed_at without manual intervention
  • Claimed offers stop receiving reminder emails
  • Webhook payload authenticity is validated (or explicitly documented as not supported by CIH)
  • 25-phase-a-pending-tasks.md entry marked done

Cross-references

  • 25-phase-a-pending-tasks.md — Phase A checklist (linked entry)
  • app/backend/src/issuer/controllers/claim.controller.ts — existing receiver
  • app/backend/src/admin/services/velocity-tenant.service.ts — where registration goes
  • app/backend/src/shared/services/cih/cih-api.types.tswebhookUrl already in the request type