Skip to content
Developer API

KovaPDF API reference

The KovaPDF API gives your server the same three signature tools the website has: send a PDF to other people for signature and follow it with webhooks, sign a PDF with your own certificate, and verify the signatures in any PDF. It is a plain REST API, JSON over HTTPS. No SDK needed.

Looking for one error code? See every error code. What changed and when: the changelog. The rules for using the API: the API terms.

Quick start

  1. Sign in and create a key on Dashboard → API keys. Copy it straight away: it is shown once. Start with a free test key.
  2. Call the API with the key in the Authorization header.
export KOVAPDF_KEY="kova_test_..."

curl https://api.kovapdf.com/api/v1/me \
  -H "Authorization: Bearer $KOVAPDF_KEY"

All endpoints live under https://api.kovapdf.com/api/v1. Requests with files are multipart/form-data; everything else is JSON. Responses are JSON, except the endpoints that return a PDF.

The three calls that take a file also take a JSON body with the file as base64, which suits tools that build flows without code: { "file": { "name": "a.pdf", "contentBase64": "JVBERi0..." } }. Use standard base64 with padding, with no data: prefix and no spaces or line breaks. The file goes through exactly the same checks and limits as an upload, and the call counts the same.

Authentication

Every request carries Authorization: Bearer <key>. A key acts as the account that made it. Requests you send are yours, signed-in cookies are ignored, and anything you could not do in the browser you cannot do with a key either.

  • Test keys start with kova_test_ and live keys with kova_live_ (see Test keys and live keys). Keys made before those two existed start with kova_. We store only a SHA-256 hash of each key, so a lost key cannot be shown again. Revoke it and create a new one.
  • Keep keys on your server. Never put one in a web page, a mobile app or a public repository.
  • Revoking a key takes effect on the next request. You can have up to 10 active keys.
  • When you make a key you choose what it may do (its scopes) and, if you like, a date it stops working: after 1 to 366 days, or never. We email you a week before a key expires. An expired key gets 401 expired_api_key.
  • In a Team or Business workspace, a key works on the workspace's signature requests. A member's own key stops the moment they leave the workspace. The Owner or an Admin can also make a key that belongs to the workspace itself: it acts for the workspace's Owner and keeps working when the person who made it leaves. The dashboard lists who made each one and when it was last used.

Test keys and live keys

This is a test key. It runs 10 tasks within 14 days. Every email it sends goes only to your own address, never to anyone else, and every file is marked TEST.

  • Test key, kova_test_. You make it yourself in your account once your email address is verified. You can hold one at a time. It runs 10 tasks within 14 days of your first test key, then it stops working: calls get 401 test_key_used_up or 401 test_key_expired, with a requestAccessUrl.
  • Every signing email a test request sends goes to the verified address on your account, whatever addresses the request names, with TEST in the subject. Every PDF a test key makes is stamped TEST on every page (for digital signing, before signing, so the signature covers the stamp). Test signature requests are deleted 7 days after they were made.
  • Every answer to a test key carries the headers KovaPDF-Mode: test and KovaPDF-Test-Tasks-Left: n, and every JSON object it gets back has "mode": "test".
  • Live key, kova_live_. You make it in the same place, once we have turned API access on for your account. The email that says access is on never contains a key. Without API access, a call with a live key gets 402 api_access_required with a requestAccessUrl. Answers to a live key carry KovaPDF-Mode: live.
  • An account with API access also gets a number of test tasks every calendar month, which never count against its live limit. When they are used, test calls get 429 test_monthly_limit until the 1st.
  • Older keys, kova_. Keys made before test and live keys existed keep working for 30 days from the change, and their answers carry a KovaPDF-Key-Notice header with the date. After that they work only for an account with API access.

To ask for API access, use the request form.

HTTP/1.1 201 Created
KovaPDF-Mode: test
KovaPDF-Test-Tasks-Left: 9
{ "id": "4f0c…", "status": "IN_PROGRESS", …, "mode": "test" }

Tasks

One task is one signature request made, one verification or one digital signing. Reading, listing, downloading and status calls are not tasks, and a call that fails is not counted.

