Set Up Callback Notifications
Get notified instantly when jobs complete instead of polling for status.
What You'll Need
- Partner Authentication Token - A Bearer token provided by SASHA for callback authentication
- Callback Secret - A secret key for HMAC-SHA256 payload signature validation
- Publicly accessible HTTPS endpoint - To receive callback notifications
- Access Token - See Get an Access Token guide
For local testing, you can use ngrok to create a public URL for your local server. See the Local Development section below.
Why Callbacks?
- ✅ Instant notifications when jobs complete
- ✅ No wasted API calls
- ✅ Scales with any number of concurrent jobs
When you submit a job with a callback_url
, SASHA will send an HTTP POST request to your endpoint when the job status changes:
Create a Callback Endpoint
- Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/callbacks/sasha-job-update', async (req, res) => {
// 1. Validate the Bearer token
const authHeader = req.headers.authorization;
if (authHeader !== `Bearer ${process.env.SASHA_PARTNER_AUTH_TOKEN}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 2. Validate HMAC-SHA256 signature
const requestSignature = req.headers['sasha-request-signature'];
const requestId = req.headers['sasha-request-id'];
const requestMethod = req.method;
const requestUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const requestPayload = JSON.stringify(req.body);
const computedSignature = crypto
.createHmac('sha256', process.env.SASHA_CALLBACK_SECRET)
.update(`${requestMethod}${requestUrl}${requestId}${requestPayload}`)
.digest('hex');
if (computedSignature !== requestSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
const job = req.body;
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processCallback(job, requestId).catch(console.error);
});
const processCallback = async (job, requestId) => {
// Check for duplicates using request ID
if (await isDuplicate(requestId)) return;
if (job.status === 'completed') {
console.log('Job completed:', job.job_id);
// Download protected image before it expires
if (job.output_url) {
await downloadImage(job.output_url, job.job_id);
}
// Store signature ID and notify user
await storeResult(job.job_id, job.signature_id);
}
else if (job.status === 'failed') {
console.error('Job failed:', job.error);
await handleFailure(job);
}
};
app.listen(3000);
Submit Jobs with Callback URL
Include your callback_url
when submitting jobs:
- REST
- gRPC
curl -X POST https://partner.api.sasha.eu/signature/embed \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_url": "https://example.com/image.jpg",
"media_mime_type": "image/jpeg",
"callback_url": "https://your-app.com/callbacks/sasha-job-update"
}'
const embedResponse = await signatureClient.embedSignatureFromURL(
{
mediaUrl: "https://example.com/image.jpg",
mediaMimeType: "image/jpeg",
callbackUrl: "https://your-app.com/callbacks/sasha-job-update",
},
{ metadata }
);
Security
SASHA callbacks use two layers of security to ensure authenticity and integrity:
1. Partner Authentication Token
Validates that the request comes from SASHA:
const authHeader = req.headers.authorization;
if (authHeader !== `Bearer ${process.env.SASHA_PARTNER_AUTH_TOKEN}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
2. Callback Payload Signature
Protects the payload from tampering in transit using HMAC-SHA256:
const requestSignature = req.headers['sasha-request-signature'];
const requestId = req.headers['sasha-request-id'];
const requestMethod = req.method;
const requestUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const requestPayload = JSON.stringify(req.body);
const computedSignature = crypto
.createHmac('sha256', process.env.SASHA_CALLBACK_SECRET)
.update(`${requestMethod}${requestUrl}${requestId}${requestPayload}`)
.digest('hex');
if (computedSignature !== requestSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
The signature is calculated as follows:
- Take the HTTP request method (e.g.,
POST
) - Concatenate the request URL (e.g.,
https://your-app.com/callbacks/sasha-job-update
) - Concatenate the value of the
SASHA-Request-ID
header - Concatenate the raw request payload (body as a string)
- Compute the HMAC-SHA256 digest of the resulting string using your Callback Secret as the key
signature = HMAC_SHA256(
request_method + request_url + request_id + request_payload,
callback_secret
)
- There are no separators or padding between the concatenated values.
Example Signature Validation
Given:
- Callback Secret:
1234567890
- Request method:
POST
- Request URL:
https://your-app.com/callbacks/sasha-job-update
- Request ID:
aa-b-c-d-ee
- Request payload:
{"job_id": "1234567890", "status": "completed"}
Concatenated string:
POSThttps://your-app.com/callbacks/sasha-job-updateaa-b-c-d-ee{"job_id": "1234567890", "status": "completed"}
Expected signature:
c977b19ba0bef139417b40a68fc904bdca2ddefa752ee6032ff5aa4607606c24
Always validate both the Bearer token AND the HMAC signature. While signature validation is not mandatory, SASHA strongly recommends it to ensure the payload genuinely comes from SASHA and was not modified in transit.
Best Practices
1. Respond Immediately
Return 200 OK
quickly, then process asynchronously:
// Good
res.status(200).send('OK');
processCallback(job).catch(console.error);
// Bad - blocks the response
await processCallback(job);
res.status(200).send('OK');
2. Handle Duplicates
SASHA may retry delivery. Use SASHA-Request-ID
to detect duplicates:
const requestId = req.headers['sasha-request-id'];
if (await isDuplicate(requestId)) {
return res.status(200).send('OK');
}
3. Download Images Promptly
The output_url
expires. Download immediately:
if (job.status === 'completed' && job.output_url) {
await fetch(job.output_url)
.then(r => r.arrayBuffer())
.then(data => saveToStorage(job.job_id, data));
}
4. Handle Failures
Different error codes need different handling:
if (job.status === 'failed') {
switch (job.error.code) {
case 'unsupported_format':
// Don't retry - invalid format
await notifyUser('Use JPEG or PNG format');
break;
case 'failed_to_fetch_from_url':
case 'internal':
// May retry or escalate
await handleRetryableError(job);
break;
}
}
Local Development
Use ngrok to test callbacks locally:
# Terminal 1: Start your server
node server.js
# Terminal 2: Create tunnel
ngrok http 3000
# Use the ngrok URL as your callback_url
# Example: https://abc123.ngrok.io/callbacks/sasha-job-update
Common Issues
Issue | Solution |
---|---|
Callbacks not received | Ensure endpoint is publicly accessible via HTTPS |
401 Unauthorized | Verify Bearer token matches your Partner Authentication Token |
Invalid signature | Check HMAC-SHA256 calculation and Callback Secret |
Wrong signature | Ensure URL doesn't include query params or fragments in signature calculation |
Duplicate processing | Implement idempotency using SASHA-Request-ID |
Timeouts | Respond with 200 OK immediately, process async |
See Also
- Quickstart - Polling approach
- Handle Token Expiration - Token refresh
- Authentication Concepts - Learn about callback authentication