Integrate with any access control system using Webhooks
Integrations use webhooks to receive real-time events from Storeganise, such as move-ins, move-outs, transfers, and unit rental updates. The provided webhook handler, hosted on napkin.io, efficiently manages these events and communicates with Access Control systems and Storeganise APIs to perform access control actions.
Webhook Events Handled
- Move-In Completion
- Description: Handles the completion of unit move-ins.
- Actions: Creates group memberships, updates access codes, and sets up access PINs.
- Move-Out Completion
- Description: Handles the completion of unit move-outs.
- Actions: Revokes group memberships.
- Unit Transfer Completion
- Description: Handles the completion of unit transfers.
- Actions: Revoke group memberships for the old unit and set up access for the new unit.
- Unit Rental Update
- Description: Handles updates to unit rentals, including PIN changes and overlocking status.
- Actions: Updates access codes and overlocking status.
- Unit Overdue Marking/Unmarking
- Description: Handles marking and unmarking units as overdue.
- Actions: Implements access control measures based on overlocking status.
List of Security Systems We Currently Work With
- Bearbox
- Noke
- PTI
- SaltoKS
- Sensorberg
- OpenTech Alliance
- Entryfy
Storeganise API key & webhook setup
Getting Started
- Webhook Configuration: Set up a webhook on napkin.io to receive Storeganise events.
- Create a webhook in https://{business}.storeganise.com/admin/settings/developer, with the url of your webhook handler (it can be AWS lambda, napkin.io, other services or a custom server) with events: job.unit_moveIn.completed, job.unit_moveOut.completed, job.unit_transfer.completed, unitRental.markOverdue, unitRental.unmarkOverdue, unitRental.updated
- Create customFields you’ll need in your integration in https://{business}.storeganise.com/admin/settings/custom-fields, for example unit.customFields.rubik_id, unitRental.customFields.rubik_pinCode, unitRental.customFields.rubik_overlocked (we use this convention to prefix all custom fields with the integration name, here “rubik”)
- Credentials: Create API keys in Storeganise and obtain credentials for your access control API
- Create an API key with admin role in https://{business}.storeganise.com/admin/settings/developer
- Contact the commercial support, for example: sales@sensorberg.com,
- Deploy the Handler: Host the provided webhook handler code and configure environment variables. (Storeganise can also host the code upon request)
- Test and Monitor: Thoroughly test the integration and monitor webhook events for real-time access control updates.
Code Snippet - Core template for an integration
The main request handler receives events from Storeganise (move-in, move-out, transfer, overlocking) and call the access control API to synchronize these changes
async function fetchSg(subpath, opts = {}) { return fetch(`https://{business}.storeganise.com/api/v1/admin/${subpath}${subpath.includes('?') ? '&' : '?'}include=customFields`, { method: opts.method, headers: { Authorization: `ApiKey ${process.env.API_KEY}`, ...opts.body && { 'Content-Type': 'application/json' }, }, body: opts.body && JSON.stringify(opts.body), }) .then(async r => { const data = await r.json().catch(() => ({})); if (r.ok) return data; throw Object.assign(new Error('sg'), { status: r.status, ...data.error }); }); } export default async (req, res) => { if (!req.body?.data?.jobId) return res.json({ message: 'missing jobId, ignore' }); try { const job = await fetchSg(`/v1/admin/jobs/${req.body.data?.jobId}`); switch (req.body.type) { case 'job.unit_moveIn.completed': { const [rental, unit, owner] = await Promise.all([ fetchSg(`unit-rentals/${job.result.unitRentalId}`), fetchSg(`units/${job.result.unitId}`), fetchSg(`users/${job.result.ownerId}`), ]); // Your logic here for renting a unit using custom fields // you would use node-fetch for calling the access control API // and you can also call the Storeganise API for setting the unitRental custom fields (access codes for example) break; } case 'job.unit_moveOut.completed': { const [rental, unit] = await Promise.all([ fetchSg(`unit-rentals/${job.result.unitRentalId}`), fetchSg(`units/${job.result.unitId}`), ]); // your logic for vacating unit break; } case 'job.unit_transfer.completed': { const [oldRental, newRental, oldUnit, newUnit] = await Promise.all([ fetchSg(`unit-rentals/${job.result.oldRentalId}`), fetchSg(`unit-rentals/${job.result.newRentalId}`), fetchSg(`units/${job.result.oldUnitId}`), fetchSg(`units/${job.result.newUnitId}`), ]); const owner = await fetchSg(`users/${job.result.ownerId}`); // your logic, usually vacate old unit and rent new one, usually reusing code from previous events break; } case 'unitRental.updated': { const rental = await fetchSg(`unit-rentals/${job.result.unitRentalId}`); const [owner, unit] = await Promise.all([ fetchSg(`users/${rental.ownerId}`), fetchSg(`units/${rental.unitId}`), ]); // your logic to update the rental access codes and/or overlocking status using custom fields break; } case 'unitRental.markOverdue': case 'unitRental.unmarkOverdue': { const rentals = await fetchSg(`unit-rentals?ids=${job.result.unitRentalIds || job.result.unitRentalId}`); // your logic for updating those rental using custom fields break; } } res.json({ message: 'ok ' + req.body.type }); } catch(err) { console.error(err); res.status(err.status || 400).json({ message: err.message }); } }