EndpointTask?
POST /sign-requestsYes, one task per request made
POST /verifyYes, one task per PDF checked
POST /digital-signYes, one task per PDF signed
GET /me, GET /sign-requests, GET /sign-requests/:id, GET /sign-requests/:id/document, POST /sign-requests/:id/remind, POST /sign-requests/:id/cancelNo
  • A test task is set aside when the call starts, so ten calls at once can never run eleven tasks, and given back when the call fails.
  • Live access has a monthly task limit or a pack of tasks. At the limit the answer is 429 monthly_limit (with Retry-After until the 1st of next month) or 429 credits_used. Nothing is charged on top. Your account gets one email at 80 percent and one at 100 percent.
  • While a live call runs a task, KovaPDF-Tasks-Left says how many tasks are left after it.

Idempotency-Key

Every POST takes an Idempotency-Key header: 1 to 255 visible characters, and a UUID is a good choice. Make a new one for each new request and store it with your own record, so a retry sends the same one.

  • The first request with a key runs as usual and its answer is kept for 24 hours, for that API key.
  • A repeat with the same key and the same body (the same fields and the same files) gets that first answer back with Idempotency-Replayed: true. Nothing runs again: no second signature request, no second email, no second task.
  • The same key with a different body gets 422 idempotency_key_reused. While the first request is still running, a repeat gets 409 idempotency_in_progress with Retry-After: 2. A key that is empty, too long or has other characters gets 400 invalid_idempotency_key.
  • An answer that a retry could change (5xx, 409, 429) is not kept, so a retry after it runs again.
  • The kept answer can hold a signed PDF from /digital-sign. It is deleted after 24 hours.
curl https://api.kovapdf.com/api/v1/sign-requests \
  -H "Authorization: Bearer $KOVAPDF_KEY" \
  -H "Idempotency-Key: 6f1c2a8e-3d4b-4f7a-9a51-2b8f0c9e7d13" \
  -F "file=@agreement.pdf" -F "spec=<spec.json"

# The same call again, after a timeout:
HTTP/1.1 201 Created
Idempotency-Replayed: true

IP allow list

A key can be limited to the addresses your servers call from: up to 20 addresses or ranges, IPv4 or IPv6, such as 203.0.113.7, 203.0.113.0/24 or 2001:db8::/32. Set it when you make the key, or change it later in your account. Empty means any address. A range that covers the whole internet is refused, so nobody thinks they set a limit when they did not.

A call from an address that is not on the list gets 403 ip_not_allowed, with the address we saw in the message. Live access can also come with a list agreed for your account; then a live call must match that list too.

HTTP/1.1 403 Forbidden
{ "error": "This API key works only from the addresses on its allow list, and 198.51.100.4 is not on it. Change the list in your account.", "code": "ip_not_allowed" }

Scopes

Give each key only what it needs. A call outside a key's scopes gets 403 insufficient_scope, with the scope it needed in required, the key's scopes in granted, and a WWW-Authenticate header that names it. GET /me works with any key. Keys made before scopes existed have all four.

ScopeWhat it allowsEndpoints
sign-requests:readRead signature requests. List your signature requests, read one with its signers and audit trail, and download its documents.GET /sign-requests
GET /sign-requests/:id

GET /sign-requests/:id/document
sign-requests:writeSend and manage signature requests. Send new signature requests, remind signers and cancel requests.POST /sign-requests
POST /sign-requests/:id/remind

POST /sign-requests/:id/cancel
verifyVerify signatures. Check the digital signatures in a PDF and get the report.POST /verify
digital-signSign with a certificate. Sign a PDF with your own .pfx or .p12 certificate.POST /digital-sign
toolsWork on files. Upload files and run tools on them: compress, merge, split, organize, convert, read text from scans, redact and convert Bangla or Hindi text.POST /files
GET /files/:id

POST /jobs

GET /jobs/:id
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="KovaPDF API", error="insufficient_scope", scope="sign-requests:write"
{
  "error": "This API key cannot do this. It needs the "sign-requests:write" permission (Send and manage signature requests). Make a key with that permission in your dashboard.",
  "code": "insufficient_scope",
  "required": "sign-requests:write",
  "granted": ["sign-requests:read"]
}

