Building an OCR as a Service

James PerkinsJames Perkins
9 min read

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.

create root key

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.

create

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.

dashboard


Project Walkthrough

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

  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

Body:

NameTypeDescription
emailstringEmail address to sign up with
namestringName of user

Returns:

NameTypeDescription
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

Headers:

NameTypeDescription
BearerstringAPI key in Bearer auth

Body:

NameTypeDescription
sampleFilefileImage to use

Returns:

NameTypeDescription
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

Headers:

NameTypeDescription
BearerstringAPI key in Bearer auth

Body:

NameTypeDescription
imageBase64stringBase64 encoded image

Returns:

NameTypeDescription
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

Body:

NameTypeDescription
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.

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

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 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
Free100/month6/min
$10/month10,000/month100/min
$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.

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.

0
Subscribe to my newsletter

Read articles from James Perkins directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

James Perkins
James Perkins