Skip to main content

Packages And Container Registry Webhooks

Unizo's Package Registry API provides webhooks to notify your application when important events occur in your package registries and artifact repositories. These real-time notifications enable you to build automated CI/CD workflows, maintain artifact synchronization, track package usage, and ensure security compliance.

Our platform normalizes webhook events from various package registry providers (JFrog Artifactory, Nexus Repository, Docker Registry, GitHub Packages, etc.) into a consistent format, making it easy to handle artifact events across different registry platforms.

Webhook Configuration

To set up webhooks for your integration, visit the Unizo Console Webhooks section for step-by-step configuration guide.

Supported Event Types

Event TypeDescriptionTrigger Conditions
artifact:createdEvent broadcast when an artifact has been createdNew artifact created
artifact:deletedEvent broadcast when an artifact is deletedExisting artifact gets deleted

Webhook Security

Every webhook request sent by Unizo includes a cryptographic signature so you can verify that the payload is authentic and has not been tampered with.

Security Headers

HeaderDescription
x-unizo-event-typeThe type of event that triggered the webhook
x-unizo-signatureHMAC-SHA256 signature of the payload, prefixed with v1= (e.g., v1=ee084789...)
x-unizo-timestampUnix epoch timestamp (seconds) when the request was signed
x-unizo-delivery-idUnique identifier for this webhook delivery

Signature Verification

The signed payload is constructed by joining the timestamp and the raw request body with a dot separator: {timestamp}.{payload}. This ensures the timestamp is covered by the signature, preventing replay attacks.

Verification Steps

  1. Parse the timestamp from the x-unizo-timestamp header and reject the request if it falls outside your tolerance window (recommended: 5 minutes).
  2. Reconstruct the signed payload by concatenating the timestamp, a literal dot (.), and the raw request body.
  3. Compute the expected signature using HMAC-SHA256 with your webhook signing secret as the key.
  4. Strip the v1= prefix from the x-unizo-signature header to get the received signature.
  5. Compare the two signatures using a constant-time comparison function to prevent timing attacks.

Reference Implementation

const crypto = require("crypto");

function verifyWebhookSignature(
  payload,
  signatureHeader,
  timestampHeader,
  secret,
  toleranceSeconds = 300
) {
  // 1. Reject stale timestamps to prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  const timestamp = parseInt(timestampHeader, 10);

  if (Number.isNaN(timestamp)) {
    return false;
  }

  if (Math.abs(now - timestamp) > toleranceSeconds) {
    return false;
  }

  // 2. Reconstruct the signed payload: "{timestamp}.{payload}"
  const signedPayload = `${timestamp}.${payload}`;

  // 3. Compute the expected signature
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  // 4. Strip the "v1=" prefix from the received signature
  const received = signatureHeader.startsWith("v1=")
    ? signatureHeader.slice(3)
    : signatureHeader;

  // 5. Constant-time comparison to prevent timing attacks
  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(received, "hex")
    );
  } catch {
    return false;
  }
}

// Usage
const isValid = verifyWebhookSignature(
  rawBody,                                // raw request body string
  headers["x-unizo-signature"],           // "v1=ee084789..."
  headers["x-unizo-timestamp"],           // "1774093147"
  process.env.UNIZO_WEBHOOK_SECRET        // your signing secret
);

if (!isValid) {
  return res.status(401).json({ error: "Invalid signature" });
}

// Signature verified — process the event
handleEvent(JSON.parse(rawBody));

Artifact Events

Artifact Created

artifact:created

Triggered when a new artifact, package, or container image is uploaded to your package registry.
POSThttps://api.yourapp.com/webhooks/unizo/pcr
Headers
NameTypeRequiredDescription
x-unizo-event-typestringYesThe type of event that was triggered
x-unizo-signaturestringYesHMAC-SHA256 signature, prefixed with v1=
Request Body Schema
PropertyTypeRequiredDescription
typestringYesEvent type identifier
versionstringYesWebhook payload version
contentTypestringYesBranch name/identifier
artifactstringYesArtifact Name
artifact.idstringYesArtifact ID
artifact.keystringYesArtifact Key
artifact.namestringYesArtifact Name
artifact.pathstringYesArtifact Path
integration.typestringYesIntegration type
integration.idstringYesIntegration ID
integration.namestringYesIntegration display name
Example Payload
{
"type": "artifact:created",
"version": "1.0.0",
"contentType": "application/json",
"artifact": {
"id": "testing",
"key": "testing-webhook",
"name": "manifest.json",
"path": "unizoinc/otg-event-catcher/v1.0.0/manifest.json"
},
"integration": {
"type": "PCR",
"id": "864e88c4-69ce-4df7-a2a1-ab3c4e439fac",
"name": "JF_Secu_1734358143372"
}
}
Response
200 OKWebhook processed successfully
400 Bad RequestInvalid webhook payload
401 UnauthorizedInvalid or missing signature

Artifact Deleted

artifact:deleted