Errors

Every error has a normal HTTP status and a JSON body with a human-readable error and a stable machine-readable code. Switch on code; the wording of error may change.

HTTP/1.1 401 Unauthorized
{ "error": "This API key has been revoked. Create a new one from your dashboard.", "code": "revoked_api_key" }
StatusMeaningExample codes
400The request is incomplete or not valid.invalid_request (with issues), NO_FILE, BAD_PDF, invalid_base64, empty_file, SIGNER_EMAIL, FIELD_BOUNDS, WRONG_PASSWORD
400The Idempotency-Key header is not valid.invalid_idempotency_key
401No key, an unknown key, a revoked key or an expired key, or a test key whose test has ended.missing_api_key, invalid_api_key, revoked_api_key, expired_api_key, test_key_used_up (all its test tasks are used), test_key_expired (its 14 days are over)
402A live call from an account without API access.api_access_required (with requestAccessUrl)
403The key or your account may not do this.insufficient_scope (the key lacks the scope; see Scopes), two_factor_required (the workspace asks for two-step sign-in), workspace_inactive, EMAIL_NOT_VERIFIED, tool_blocked, account_inactive, FREE_SIGNER_LIMIT (more people than the Free plan allows on one request), key_paused (the key paused itself after a burst of tasks or too many bounced emails, or we paused it), ip_not_allowed (see IP allow list)
404Not found, or not yours.NOT_FOUND, NO_FINAL, not_found
409The document's state does not allow it, or the same Idempotency-Key is still running.CLOSED, SIGNATURE_FIELD_ALREADY_SIGNED, idempotency_in_progress
413The file is too large.file_too_large, FILE_TOO_LARGE
422The file could not be processed, or an Idempotency-Key was reused for a different request.invalid_pdf, encrypted_pdf, idempotency_key_reused
429A limit was reached. Wait for Retry-After seconds.rate_limited, daily_limit (the key's calls for today are used; it opens again at midnight UTC), limit_reached, DAILY_LIMIT, FREE_MONTHLY_LIMIT (the Free plan's signature requests for this calendar month are used; they come back on the 1st, or upgrade), FREE_RECIPIENT_DAILY (on the Free plan, one of the people has already had a lot of signature emails today; try again tomorrow, or upgrade), monthly_limit (your account's monthly tasks are used; nothing is charged on top), credits_used (your pack of tasks is used), test_monthly_limit (this month's test tasks of an account with API access are used)
5xxOur fault. Safe to retry with backoff.service_unavailable

Every code, with what it means and what to do: API error codes.

Limits

  • 60 requests a minute per key. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers; a 429 carries Retry-After.
  • 30 signature requests sent an hour per key. Reminders count too.
  • A daily cap per key, counted in UTC days: the lower of the cap you set on the key in the dashboard and your plan's cap for one key. Paid plans have higher caps than Free. The dashboard shows each key's calls per day and today's cap. When it is reached, calls get 429 daily_limit until midnight UTC, and those refused calls do not count.
  • Every 429 has a Retry-After header in seconds and a plain sentence in error. Wait that long, then try again.
  • Your account's own limits still apply. They are the same as in the browser: your plan's allowance, signature requests included, and any tool an administrator has switched off.
  • Tasks. A test key runs 10 tasks within 14 days. Live access has a monthly task limit (429 monthly_limit, with Retry-After) or a pack of tasks (429 credits_used). See Tasks.
  • A key that pauses itself. A key that runs far more tasks in 10 minutes than an integration normally does, or whose signing emails bounce too often, pauses itself and gets 403 key_paused. Write to us and we look at it with you.
  • Files up to 15 MB for signature requests. For signing and verifying, the same size and page limits as the website.

Request signatures

Send a PDF to one or more people. Each gets an email with a private link, signs in their browser, and everyone receives the finished document, sealed and with a certificate of completion. Your account needs a verified email address. The request goes out in your name.

POST/sign-requests

