Building an OCR as a Service
Unkey is an open-source API key management tool with real-time API key creation, updating, and verification.
In this blog, we'll take a look at how we can use Unkey to make an Optical Character Recognition (OCR) API as a service. The OCR API takes in an image and returns the textual characters present in it.
You can find the complete source code on GitHub.
Pre-requisites
Signup and get started with the Unkey dashboard. As soon as you create your account you will be asked to create your workspace. Give it a name, and a slug. The name is shown only to you and not to your users.
Then you can create your first API which allows you to track usage as well as segment keys, the name you choose is also not visible to users.
The admin dashboard gives you access to several features in a simple-to-use interface. You can create new APIs, issue keys, revoke keys and see usage stats. You can also invite other users to your account so that they can manage your APIs.
Project Walkthrough
The project is an Express API in NodeJS.
It takes an image either as a file or base64 and does OCR on it and returns the resulting text.
OCR
OCR is done via an npm package tesseract.js. Following is its function which takes in the image and recognizes English and Spanish languages.
const doOcr = async (image) => {
try {
// It detects English and Spanish
const { data } = await Tesseract.recognize(image, "spa+eng", {
logger: (m) => console.log(m),
});
return { data: data, error: null };
} catch (error) {
console.log(error);
return { data: null, error: error };
}
};
Endpoints Explained
/signup
: Sign up for an API key. Returns a JSON object with the API key.
It validates the email and provisions and returns an API key. The key is then used to authenticate the OCR endpoints.
Type: POST
Body:
Name | Type | Description |
string | Email address to sign up with | |
name | string | Name of user |
Returns:
Name | Type | Description |
key | string | API Key |
keyId | string | API Key ID |
/upload
: Upload an image to perform OCR. Returns a JSON object with the OCR results. It uses the API key to authenticate the request. It then performs OCR on the image and returns the results.
Type: POST
Headers:
Name | Type | Description |
Bearer | string | API key in Bearer auth |
Body:
Name | Type | Description |
sampleFile | file | Image to use |
Returns:
Name | Type | Description |
text | string, null | OCR Results |
error | string, null | Error if any |
/uploadBase64
: Upload a base64 encoded image to perform OCR. Returns a JSON object with the OCR results. It uses the API key to authenticate the request. It then performs OCR on the image and returns the results.
Type: POST
Headers:
Name | Type | Description |
Bearer | string | API key in Bearer auth |
Body:
Name | Type | Description |
imageBase64 | string | Base64 encoded image |
Returns:
Name | Type | Description |
text | string, null | OCR Results |
error | string, null | Error if any |
/upgradeUser
: Upgrade a user to a paid plan. It validates an imaginary transaction id and then upgrades the user. It increases the usage limit of the user based on the subscription the user has purchased.
Type: POST
Headers: None
Body:
Name | Type | Description |
string | Email address of the user | |
transactionId | string | Imaginary transaction id |
apiKeyId | string | Id of the API key to be updated. It is returned when a key is created. |
Returns: None
Understanding Unkey API key authentication
Unkey uses fast and efficient on-the-edge systems to verify a key. It adds less than 40ms to our requests.
The key is provisioned per user in the /signup
route. The user can then use the key to authenticate requests to the OCR endpoints.
// /signUp endpoint
app.post("/signUp", async (req: Request, res: Response) => {
const { name = "John Doe", email = "john@example.com" } = req.body;
// Imaginary name and email validation
const myHeaders = new Headers();
myHeaders.append("Authorization", `Bearer ${process.env.UNKEY_ROOT_KEY}`);
myHeaders.append("Content-Type", "application/json");
const raw = JSON.stringify({
apiId: process.env.UNKEY_API_ID,
prefix: "ocr",
byteLength: 16,
ownerId: email,
meta: {
name: name,
email: email,
},
expires: Date.now() + 2592000000 // 30 days from now
ratelimit: {
duration: 1000,
limit: 1,
},
});
const createKeyResponse = await fetch(
"https://api.unkey.dev/v1/keys.createKey",
{
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow",
},
);
const createKeyResponseJson = await createKeyResponse.json();
if (createKeyResponseJson.error)
return res
.status(400)
.json({ error: createKeyResponseJson.error, keys: null });
return res.status(200).json({ keys: [createKeyResponseJson], error: null });
});
The user then has to send the API key in the Authorization
header as a Bearer
token. To verify the key, a simple API call is made to Unkey. More on this further ahead.
To verify the key, we've made a middleware in the middleware.ts
file.
Key Creation
As shown in the above code block, the key is created in the /signup
route in index.ts
.
Its params are explained in detail in the official docs
Following is a description of the params used in this example:
apiId
: The API ID to create the key for. You create this API in the Unkey dashboard.prefix
: The prefix to use for the key. Every key is prefixed with this. This is useful to identify the key's purpose. For eg. we can have prefixes likeuser_
,admin_
,service_
,staging_
,trial_
,production_
etc.byteLength
: The byte length used to generate the key determines its entropy as well as its length. Higher is better, but keys become longer and more annoying to handle. The default is 16 bytes or 2^128 possible combinations.ownerId
: This can be any string. In this example, we're using the user's email address as the id. By doing this we'll be able to verify the appropriate owner of the key.meta
: Any metadata information we want to store with the key. In this example, we're storing the user's name and email.expires
: Keys can be auto-expired by providing a UNIX timestamp in milliseconds. Once keys expire they will automatically be deleted and are no longer valid.rateLimit
: Keys can be rate limited by certain parameters. This is extremely beneficial as it prevents abuse of our API. The rate limit is enforced on the edge, so it's extremely fast and efficient. The rate limit params we've used in this example are:type
: Type of the rate limit. Read more: Rate Limitinglimit
: The number of requests allowed in the given timerefill
: The number of requests to refill in the given timerefillInterval
: The interval by which the requests are refilled
In the rate limit set in the /signUp
route, the user is on a trial plan and is allowed 1 request every 10 seconds.
Key Verification
The API key verification is done in the middleware.ts
file. We're making an API call to the Unkey API to verify the key by passing the key
into the request body.
import { Request, Response, NextFunction } from "express";
// An Express Middleware
const verifyApiKey = async (
req: Request,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers.authorization;
if (authHeader) {
// Get the token from request headers
const token = authHeader.split(" ")[1].trim();
try {
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const raw = JSON.stringify({
key: token,
});
const verifyKeyResponse = await fetch(
"https://api.unkey.dev/v1/keys.verifyKey",
{
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow",
}
);
const verifyKeyResponseJson = await verifyKeyResponse.json();
if (
!verifyKeyResponseJson.valid &&
verifyKeyResponseJson.code === "RATE_LIMITED"
)
return res.status(429).json({ message: "RATE_LIMITED" });
if (!verifyKeyResponseJson.valid)
return res.status(401).json({ message: "Unauthorized" });
next();
} catch (err) {
console.log("ERROR: ", err);
return res.status(401).json({ message: "Unauthorized" });
}
} else {
return res.status(401).json({ message: "Unauthorized" });
}
};
export default verifyApiKey;
Key verification response
{
"valid": true,
"ownerId": "john@example.com",
"meta": {
"email": "john@example.com",
"name": "John Doe"
},
"expires": Date.now() + 2592000000 // 30 days from now
"ratelimit": {
"limit": 1,
"remaining": 0,
"reset": 1690350175693
}
}
Let's understand the response in detail:
valid
: This is eithertrue
orfalse
telling us if the key is valid or not.expires
: The UNIX timestamp in milliseconds when the key expires. Here, we've given the user a 30 days trial. So the key expires in 30 days.ratelimit
: Currently, the user is limited to 1 request every 10 seconds. Thelimit
param tells us how many more requests the user has left. Thereset
tells us the time when the requests will be refilled. We can use this to show the user how much time is left before they can make another request.
API as a Service Subscription Model
Suppose we want to offer an API as a paid service with subscription tiers. We can easily implement that using Unkey.
Consider the following plans:
Price | Requests | Rate Limit |
Free | 100/month | 6/min |
$10/month | 10,000/month | 100/min |
$100/month | 100,000/month | No limit |
Suppose the user upgrades to the $10/month plan, then, we can update the rate limit of the key to allow 100 requests per minute. Following is the /upgradeUser
endpoint that does it. In the following snippet, we're updating the rate limit parameters for the user key.
app.post("/upgradeUser", async (req: Request, res: Response) => {
const { transactionId, email, apiKeyId } = req.body;
// Imaginary transactionId and email validation.
// Let's imagine the user upgraded to a paid plan.
// Now we have to increase the usage quota of the user.
// We can do that by updating the key.
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", `Bearer ${process.env.UNKEY_ROOT_KEY}`);
const raw = JSON.stringify({
keyId: apiKeyId,
ratelimit: {
async: true, // Fast rate limiting
duration: 1000, // Rate limit duration
limit: 100, // Maximum allowed requests for the user
},
});
const updateKeyRequest = await fetch(
"https://api.unkey.dev/v1/keys.updateKey",
{
keyId: "example_key"
method: "PUT",
headers: myHeaders,
body: raw,
redirect: "follow",
}
);
if (updateKeyRequest.status !== 200)
return res.status(400).json({ message: "Something went wrong" });
return res.status(200).json({ message: "User upgraded successfully" });
});
In the set rate limiting, users are granted 100 requests every minute. If the number of requests surpasses 100 within a single minute, a rate limit will be imposed. However, this limit is reset and replenished every minute, giving users a fresh allocation of 100 requests to use again.
Conclusion
This tutorial aims to present to you an end-to-end use case scenario of how Unkey can fit and fulfill your requirements. Join our Discord and get in touch. Share what's on your mind. Give the Unkey repo a star on GitHub and keep building.
Subscribe to my newsletter
Read articles from James Perkins directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by