Webhooks
Verifying the events Superroute sends about deposits and pickups.
Events
| Event | Meaning |
|---|---|
partner_locker.delivery.doors_opened |
Compartment doors were opened for a delivery. |
partner_locker.delivery.delivered |
A delivery was confirmed into the lockers. Carries the pickup code and the compartments. |
partner_locker.pickup.completed |
The recipient collected the delivery. |
partner_locker.delivery.failed |
A delivery failed. |
partner_locker.delivery.expired |
A delivery expired. |
partner_locker.delivery.cancelled |
A delivery was cancelled. |
partner_locker.delivery.correction_reopened |
Compartments were reopened to correct a delivery. |
partner_locker.delivery.code_rotated |
An access or pickup code was replaced. |
The carrier_package.distribution.* events of the carrier channel are verified the same way.
Verification
Verify every webhook against the raw body exactly as received, before parsing it. sLocker checks the timestamp (5 minutes either way by default), the X-Webhook-Content-Digest header and the X-Webhook-Signature header, and returns the event.
- The signature is v1= followed by the base64 HMAC-SHA256 of {timestamp}\n{X-Webhook-Delivery-Id}\n{body}, made with the signing secret of your endpoint.
- During a key rotation the previous secret stays valid for 24 hours: pass both secrets.
- Endpoints switched to Standard Webhooks headers or to CloudEvents bodies are verified as well.
- Handle each eventId once: it stays the same on every retry, while deliveryId changes on every attempt.
- Answer with a 2xx status within 10 seconds, and keep sandbox events (event.test) apart from production.
// JavaScript / TypeScript (Express)
import { verifyWebhook, WebhookVerificationError } from '@superrouteca/slocker';
app.post('/webhooks/slocker', express.raw({ type: '*/*' }), async (req, res) => {
try {
const event = await verifyWebhook(req.body, req.headers, [process.env.SLOCKER_SECRET, process.env.SLOCKER_OLD_SECRET]);
if (!event.test) await queue.add(event.eventId, event);
res.sendStatus(204);
} catch (e) {
if (e instanceof WebhookVerificationError) return res.status(400).send(e.reason);
throw e;
}
});
Each language
| Language | Verify | Sign a test request |
|---|---|---|
| JavaScript / TypeScript | await verifyWebhook(body, headers, secrets) |
await signWebhook(secret, body) |
| Python | slocker.verify(body, headers, secrets) |
slocker.sign(secret, body) |
| PHP | Webhooks::verify(file_get_contents('php://input'), getallheaders(), $secrets) |
Webhooks::sign($secret, $body) |
| Java / Kotlin | Webhooks.verify(body, headers, secrets) |
Webhooks.sign(...) |
| Go | slocker.Verify(body, slocker.HeaderMap(r.Header), secrets, nil) |
slocker.Sign(secret, body, slocker.SignOptions{}) |
| C# / .NET | Webhooks.Verify(body, headers, secrets) |
Webhooks.Sign(secret, body) |
| Dart / Flutter | verifyWebhook(bodyBytes, headers, secrets) |
signWebhook(secret, bodyBytes) |
| Swift | try Webhooks.verify(body:headers:secrets:) |
Webhooks.sign(secret:body:) |
# Python
import slocker
event = slocker.verify(request.get_data(), dict(request.headers), [secret, old_secret])
// PHP
use Superroute\SLocker\Webhooks;
$event = Webhooks::verify(file_get_contents('php://input'), getallheaders(), [$secret, $oldSecret]);
// Go
event, err := slocker.Verify(body, slocker.HeaderMap(r.Header), []string{secret, oldSecret}, nil)
// C# / .NET
var webhookEvent = Webhooks.Verify(body, headers, new[] { secret, oldSecret });
A refused webhook raises an error with one of these reasons:
MISSING_HEADERINVALID_TIMESTAMPTIMESTAMP_OUT_OF_TOLERANCEDIGEST_MISMATCHSIGNATURE_MISMATCHINVALID_PAYLOAD