Multipart form with two parts: file (the PDF) and spec (a JSON string describing the request).

curl https://api.kovapdf.com/api/v1/sign-requests \
  -H "Authorization: Bearer $KOVAPDF_KEY" \
  -F "file=@agreement.pdf" \
  -F 'spec={
    "title": "Services Agreement",
    "message": "Please sign by Friday.",
    "sequential": true,
    "expiresInDays": 14,
    "reminderEveryDays": 3,
    "signers": [
      { "name": "John Smith",   "email": "john@example.com",  "order": 1 },
      { "name": "Emily Carter", "email": "emily@example.com", "order": 2, "accessCode": "NW2026" }
    ],
    "fields": [
      { "signer": 0, "kind": "SIGNATURE", "page": 1, "x": 72,  "y": 90, "width": 180, "height": 48 },
      { "signer": 0, "kind": "DATE",      "page": 1, "x": 72,  "y": 50, "width": 180, "height": 16 },
      { "signer": 1, "kind": "SIGNATURE", "page": 1, "x": 340, "y": 90, "width": 180, "height": 48 }
    ]
  }'
spec fieldTypeNotes
titlestringRequired, up to 150 characters. The email subject.
messagestringOptional note in the invitation, up to 2,000 characters.
sequentialbooleanRequired. true: one after another by order. false: everyone at once.
expiresInDaysintegerOptional, 1 to 60 days. Without it, the usual expiry applies.
reminderEveryDaysintegerOptional automatic reminders, every 1 to 14 days.
signers[]arrayname, email, order (from 1), optional accessCode (4 to 12 letters or digits, sent to them separately by you), optional role: SIGNER (default), APPROVER, WITNESS (with witnessFor: the index of the signer they witness) or CC.
fields[]arraysigner (index into signers), kind (SIGNATURE, INITIALS, STAMP, NAME, EMAIL, COMPANY, TITLE, DATE, DATE_PICK, TEXT, NUMBER, CHECKBOX, DROPDOWN, RADIO, ATTACHMENT, FORMULA, NOTE, APPROVAL), page (from 1), x, y, width, height in PDF points from the page's lower-left corner, optional required and label. EMAIL is filled in with the recipient's own address. DATE is the day they sign; DATE_PICK is a date they choose. NOTE shows words to the recipient and has nothing to fill in. APPROVAL is the Approve or Decline box, and only an APPROVER can have it; an approver has only APPROVAL and NOTE fields. Every signer and witness needs at least one SIGNATURE field. Optional prefill fills a TEXT, NUMBER, DATE_PICK, COMPANY or TITLE field in advance. Optional options: choices and defaultValue for a dropdown; group and choice for each radio button (two or more per group); validate (number, email, date with dateFormat) and maxLength for text; dateFormat for a DATE_PICK; note for a NOTE; formula such as [Qty] * [Price] over the same signer's number fields, with decimals; appendToPdf for an attachment; and on any field but a signature or approval showIf: { field, value }, where field is the index of the same signer's checkbox ("true"/"false"), dropdown, radio button or text field.

Returns 201 with the request, the same object as GET /sign-requests/:id. Each signer in it has a link: their private signing link, if you want to show it in your own app as well as by email.

GET/sign-requests

Your requests, newest first (up to 200), with each signer's status.

curl https://api.kovapdf.com/api/v1/sign-requests -H "Authorization: Bearer $KOVAPDF_KEY"

GET/sign-requests/:id

One request: status (IN_PROGRESS, COMPLETED, DECLINED, CANCELLED, EXPIRED), its signers with their status and times, and the full audit trail in events.

{
  "id": "4f0c…",
  "title": "Services Agreement",
  "status": "IN_PROGRESS",
  "sequential": true,
  "expiresAt": "2026-10-06T09:12:00.000Z",
  "hasFinal": false,
  "signers": [
    { "id": "aa57…", "name": "John Smith", "email": "john@example.com", "order": 1,
      "status": "SIGNED", "signedAt": "2026-09-22T09:30:11.000Z", "link": "https://…/sign/…" },
    { "id": "c1d2…", "name": "Emily Carter", "email": "emily@example.com", "order": 2,
      "status": "SENT", "isTurn": true, "link": "https://…/sign/…" }
  ],
  "events": [ { "at": "…", "type": "CREATED", "text": "Created and sent by the sender (2 signer(s), in order)" } ]
}

