Skip to main content

Webhooks

Zupertry sends a signed HTTP POST to your webhook URL when a job completes or fails. Webhooks are the recommended way to receive results — more reliable than polling.

Setup

Provide your webhook URL when submitting a job:
{
  "model_image_url": "https://...",
  "garment_image_url": "https://...",
  "webhook_url": "https://your-app.com/webhooks/zupertry"
}
Or set a default webhook URL for your entire organisation in the console under Settings → Webhook URL. The per-request webhook_url overrides the org-level default.

Event types

EventWhen sent
job.completedJob finished successfully, output image is ready
job.failedJob failed after all retries, credit refunded

Payload shape

{
  "type": "job.completed",
  "data": {
    "job_id": "job_abc123",
    "workspace_id": "ws_xyz",
    "org_id": "org_def456",
    "status": "completed",
    "output_url": "https://storage.googleapis.com/zupertry-outputs/...",
    "credits_consumed": 1,
    "created_at": "2026-03-19T10:00:00.000Z",
    "completed_at": "2026-03-19T10:00:05.231Z"
  }
}
For job.failed:
{
  "type": "job.failed",
  "data": {
    "job_id": "job_abc123",
    "status": "failed",
    "error": "Vertex AI returned an unexpected response format",
    "credits_consumed": 0,
    "created_at": "2026-03-19T10:00:00.000Z"
  }
}

Signature verification

Every webhook request includes an X-Zupertry-Signature header. This is an HMAC-SHA256 of the raw request body, signed with your webhook signing secret.
Always verify the signature before processing a webhook. Skip this and anyone can forge events to your endpoint.

Node.js

const crypto = require('crypto');

function verifySignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody) // must be the raw Buffer, not parsed JSON
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

// Express example
app.use('/webhooks/zupertry', express.raw({ type: 'application/json' }));
app.post('/webhooks/zupertry', (req, res) => {
  const valid = verifySignature(
    req.body,
    req.headers['x-zupertry-signature'],
    process.env.ZUPERTRY_WEBHOOK_SECRET
  );

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

  const event = JSON.parse(req.body.toString());
  // handle event...
  res.json({ received: true });
});

Python (Flask)

import hmac, hashlib, os
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/zupertry', methods=['POST'])
def webhook():
    sig = request.headers.get('X-Zupertry-Signature', '')
    secret = os.environ['ZUPERTRY_WEBHOOK_SECRET'].encode()
    expected = hmac.new(secret, request.data, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(sig, expected):
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.get_json(force=True)
    print(f"Event: {event['type']}", event['data']['job_id'])
    return jsonify({'received': True})

Retry logic

If your endpoint returns a non-2xx status (or doesn’t respond within 10 seconds), Zupertry retries the delivery:
AttemptDelay
1st retry60 seconds
2nd retry5 minutes
After 3 failuresMarked failed, visible in Logs
Always respond 200 OK as quickly as possible (within the 10-second window). Do any heavy processing asynchronously — add the event to a queue and process it later.

Idempotency

Your endpoint may receive the same event more than once (network timeouts cause retries even after successful delivery). Always make your handler idempotent by using job_id as a deduplication key:
const processedJobs = new Set(); // use Redis or database in production

app.post('/webhooks/zupertry', (req, res) => {
  // ... signature verification ...

  const event = JSON.parse(req.body.toString());
  const jobId = event.data.job_id;

  if (processedJobs.has(jobId)) {
    return res.json({ received: true, duplicate: true }); // already handled
  }

  processedJobs.add(jobId);
  // process the event...
  res.json({ received: true });
});

Reading webhook logs

All delivery attempts are logged in the Zupertry console under Logs → Webhook logs. Each row shows:
  • Delivery attempt number
  • HTTP status returned by your endpoint
  • Response time (ms)
  • Timestamp
Click any row to expand the full request payload and response body.

Testing webhooks locally

Use ngrok to expose your local server:
ngrok http 3000
# → Forwarding: https://abc123.ngrok.io -> localhost:3000
Use the ngrok URL as your webhook URL when submitting test jobs.