Building an OCR as a Service

Learn how to use Unkey to make an Optical Character Recognition (OCR) API as a service

Written by

Wilfred Almeida

Published on

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.


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 is done via an npm package tesseract.js. Following is its function which takes in the image and recognizes English and Spanish languages.

 1const doOcr = async (image) => {
 2  try {
 3    // It detects English and Spanish
 4    const { data } = await Tesseract.recognize(image, "spa+eng", {
 5      logger: (m) => console.log(m),
 6    });
 8    return { data: data, error: null };
 9  } catch (error) {
10    console.log(error);
11    return { data: null, error: error };
12  }

Endpoints Explained

  1. /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


emailstringEmail address to sign up with
namestringName of user


keystringAPI Key
keyIdstringAPI Key ID
  1. /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


BearerstringAPI key in Bearer auth


sampleFilefileImage to use


textstring, nullOCR Results
errorstring, nullError if any
  1. /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


BearerstringAPI key in Bearer auth


imageBase64stringBase64 encoded image


textstring, nullOCR Results
errorstring, nullError if any
  1. /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


emailstringEmail address of the user
transactionIdstringImaginary transaction id
apiKeyIdstringId 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.

 1// /signUp endpoint"/signUp", async (req: Request, res: Response) => {
 3  const { name = "John Doe", email = "" } = req.body;
 5  // Imaginary name and email validation
 7  const myHeaders = new Headers();
 8  myHeaders.append("Authorization", `Bearer ${process.env.UNKEY_ROOT_KEY}`);
 9  myHeaders.append("Content-Type", "application/json");
11  const raw = JSON.stringify({
12    apiId: process.env.UNKEY_API_ID,
13    prefix: "ocr",
14    byteLength: 16,
15    ownerId: email,
16    meta: {
17      name: name,
18      email: email,
19    },
20    expires: + 2592000000 // 30 days from now
21    ratelimit: {
22      duration: 1000,
23      limit: 1,
24    },
25  });
28  const createKeyResponse = await fetch(
29    "",
30    {
31      method: "POST",
32      headers: myHeaders,
33      body: raw,
34      redirect: "follow",
35    },
36  );
37  const createKeyResponseJson = await createKeyResponse.json();
39  if (createKeyResponseJson.error)
40    return res
41      .status(400)
42      .json({ error: createKeyResponseJson.error, keys: null });
44  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 like user_, 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 Limiting

    • limit: The number of requests allowed in the given time

    • refill: The number of requests to refill in the given time

    • refillInterval: 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.

 1import { Request, Response, NextFunction } from "express";
 3// An Express Middleware
 4const verifyApiKey = async (
 5  req: Request,
 6  res: Response,
 7  next: NextFunction
 8) => {
 9  const authHeader = req.headers.authorization;
10  if (authHeader) {
11    // Get the token from request headers
12    const token = authHeader.split(" ")[1].trim();
14    try {
15      const myHeaders = new Headers();
16      myHeaders.append("Content-Type", "application/json");
18      const raw = JSON.stringify({
19        key: token,
20      });
22      const verifyKeyResponse = await fetch(
23        "",
24        {
25          method: "POST",
26          headers: myHeaders,
27          body: raw,
28          redirect: "follow",
29        }
30      );
31      const verifyKeyResponseJson = await verifyKeyResponse.json();
33      if (
34        !verifyKeyResponseJson.valid &&
35        verifyKeyResponseJson.code === "RATE_LIMITED"
36      )
37        return res.status(429).json({ message: "RATE_LIMITED" });
39      if (!verifyKeyResponseJson.valid)
40        return res.status(401).json({ message: "Unauthorized" });
42      next();
43    } catch (err) {
44      console.log("ERROR: ", err);
45      return res.status(401).json({ message: "Unauthorized" });
46    }
47  } else {
48    return res.status(401).json({ message: "Unauthorized" });
49  }
52export default verifyApiKey;

Key verification response

 2    "valid": true,
 3    "ownerId": "",
 4    "meta": {
 5        "email": "",
 6        "name": "John Doe"
 7    },
 8    "expires": + 2592000000 // 30 days from now
 9    "ratelimit": {
10        "limit": 1,
11        "remaining": 0,
12        "reset": 1690350175693
13    }

Let's understand the response in detail:

  • valid: This is either true or false 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. The limit param tells us how many more requests the user has left. The reset 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:

PriceRequestsRate Limit
$100/month100,000/monthNo 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."/upgradeUser", async (req: Request, res: Response) => {
 2  const { transactionId, email, apiKeyId } = req.body;
 4  // Imaginary transactionId and email validation.
 5  // Let's imagine the user upgraded to a paid plan.
 6  // Now we have to increase the usage quota of the user.
 7  // We can do that by updating the key.
 9  const myHeaders = new Headers();
10  myHeaders.append("Content-Type", "application/json");
11  myHeaders.append("Authorization", `Bearer ${process.env.UNKEY_ROOT_KEY}`);
13  const raw = JSON.stringify({
14    keyId: apiKeyId,
15    ratelimit: {
16      async: true, // Fast rate limiting
17      duration: 1000, // Rate limit duration
18      limit: 100, // Maximum allowed requests for the user
19    },
20  });
22  const updateKeyRequest = await fetch(
23    "",
24    {
25      keyId: "example_key"
26      method: "PUT",
27      headers: myHeaders,
28      body: raw,
29      redirect: "follow",
30    }
31  );
33  if (updateKeyRequest.status !== 200)
34    return res.status(400).json({ message: "Something went wrong" });
36  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.


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.

Protect your API.
Start today.

2500 verifications and 100K successful rate‑limited requests per month. No CC required.