POST/sign-requests/:id/remind

Emails the people whose turn it is again. Optional JSON body { "signerId": "…" } to remind one person. Each person can be reminded once every 12 hours; anyone reminded more recently is listed in skipped.

curl -X POST https://api.kovapdf.com/api/v1/sign-requests/$ID/remind \
  -H "Authorization: Bearer $KOVAPDF_KEY" -H "Content-Type: application/json" -d '{}'
# { "sent": 1, "skipped": [] }

POST/sign-requests/:id/cancel

Closes a request that is still waiting for signatures. The signing links stop working.

curl -X POST https://api.kovapdf.com/api/v1/sign-requests/$ID/cancel -H "Authorization: Bearer $KOVAPDF_KEY"

GET/sign-requests/:id/document?version=original|current|final

Downloads a PDF. original (default) is the file you uploaded; current shows the signatures collected so far; final is the sealed, signed document with its certificate of completion, and exists once everyone has signed (404 NO_FINAL before that).

curl -o signed.pdf "https://api.kovapdf.com/api/v1/sign-requests/$ID/document?version=final" \
  -H "Authorization: Bearer $KOVAPDF_KEY"

Digital signature

POST/digital-sign

Signs a PDF with your own certificate (PAdES) and returns the signed PDF. Multipart fields: file (the PDF), certificate (a .pfx/.p12), password, and any options below. The certificate and password are used once, in memory: they are never stored and never logged.

curl https://api.kovapdf.com/api/v1/digital-sign \
  -H "Authorization: Bearer $KOVAPDF_KEY" \
  -F "file=@contract.pdf" \
  -F "certificate=@me.pfx" \
  -F "password=$PFX_PASSWORD" \
  -F "reason=Approved" -F "location=London" \
  -F "placement=bottom-right" \
  -o contract-signed.pdf
OptionDefaultNotes
reason, locationNoneShown in the signature and its stamp.
hashAlgorithmSHA-256or SHA-512.
visibletruefalse for a signature with no stamp on the page.
placementbottom-rightbottom-left, top-right, top-left; on the last page unless page is set.
page, rectNoneExact position: rect is a JSON array [x1,y1,x2,y2] in points from the lower-left of the page as displayed. Overrides placement.
fieldNameNoneSign into an existing empty signature field.
signatureImageNoneA PNG, JPEG or WebP file part to draw in the stamp.
certifynoneno-changes, form-filling, form-filling-and-comments: a certification signature (first signature only).
lockDocumentfalseLock the form fields after signing.
timestamptrueAdd a trusted timestamp.
ltv, archiveTimestampfalseLong-term validation data (needs timestamp), and an archive timestamp (needs ltv).
showName, showDate, showReason, showLocation, showLabelstrueWhat the visible stamp shows.

The response is the PDF. Headers describe what it verifiably has: X-Signature-Profile (e.g. PAdES-B-T), X-Signature-Timestamp, X-Signature-LTV, X-Signature-Hash, X-Signature-Certify, X-Signature-Signer (URL-encoded) and X-Signature-Status (base64 JSON). A wrong password is 400 WRONG_PASSWORD; an expired certificate is 400 CERTIFICATE_EXPIRED.

Verify signatures

POST/verify

Checks every signature in a PDF (integrity, certificate, chain, revocation, timestamp and whether the document changed after signing) and returns the same report the Verify tool shows.

curl https://api.kovapdf.com/api/v1/verify \
  -H "Authorization: Bearer $KOVAPDF_KEY" \
  -F "file=@signed.pdf"
{
  "fileSha256": "9c1e…",
  "pageCount": 3,
  "signatures": [
    {
      "verdict": "UNCHANGED",
      "status": { "signature": "VALID", "certificate": "VALID", "certificateChain": "VALID",
                  "revocation": "NOT_REVOKED", "trust": "TRUSTED", "timestamp": "VALID",
                  "ltv": "PRESENT_NOT_VERIFIED", "pades": "B-LT_STRUCTURALLY_CONFORMANT" },
      …
    }
  ],
  …
}