Triggered when an artifact is removed from the package registry.
POSThttps://api.yourapp.com/webhooks/unizo/pcr
Headers
NameTypeRequiredDescription
x-unizo-event-typestringYesThe type of event that was triggered
x-unizo-signaturestringYesHMAC-SHA256 signature, prefixed with v1=
Request Body Schema
PropertyTypeRequiredDescription
typestringYesEvent type identifier
versionstringYesWebhook payload version
contentTypestringYesBranch name/identifier
artifactstringYesArtifact Name
artifact.idstringYesArtifact ID
artifact.keystringYesArtifact Key
artifact.pathstringYesArtifact Path
artifact.namestringYesArtifact Name
integration.typestringYesIntegration type
integration.idstringYesIntegration ID
integration.namestringYesIntegration display name
Example Payload
{
"type": "artifact:deleted",
"version": "1.0.0",
"contentType": "application/json",
"artifact": {
"id": "docker-trial",
"key": "docker-trial-webhook",
"path": "exposures/latest/manifest.json",
"name": "manifest.json"
},
"integration": {
"type": "PCR",
"id": "be03cab2-e9d2-4d95-ad45-c5411b2be10a",
"name": "JF_Secu_1734088106577"
}
}
Response
200 OKWebhook processed successfully
400 Bad RequestInvalid webhook payload
401 UnauthorizedInvalid or missing signature

Webhook Delivery & Retries

Unizo implements a robust delivery system with automatic retries to ensure your webhooks are delivered reliably:

  • Timeout: 30 seconds per delivery attempt
  • Retry Schedule: 5 attempts with exponential backoff
    • Attempt 1: Immediate
    • Attempt 2: 1 minute delay
    • Attempt 3: 5 minutes delay
    • Attempt 4: 30 minutes delay
    • Attempt 5: 2 hours delay
  • Success Criteria: HTTP status codes 200-299
  • Failure Handling: After 5 failed attempts, the webhook is marked as failed

Best Practices

1. Idempotency

Always implement idempotent webhook handlers using the x-unizo-delivery-id header:

Idempotent Webhook Handler

// Example: Handling webhooks idempotently
app.post('/webhooks/pcr', async (req, res) => {
const deliveryId = req.headers['x-unizo-delivery-id'];

// Check if we've already processed this delivery
const existingDelivery = await db.webhookDeliveries.findOne({ 
  deliveryId 
});

if (existingDelivery) {
  console.log(`Duplicate delivery detected: ${deliveryId}`);
  return res.status(200).json({ 
    status: 'already_processed' 
  });
}

// Process the webhook
try {
  await processArtifactEvent(req.body);
  
  // Record the delivery
  await db.webhookDeliveries.create({
    deliveryId,
    processedAt: new Date()
  });
  
  res.status(200).json({ status: 'success' });
} catch (error) {
  console.error('Webhook processing failed:', error);
  res.status(500).json({ status: 'error' });
}
});

2. CI/CD Integration

Integrate package events into your CI/CD pipeline:

CI/CD Pipeline Integration

// Example: Trigger deployments on new releases
app.post('/webhooks/pcr', async (req, res) => {
const { type, data } = req.body;

// Acknowledge receipt
res.status(200).json({ status: 'received' });

switch (type) {
  case 'version:released':
    // Trigger deployment pipeline
    if (data.release.version.match(/^d+.d+.0$/)) {
      // Major or minor release - deploy to staging
      await cicdApi.triggerPipeline({
        pipeline: 'deploy-staging',
        parameters: {
          package: data.package.name,
          version: data.release.version,
          artifacts: data.release.artifacts
        }
      });
      
      // Notify team
      await slackApi.postMessage({
        channel: '#releases',
        text: `🚀 New release: ${data.package.name} v${data.release.version}`,
        attachments: [{
          title: 'Release Notes',
          text: data.release.release_notes
        }]
      });
    }
    break;
    
  case 'scan:completed':
    // Check security policy
    if (data.policy_status === 'fail') {
      // Block deployment
      await cicdApi.failPipeline({
        buildId: data.artifact.id,
        reason: 'Security scan failed',
        details: `Found ${data.vulnerabilities.critical} critical vulnerabilities`
      });
      
      // Create security ticket
      await jiraApi.createIssue({
        type: 'Security',
        priority: 'Critical',
        summary: `Security vulnerabilities in ${data.artifact.name}:${data.artifact.version}`,
        description: formatVulnerabilities(data.vulnerabilities)
      });
    }
    break;
}
});

3. Artifact Tracking and Compliance

Maintain artifact inventory and compliance records:

Artifact Compliance Tracking

// Example: Track artifacts for compliance
app.post('/webhooks/pcr', async (req, res) => {
const { type, data, integration } = req.body;

// Create audit record
const auditEntry = {
  timestamp: new Date(),
  eventType: type,
  integration: integration.name,
  artifact: {
    id: data.artifact?.id,
    name: data.artifact?.name,
    version: data.artifact?.version,
    type: data.artifact?.type
  }
};

switch (type) {
  case 'artifact:created':
    // Register in artifact inventory
    await inventoryDb.artifacts.create({
      ...data.artifact,
      status: 'active',
      compliance: {
        scanned: false,
        approved: false,
        licenses: []
      }
    });
    
    // Trigger compliance checks
    await complianceApi.scanArtifact({
      artifactId: data.artifact.id,
      checks: ['license', 'security', 'quality']
    });
    break;
    
  case 'vulnerability:detected':
    // Update compliance status
    await inventoryDb.artifacts.update(data.artifact.id, {
      compliance: {
        scanned: true,
        securityStatus: data.policy_status,
        vulnerabilities: {
          critical: data.vulnerabilities.critical,
          high: data.vulnerabilities.high,
          lastScan: data.scan.completed_at
        }
      }
    });
    
    // Generate compliance report
    if (data.vulnerabilities.critical > 0) {
      await reportingApi.generateSecurityReport({
        artifact: data.artifact,
        vulnerabilities: data.vulnerabilities,
        recipients: ['[email protected]']
      });
    }
    break;
}

// Store audit trail
await auditDb.packageEvents.create(auditEntry);

res.status(200).json({ status: 'tracked' });
});

Need Help?

For webhook-related support: