API Integration 0.0.84
Introduction
So, you’ve integrated the Lens SDK into your mobile app, which is producing verified photos and videos that you can trust – now what? For some use cases, you just upload the files to your backend and call it a day. You’re simply swapping out unverified captures with verified ones.
But, what if you want to easily extract the verified data sealed within the capture? Or the location details that correspond to the latitude and longitude where the capture was taken? That’s where the value-added web services provided by the Lens API come into play.
In this guide, we’ll walk through an end-to-end integrating of these services into your backend app or API. We’ll be using Node.js with the Express web application framework, but the code and concepts should be easy enough to translate into the language/framework of your backend.
Getting Started
Before we get into the details, let’s set the stage with a simple, bare-bones
Express app that we can flesh out as we move forward. There’s nothing special
about this – it’s just enough to get a web server running on
http://localhost:3000 with the necessary dependencies:
package.json
{
"type": "module",
"scripts": {
"start": "nodemon app.js"
},
"dependencies": {
"express": "^4.17.1",
"form-data": "^4.0.0",
"multer": "^1.4.3",
"node-fetch": "^3.0.0"
},
"devDependencies": {
"nodemon": "^2.0.13"
}
}
app.js
/* Dependencies */
import express from 'express'
/* Constants */
const PORT = 3000
/* Variables */
const app = express()
/* Services */
/* Routes */
app.post('/', (req, res) => {
res.send({})
})
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`)
})
To keep from repeating this code over-and-over as we flesh out the details,
we’ll simply reference the name of the section (like /* Dependencies */) where
the code in question is to live.
If you’d like to run the code yourself along the way, simply install the latest LTS version of Node.js – we like nvm for this – install the dependencies, and start the server:
$ nvm install --lts
$ npm install
$ npm start
Authentication
Lens uses API keys to authenticate with its API. Each org unit can have one or more API keys, and each one can be scoped to the exact permissions you desire. This allows you to create a separate key for each platform, environment, or use – whatever makes sense for your setup.
(At this time, API keys are managed internally by the Truepic team. If you don’t have one yet, please reach out to have one created!)
Once you have an API key, let’s set it as a constant in our code:
/* Constants */
const API_KEY =
'cTrvjmhDN2qlS43akO/1XnWQ/pL7dF9CyKOKoc1zZ6/Xzw/rE/mPEbjhFstJYiss'
(Make sure to replace that with your own API key, otherwise you’ll receive a
401 Unauthorized response!)
API keys should be treated as sensitive secrets and stored in a secure way
outside of version control. In other words, we definitely shouldn’t be
hard-coding the API key in our code like this! Rather, we should store it in an
environment variable and reference it via process.env.API_KEY. But, for the
sake of this walk through, we’ll roll with hard-coding it for convenience.
Upload the Capture for Processing
Please note that there is currently a file size limit of 100 MB per video file uploaded to the API, which is approximately 45 seconds of length, depending on the device.
The first major piece of code we’re going to add is a POST / route that
accepts a photo or video upload and sends the file along to the Lens API for
processing. We included a stub of this route in our initial code:
/* Routes */
app.post('/', (req, res) => {
res.send({})
})
We’re going to leverage a few Node.js packages to make this happen:
- Multer to handle the incoming
multipart/form-dataupload, - Form-Data to create the outgoing
multipart/form-datarequest body that includes the uploaded file, and - Node Fetch to
POSTsaidmultipart/form-datarequest to the Lens API.
(The functionality provided by these packages is very common for a backend web app, so you shouldn’t have any trouble finding similar packages in your language/framework.)
Let’s start with the service that uploads the file to the Lens API:
/* Dependencies */
import FormData from 'form-data'
import fetch from 'node-fetch'
/* Services */
async function uploadToLens(file) {
const form = new FormData()
form.append(
'options',
JSON.stringify({
verification: true,
})
)
form.append(
'custom_data',
JSON.stringify({
session_id: 123,
})
)
form.append('file', file, 'file.jpg')
const response = await fetch('https://lens-api.truepic.com/captures', {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
},
body: form,
})
return response.json()
}
You’ll notice that, in addition to the file being uploaded, we’re also
including two JSON-encoded parts in our multipart/form-data body: options
and custom_data.
options instructs Lens how to process the file. In this case, we’re only
instructing it to run the verification service, but we could have specified
any number of other services to run in parallel, such as location or
reverse_image_search.
custom_data allows you to associate any custom data you want with the upload.
While optional, it’s typically helpful to include an identifier of some sort
that ties the upload to a resource in your system. For example, say your app is
used for conducting virtual inspections. You could include an inspection_id to
know which inspection the upload in question is associated with.
With that service in place, let’s quickly update our POST / route to accept a
capture upload and pass it along to the service:
/* Dependencies */
import multer from 'multer'
/* Variables */
const upload = multer({ storage: multer.memoryStorage() })
/* Routes */
app.post('/', upload.single('file'), async (req, res) => {
const capture = await uploadToLens(req.file.buffer)
res.send(capture)
})
The upload is stored in memory (simply for convenience), passed to the service, and then the Lens API response is piped back through so you can easily see what happened. If successful, the response should look something like this:
{
"capture": {
"created_at": "2021-09-16T20:55:24.686Z",
"custom_data": {
"session_id": 123
},
"file_hash": "cLBztEy/HJmLSPnn/BMdz2bisUJOVV4bbztQQiVy/rU=",
"file_size": 6815727,
"id": "131b2e2f-e3e2-4503-859d-26ce2df68102",
"processed_at": null,
"status": "WAITING",
"type": "PHOTO",
"updated_at": "2021-09-16T20:55:24.686Z",
"url": "https://s3.us-east-2.amazonaws.com/lens-captures-staging/34c4e7c8-e498-43a8-880c-9da96553e982.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIASOSZDTMLGKMVA2E5%2F20220128%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20220128T170535Z&X-Amz-Expires=36000&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEK%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMiJHMEUCIHW%2Bg7pZQHXnzx7iUsy3sBPR4V0cBVB%2Bnvkm2vBghAeiAiEAvVUsk%2FTCK2%2Fpw1sTNOk81fBYNmkmsLweOEags%2FGafRMqgwQI2P%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARACGgwxNjg3NjQzNDkyMDYiDDdt9OkLcyq5RVHZ1SrXA84eVBlxT8xJ5CadFXM1iD%2FKzexRXrWG9iSJgKRnqBlG303Xs0HFKXKzgX%2B9FGVKpSOIP2Mjh8TbiZ8bpZjOIe%2FK%2FHUW6NYGAeQ9rpCEuTOD7LLS%2BCxaFvD6KQWwzC8peHU7DjmFi40zEtyYRybL%2FC3CzDoEMlFtWXYOJhG%2FF8YdBTbnXFQ%2B23aApSWpAKENLhy11sL8DH8Rp1kQn9Cims55HaU04UhIUJitO4cRRT3lLdX6Zln3bk4cf3MltORhsszhHy18gyZ3psIBC1fPWOa8pBONm4MaKzLuN%2F2yfWehi6YV9WcsnjKzLTLgIvPl25HVbQSB9BJZIEWva9cvBKFRgH9ZI63lJX6wxAvB5chCFNAS9yFlCsvjK3JluyUhSLxPGkatIT3oJ3NrHY%2BTILerPytLn70ICr5iahTArO2emFrCB90tq8qu1cHzzpWdGdF2BZbi993MQ2lN84uOx%2FyJdRbYCbAE5BB76EsApwgMBQHDKVXFN841eW89ZHYgR5c2BuiaupwxZckT5PbgZdxqbsfo9pRn1jLEX%2BfaA1FaWwVvurGAkD2b%2FLquUA03B2H3kDVBSWM36MlgtYkpy3WlPDZYuhjXjbsETcfY2yoCsTKfNfeERDCtiNCPBjqlAU1KsdMNCpCUvjFNwWkYyk%2B4Q2FGYnmf%2BOjwrn6vVnCvC1aLR7vNu1l3wMCfSYyTswsH0X%2FhSj%2BSR7Kgv5GOGYZydAeY7l1xHbpzNboPqu7V64Z64TfPb1C1eNrPEXsBTmeMD%2FSZQdpC43W0ISfp0VXfv%2Bm1%2F464a6SpxI5giQEIdUIlrxZGx3QrwGqOvmKEL5RK5KQPOt3Eok4s7BPHxrQEof4JJg%3D%3D&X-Amz-Signature=7f0028823844fcc760c9fb1e63fd8b2d1f9954ea1734bcc1384d91877025dd50&X-Amz-SignedHeaders=host&x-id=GetObject"
}
}
The upload is now waiting to be processed! Within a few seconds (typically), it
should be fully processed as instructed by the options we passed in and ready
for use.
But how do we know when that happens? The answer is webhooks.
Receive the Processed Capture Via Webhook
Lens uses webhooks to notify you when a capture has been created, updated, processed, and deleted. Each org unit can have one or more webhooks configured, and each one can be scoped to the exact types you desire. This allows you to receive the results everywhere you need.
(At this time, webhooks are managed internally by the Truepic team. Please reach out if we haven’t set one up for you yet!)
To receive a webhook request from Lens, we need to add a POST route to handle
it:
/* Routes */
app.post('/webhook', (req, res) => {
console.log(`Processing webhook: ${req.body.type}`)
console.dir(req.body.data, { depth: null })
res.sendStatus(200)
})
It’s totally up to you how to proceed with the data. Here we’re just logging it to the console, but you could create or update records in your database, download the file(s) to store in your infrastructure, kick off subsequent processes that depend on the data – or any combination of those. Again, it depends on your use-case. Our only recommendation is that you run those tasks asynchronously as background jobs if they’re going to take more than a few seconds. Lens will close the connection if it doesn’t receive a response in a reasonable amount of time.
Finally, we reply with a 200 status to signal that the webhook has been
received successfully. No response body is necessary. If an error does occur, a
4xx or 5xx status will cause Lens to retry the request later (up to 5
times).
Verify the Webhook Request (Optional)
We’re now receiving and processing the webhook, but there’s a potential security hole: a completely open route that doesn’t require any authentication. A bad actor with enough willpower could craft a request to trick the webhook route into trusting data that’s not from Lens.
To prevent against this, each request includes a truepic-signature header that
can be verified to ensure it’s actually from Lens. While this is completely
optional, we recommend it for the increased security – and it’s not difficult to
implement.
The truepic-signature header looks like this:
truepic-signature: t=1634066973,s=6FBEiVZ8EO79dk5XllfnG18b83ZvLt2kdxcE8FJ/BwU
As you can see, there’s a t value that’s the timestamp of when the request was
sent, and an s value that’s the signature of the request.
Let’s walk through how to parse and verify these.
Parse the Header Value
To start, split the header value on the comma (,). This should leave you with
two parts:
t=1634066973s=6FBEiVZ8EO79dk5XllfnG18b83ZvLt2kdxcE8FJ/BwU
Next, split each part on the equals (=). This should leave you with two values
for each part:
t1634066973s6FBEiVZ8EO79dk5XllfnG18b83ZvLt2kdxcE8FJ/BwU
Here’s our service that does that with some validation along the way:
/* Services */
function parseSignatureHeader(header) {
const error = new Error('Invalid truepic-signature header')
if (!header?.length) {
throw error
}
const [timestampParts, signatureParts] = header.split(',')
if (!timestampParts?.length || !signatureParts?.length) {
throw error
}
let [t, timestamp] = timestampParts.split('=')
if (t !== 't' || !timestamp?.length) {
throw error
}
timestamp = Number(timestamp)
if (isNaN(timestamp)) {
throw error
}
const [s, signature] = signatureParts.split('=')
if (s !== 's' || !signature?.length) {
throw error
}
return { timestamp, signature }
}
Verify the Timestamp
The timestamp (t) can be verified to ensure it’s a recent request and not a
potentially delayed replay attack. Some leeway should be allowed in case the
clocks on either end of the request aren’t quite in sync.
Here’s our service with 5 minutes of leeway:
/* Services */
function verifyTimestamp({ timestamp, leewayMinutes = 5 }) {
const diff = Math.abs(Date.now() - timestamp * 1000)
const diffMinutes = Math.ceil(diff / (1000 * 60))
return leewayMinutes >= diffMinutes
}
Verify the Signature
The signature (s) can be verified to ensure the recipient and payload haven’t
been tampered with. This is done by rebuilding the signature – an HMAC digest
using SHA-256 that’s Base64-encoded – with a secret that only Lens and you are
privy to. (The Truepic team shared this secret with you when we set up your
webhook earlier.)
To create the message to sign, join your webhook’s URL together with the
timestamp (t) and the raw request body using a comma (,). It’s important
to use the raw request body (a string) before it’s parsed as JSON, as different
languages/frameworks can parse/stringify JSON in subtly different ways, which
could result in different signatures.
Next, sign the {{url}},{{timestamp}},{{body}} message with the secret and
compare the Base64-encoded signatures with a constant-time algorithm to prevent
timing attacks.
Here’s how that looks in our service that relies on Node.js’s crypto module
for all of the heavy lifting:
/* Dependencies */
import { createHmac, timingSafeEqual } from 'crypto'
/* Services */
function verifySignature({ url, timestamp, body, secret, signature }) {
const comparisonSignature = createHmac('sha256', secret)
comparisonSignature.update([url, timestamp, body].join(','))
return timingSafeEqual(
Buffer.from(comparisonSignature.digest('base64'), 'base64'),
Buffer.from(signature, 'base64')
)
}
Putting It All Together
Now that we have services to parse and verify the truepic-signature header,
let’s update our webhook route to put it all together:
/* Constants */
const WEBHOOK_URL = `http://localhost:${PORT}/webhook`
const WEBHOOK_SECRET = 'secret'
/* Routes */
app.post(
'/webhook',
express.raw({
type: 'application/json',
}),
(req, res, next) => {
const { timestamp, signature } = parseSignatureHeader(
req.header('truepic-signature')
)
const isTimestampVerified = verifyTimestamp({ timestamp })
if (!isTimestampVerified) {
console.warn('Invalid timestamp')
return res.sendStatus(200)
}
const isSignatureVerified = verifySignature({
url: WEBHOOK_URL,
timestamp,
body: req.body.toString(),
secret: WEBHOOK_SECRET,
signature,
})
if (!isSignatureVerified) {
console.warn('Invalid signature')
return res.sendStatus(200)
}
next()
},
(req, res) => {
const body = JSON.parse(req.body.toString())
console.log(`Processing webhook: ${body.type}`)
console.dir(body, { depth: null })
res.sendStatus(200)
}
)
Delete the Processed Capture (Optional)
The last piece of code we’re going to add is a service that deletes the processed capture and all associated files from Lens. This is optional, as we auto-delete captures 30 days after they’re processed (or any configurable length of time you desire). But it’s good hygiene to not leave potentially sensitive data on a third-party system like Lens any longer than necessary, so we offer an API to delete a capture as soon as you’re ready.
Here’s how that service looks:
/* Services */
async function deleteFromLens(id) {
const response = await fetch(`https://lens-api.truepic.com/captures/${id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${API_KEY}`,
},
})
return response.ok
}
Where this should be called from depends on how you process the file. If you
have everything you need by the end of the webhook request, you could slot it in
just before replying with the 200 status:
// ...
await deleteFromLens(body.data.id)
res.sendStatus(200)
If you end up with a more complex processing pipeline that consists of asynchronous background jobs, however, it probably makes more sense to wait until the final step of that sequence. The general principle is this: delete once you’ve stored all of the data and files on your end. That’s when it’s safe to remove it from Lens.
Wrapping Up
Phew! We’ve made it to the end. Hopefully the code and concepts we’ve walked through give you the confidence and understanding to translate into the language/framework of your backend. Even though we’ve written a lot of words in this guide, the combined code comes in well under 200 lines. We think it’s straight-forward, but when it’s not, we’re happy to help with any hurdles or questions along the way!