verdict per signature is one of UNCHANGED, ALLOWED_CHANGES, CHANGED, REVOKED or BROKEN. A PDF with no signatures returns an empty signatures array.

Webhooks

Add an HTTPS endpoint on Dashboard → API keys and we will POST a JSON event to it whenever one of your signature requests changes, including requests sent from the website. Choose which events to receive, or leave them all on.

EventWhen
request.sentA request was created and the first invitations went out.
signer.viewedA signer opened the document for the first time.
signer.signedA signer (or witness) signed.
signer.approvedAn approver approved.
signer.declinedA signer declined; the request is closed.
signer.reassignedA signer handed their turn to someone else.
request.completedEveryone signed; the final document is ready.
request.expiredThe expiry date passed before everyone signed.
request.cancelledYou cancelled the request.
pingYou pressed “Send test” on the dashboard.
POST /your/webhook HTTP/1.1
Content-Type: application/json
KovaPDF-Event: signer.signed
KovaPDF-Delivery: 7b9eebb3-91bb-4feb-991a-63685a471937
KovaPDF-Signature: t=1790000000,v1=5f2c…

{
  "id": "7b9eebb3-91bb-4feb-991a-63685a471937",
  "type": "signer.signed",
  "createdAt": "2026-09-22T09:30:11.482Z",
  "data": {
    "request": { "id": "4f0c…", "title": "Services Agreement", "status": "IN_PROGRESS",
                 "createdAt": "…", "completedAt": null, "expiresAt": "…" },
    "signer":  { "id": "aa57…", "name": "John Smith", "email": "john@example.com", "role": "SIGNER", "status": "SIGNED" }
  }
}

Delivery and retries

  • Answer with any 2xx within 10 seconds. Anything else (an error status, a redirect, a timeout) counts as a failure.
  • Failures are retried after 10 s, 1 min, 5 min, 30 min, 2 h and 6 h (7 attempts in all). The last 20 deliveries per endpoint, with their status, are on the dashboard, where you can also resend one.
  • Delivery is at-least-once and not strictly ordered. Use the id (also in KovaPDF-Delivery) to ignore duplicates, and fetch the request when you need its latest state.

Verifying the signature

Every delivery is signed with your endpoint's signing secret (whsec_…, shown on the dashboard).KovaPDF-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw request body>. Compute it over the raw body bytes (before any JSON parsing), compare in constant time, and reject timestamps more than five minutes old.

Node.js

import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.KOVAPDF_WEBHOOK_SECRET; // whsec_...

function verifyKovaSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds || !parts.v1) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

const app = express();
app.post("/kovapdf/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyKovaSignature(raw, req.get("KovaPDF-Signature") ?? "", SECRET)) return res.sendStatus(400);
  const event = JSON.parse(raw);
  // ...handle event.type, deduplicate on event.id...
  res.sendStatus(200);
});
app.listen(3000);

Python

import hashlib, hmac, os, time
from flask import Flask, request, abort

SECRET = os.environ["KOVAPDF_WEBHOOK_SECRET"]  # whsec_...

def verify_kova_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

app = Flask(__name__)

@app.post("/kovapdf/webhook")
def kovapdf_webhook():
    raw = request.get_data()  # raw bytes, before parsing
    if not verify_kova_signature(raw, request.headers.get("KovaPDF-Signature", ""), SECRET):
        abort(400)
    event = request.get_json()
    # ...handle event["type"], deduplicate on event["id"]...
    return "", 200

Rotating a secret on the dashboard takes effect immediately: deliveries from then on (including retries of older events) are signed with the new secret.

OpenAPI file

The whole API is described in one OpenAPI 3.1 file: every endpoint, the key header, the scope each endpoint needs, the errors, the limits and examples. Import it into an API client, or generate a client in the language you use.

curl https://api.kovapdf.com/api/v1/openapi.json -o kovapdf-openapi.json