Dokaz Industries

Ten small APIs that do one thing well.

Invoice PDFs from JSON. Email verification with real MX checks. Website intelligence. Text AI. QR codes and barcodes. Markdown to PDF. CSV to JSON and back, and JSON to Excel. Calendar (.ics) files. Photo metadata removal. All on Cloudflare's edge, all with a free tier, no card to start.

Try one right now

curl "https://api.dokaz.net/v1/email/verify?email=someone@gmial.com"
curl "https://api.dokaz.net/v1/qr?data=https://dokaz.net&format=svg" -o qr.svg
curl "https://api.dokaz.net/v1/invoice/sample" -o sample.pdf
curl "https://api.dokaz.net/v1/site/intel?url=https://example.com"
curl "https://api.dokaz.net/v1/barcode?data=4006381333931&type=ean13&text=1" -o ean.svg
curl -X POST "https://api.dokaz.net/v1/convert/csv-to-json" -H "content-type: text/csv" --data-binary @people.csv
curl -X POST "https://api.dokaz.net/v1/image/strip" --data-binary @photo.jpg -o clean.jpg
curl "https://api.dokaz.net/v1/calendar/ics?title=Demo&start=2026-10-08T17:00:00Z" -o demo.ics

Free tier: 100 calls per day per IP, no key. Paid keys go in the X-Api-Key header.

Products

Invoice PDF

Turn a JSON payload into a clean, professional invoice, estimate or receipt PDF in under a millisecond. Totals, tax and discounts are computed server-side; multi-page output for up to 60 line items.

POST /v1/invoice/pdf — Render an invoice/estimate/receipt as a PDF (application/pdf, Content-Disposition: attachment). Required: number, date (YYYY-MM-DD), from.name, to.name, items[] (description, qty, unit_price). Optional: kind, due, currency (USD/EUR/GBP/CAD/AUD), per-line or global tax_rate (%), discount (amount), notes, terms, paid, logo_text. total = subtotal + tax - discount. Max 60 items.

Request

{
  "kind": "invoice",
  "number": "INV-2026-0142",
  "date": "2026-09-06",
  "due": "2026-10-06",
  "currency": "USD",
  "logo_text": "NORTHWIND",
  "from": {
    "name": "Northwind Studio LLC",
    "address": [
      "1200 Market Street, Suite 410",
      "San Francisco, CA 94102"
    ],
    "email": "billing@northwind.studio",
    "phone": "+1 (415) 555-0142"
  },
  "to": {
    "name": "Acme Robotics Inc.",
    "address": "42 Foundry Lane\nAustin, TX 78701",
    "email": "ap@acme-robotics.com"
  },
  "items": [
    {
      "description": "Brand identity design",
      "qty": 1,
      "unit_price": 2400
    },
    {
      "description": "Product UI design – 6 screens",
      "qty": 6,
      "unit_price": 350
    },
    {
      "description": "Hosting & domain setup",
      "qty": 1,
      "unit_price": 150,
      "tax_rate": 0
    }
  ],
  "tax_rate": 8.25,
  "discount": 200,
  "notes": "Thank you for your business.",
  "terms": "Payment due within 30 days.",
  "paid": false
}

Response

<binary PDF, ~6 KB; header %PDF-1.4>
POST /v1/invoice/preview — Same body as /pdf; returns the computed totals and per-line amounts as JSON so you can check the math before rendering.

Request

{
  "kind": "invoice",
  "number": "INV-2026-0142",
  "date": "2026-09-06",
  "due": "2026-10-06",
  "currency": "USD",
  "logo_text": "NORTHWIND",
  "from": {
    "name": "Northwind Studio LLC",
    "address": [
      "1200 Market Street, Suite 410",
      "San Francisco, CA 94102"
    ],
    "email": "billing@northwind.studio",
    "phone": "+1 (415) 555-0142"
  },
  "to": {
    "name": "Acme Robotics Inc.",
    "address": "42 Foundry Lane\nAustin, TX 78701",
    "email": "ap@acme-robotics.com"
  },
  "items": [
    {
      "description": "Brand identity design",
      "qty": 1,
      "unit_price": 2400
    },
    {
      "description": "Product UI design – 6 screens",
      "qty": 6,
      "unit_price": 350
    },
    {
      "description": "Hosting & domain setup",
      "qty": 1,
      "unit_price": 150,
      "tax_rate": 0
    }
  ],
  "tax_rate": 8.25,
  "discount": 200,
  "notes": "Thank you for your business.",
  "terms": "Payment due within 30 days.",
  "paid": false
}

Response

{
  "subtotal": 4650,
  "tax": 371.25,
  "discount": 200,
  "total": 4821.25,
  "currency": "USD",
  "line_items": [
    {
      "description": "Brand identity design",
      "qty": 1,
      "unit_price": 2400,
      "tax_rate": 8.25,
      "amount": 2400,
      "tax": 198
    },
    {
      "description": "Product UI design – 6 screens",
      "qty": 6,
      "unit_price": 350,
      "tax_rate": 8.25,
      "amount": 2100,
      "tax": 173.25
    },
    {
      "description": "Hosting & domain setup",
      "qty": 1,
      "unit_price": 150,
      "tax_rate": 0,
      "amount": 150,
      "tax": 0
    }
  ]
}
GET /v1/invoice/sample — A sample invoice PDF rendered from built-in demo data (inline, opens in the browser).

Request

GET /v1/invoice/sample

Response

<binary PDF, Content-Disposition: inline; filename="invoice-INV-2026-0142.pdf">

Email Verify

Syntax, MX via DNS-over-HTTPS, disposable-domain list, role accounts, typo suggestions — scored 0-100.

GET /v1/email/verify — Verify one address: syntax, MX (DNS-over-HTTPS, with A/AAAA implicit-MX fallback), disposable domain, role account, free provider, typo suggestion, gibberish local part. Returns a 0-100 score; `valid` is syntax AND MX AND not disposable.

Request

GET https://api.dokaz.net/v1/email/verify?email=jane.doe%2Bnews%40gmail.com

Response

{
  "email": "jane.doe+news@gmail.com",
  "normalized": "janedoe@gmail.com",
  "valid": true,
  "score": 100,
  "checks": {
    "syntax": true,
    "domain_has_mx": true,
    "mx_hosts": [
      "gmail-smtp-in.l.google.com",
      "alt1.gmail-smtp-in.l.google.com"
    ],
    "disposable": false,
    "role_account": false,
    "free_provider": true,
    "typo_suggestion": null,
    "gibberish_local": false
  },
  "reason": "ok"
}
POST /v1/email/verify — Same as GET, with a JSON body.

Request

POST https://api.dokaz.net/v1/email/verify
content-type: application/json

{ "email": "sales@gmial.com" }

Response

{
  "email": "sales@gmial.com",
  "normalized": "sales@gmial.com",
  "valid": false,
  "score": 0,
  "checks": {
    "syntax": true,
    "domain_has_mx": false,
    "mx_hosts": [],
    "disposable": false,
    "role_account": true,
    "free_provider": false,
    "typo_suggestion": "gmail.com",
    "gibberish_local": false
  },
  "reason": "domain has no MX or A/AAAA records; role account; did you mean gmail.com?"
}
POST /v1/email/verify/batch — Verify up to 50 addresses concurrently (one DNS lookup per unique domain, at most 45 DNS queries per batch; past that, domain_has_mx is null and reason says "dns budget exhausted"). Results come back in request order. Counts as one API call.

Request

POST https://api.dokaz.net/v1/email/verify/batch
content-type: application/json

{ "emails": ["jane@example.org", "bob@mailinator.com"] }

Response

{
  "count": 2,
  "results": [
    {
      "email": "jane@example.org",
      "valid": false,
      "score": 30,
      "reason": "domain has no MX or A/AAAA records",
      "checks": {
        "...": "..."
      }
    },
    {
      "email": "bob@mailinator.com",
      "valid": false,
      "score": 40,
      "reason": "disposable domain",
      "checks": {
        "...": "..."
      }
    }
  ]
}

Site Intel

URL → title/OG, tech stack, contact emails/phones, socials, analytics, and locked-SaaS platform detection.

GET /v1/site/intel — Fetch a public web page (http/https, up to 3 redirects, first 512 KB; `truncated` says whether more was cut) and return title/meta/OpenGraph, tech stack, locked vertical-SaaS platform (Shopmonkey, Tekmetric, Jobber, Toast, Vagaro, Mindbody, Square Online, ...; only on a positive marker), contact emails/phones, social profiles, link counts, analytics tags, schema.org types and server headers.

Request

GET https://api.dokaz.net/v1/site/intel?url=https%3A%2F%2Frainierplumbing.com

Response

{
  "url": "https://rainierplumbing.com",
  "final_url": "https://www.rainierplumbing.com/",
  "status": 200,
  "redirect_chain": [
    "https://rainierplumbing.com/",
    "https://www.rainierplumbing.com/"
  ],
  "truncated": false,
  "title": "Rainier Plumbing & Heating | Tacoma Plumber",
  "description": "Licensed plumber serving Tacoma and Pierce County. 24/7 emergency service.",
  "canonical": "https://www.rainierplumbing.com/",
  "language": "en-US",
  "favicon": "https://www.rainierplumbing.com/wp-content/uploads/favicon.png",
  "og": {
    "title": "Rainier Plumbing & Heating",
    "description": null,
    "image": "https://www.rainierplumbing.com/wp-content/uploads/van.jpg",
    "type": "website",
    "site_name": "Rainier Plumbing"
  },
  "twitter_card": "summary_large_image",
  "generator": "WordPress 6.5.2",
  "tech": [
    "WordPress",
    "Elementor",
    "jQuery",
    "Google Fonts",
    "reCAPTCHA",
    "Cloudflare"
  ],
  "locked_platform": null,
  "contacts": {
    "emails": [
      "office@rainierplumbing.com"
    ],
    "phones": [
      "+12535550142"
    ],
    "addresses_hint": [
      "1420 Pacific Ave, Tacoma, WA, 98402"
    ]
  },
  "socials": {
    "facebook": "https://www.facebook.com/rainierplumbing",
    "instagram": null,
    "linkedin": null,
    "twitter": null,
    "youtube": null,
    "tiktok": null,
    "yelp": "https://www.yelp.com/biz/rainier-plumbing-tacoma"
  },
  "links": {
    "internal": 42,
    "external": 6
  },
  "has_forms": true,
  "has_analytics": [
    "ga4"
  ],
  "schema_org_types": [
    "Plumber",
    "PostalAddress"
  ],
  "headers": {
    "server": "cloudflare",
    "x_powered_by": null,
    "cf_ray_present": true
  },
  "fetched_at": "2026-09-07T06:12:44.000Z"
}
POST /v1/site/intel — Same as GET, with a JSON body. A scheme-less "example.com" is treated as https://example.com.

Request

POST https://api.dokaz.net/v1/site/intel
content-type: application/json

{ "url": "rainierplumbing.com" }

Response

{
  "url": "https://rainierplumbing.com",
  "final_url": "https://www.rainierplumbing.com/",
  "status": 200,
  "redirect_chain": [
    "https://rainierplumbing.com/",
    "https://www.rainierplumbing.com/"
  ],
  "truncated": false,
  "title": "Rainier Plumbing & Heating | Tacoma Plumber",
  "description": "Licensed plumber serving Tacoma and Pierce County. 24/7 emergency service.",
  "canonical": "https://www.rainierplumbing.com/",
  "language": "en-US",
  "favicon": "https://www.rainierplumbing.com/wp-content/uploads/favicon.png",
  "og": {
    "title": "Rainier Plumbing & Heating",
    "description": null,
    "image": "https://www.rainierplumbing.com/wp-content/uploads/van.jpg",
    "type": "website",
    "site_name": "Rainier Plumbing"
  },
  "twitter_card": "summary_large_image",
  "generator": "WordPress 6.5.2",
  "tech": [
    "WordPress",
    "Elementor",
    "jQuery",
    "Google Fonts",
    "reCAPTCHA",
    "Cloudflare"
  ],
  "locked_platform": null,
  "contacts": {
    "emails": [
      "office@rainierplumbing.com"
    ],
    "phones": [
      "+12535550142"
    ],
    "addresses_hint": [
      "1420 Pacific Ave, Tacoma, WA, 98402"
    ]
  },
  "socials": {
    "facebook": "https://www.facebook.com/rainierplumbing",
    "instagram": null,
    "linkedin": null,
    "twitter": null,
    "youtube": null,
    "tiktok": null,
    "yelp": "https://www.yelp.com/biz/rainier-plumbing-tacoma"
  },
  "links": {
    "internal": 42,
    "external": 6
  },
  "has_forms": true,
  "has_analytics": [
    "ga4"
  ],
  "schema_org_types": [
    "Plumber",
    "PostalAddress"
  ],
  "headers": {
    "server": "cloudflare",
    "x_powered_by": null,
    "cf_ray_present": true
  },
  "fetched_at": "2026-09-07T06:12:44.000Z"
}

Text AI

Summarize, classify, extract, rewrite and score sentiment/keywords with an LLM. JSON in, JSON out.

POST /v1/text/summarize — Summarize text in N sentences, as a paragraph or bullets.

Request

{"text":"Cloudflare Workers run JavaScript at the edge in 300+ cities. They start in under 5ms and scale to zero, so you only pay for requests. D1 adds a SQLite database and Workers AI adds inference, all on the same free tier.","sentences":2,"style":"bullets"}

Response

{
  "summary": "- Cloudflare Workers run edge JavaScript in 300+ cities with sub-5ms cold starts and pay-per-request pricing.\n- D1 (SQLite) and Workers AI (inference) sit on the same free tier.",
  "sentences": 2,
  "input_chars": 213,
  "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
}
POST /v1/text/sentiment — Sentiment label with a signed score (-1..1) and confidence (0..1). Scores the first 2000 characters.

Request

{"text":"Setup took two minutes and it just worked. Support answered within the hour."}

Response

{
  "label": "positive",
  "score": 0.9982,
  "confidence": 0.9991,
  "model": "@cf/huggingface/distilbert-sst-2-int8"
}
POST /v1/text/keywords — Weighted keywords and key phrases (5..30, default 10). Falls back to a term-frequency ranking (model "fallback") if the LLM output is unusable.

Request

{"text":"Our invoice PDF API turns JSON into a clean one-page invoice or estimate. No template editor, no library, under one millisecond of CPU.","max":5}

Response

{
  "keywords": [
    {
      "term": "invoice pdf api",
      "weight": 0.95
    },
    {
      "term": "json",
      "weight": 0.8
    },
    {
      "term": "estimate",
      "weight": 0.7
    },
    {
      "term": "one-page invoice",
      "weight": 0.65
    },
    {
      "term": "cpu",
      "weight": 0.4
    }
  ],
  "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
}
POST /v1/text/classify — Zero-shot classification into your own labels (2..20) with a probability per label.

Request

{"text":"My card was charged twice for the same order, please refund one.","labels":["billing","bug report","feature request","other"]}

Response

{
  "label": "billing",
  "scores": {
    "billing": 0.91,
    "bug report": 0.06,
    "feature request": 0.01,
    "other": 0.02
  },
  "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
}
POST /v1/text/extract — Structured extraction: name the fields you want (1..20) and get a JSON object back, null where absent.

Request

{"text":"Hi, this is Dana Whitfield from Northwind Traders. Call me at (555) 010-2233 or dana@northwind.example to book the 14 March demo.","fields":["name","company","phone","email","date","budget"]}

Response

{
  "data": {
    "name": "Dana Whitfield",
    "company": "Northwind Traders",
    "phone": "(555) 010-2233",
    "email": "dana@northwind.example",
    "date": "14 March",
    "budget": null
  },
  "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
}
POST /v1/text/rewrite — Rewrite in a tone: professional, casual, concise, friendly or formal. Facts are preserved.

Request

{"text":"hey so the thing is broken again, can u fix it today?? kinda urgent","tone":"professional"}

Response

{
  "text": "Hello, the issue has reoccurred. Could you please address it today? It is fairly urgent.",
  "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
}

QR Code API

Generate QR codes as SVG or PNG (or the raw module matrix) — URLs, text, Wi‑Fi credentials and vCards. Versions 1–40, all EC levels, custom colours, rounded modules.

GET /v1/qr — Render a QR code. Params: data (required, ≤2953 bytes), format=svg|png (default svg), size=64..2048 px (default 512), margin=0..10 modules (default 4), ec=L|M|Q|H (default M), fg/bg hex colours, rounded=0|1. Responds with the image and a one-year cache header; x-qr-version and x-qr-modules headers describe the symbol.

Request

GET /v1/qr?data=https%3A%2F%2Fdokaz.net&format=svg&size=256&ec=Q&fg=%23112233

Response

<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 33 33" shape-rendering="crispEdges"><rect width="33" height="33" fill="#ffffff"/><path fill="#112233" d="M4 4h7v1h-7z…"/></svg>
POST /v1/qr — Same parameters as GET, as a JSON body. format="json" returns the raw module matrix (rows of 1/0) so you can render it yourself.

Request

POST /v1/qr
{"data":"HELLO WORLD","ec":"M","format":"json"}

Response

{"version":1,"ec":"M","mode":"alphanumeric","mask":2,"modules":21,"matrix":["111111100101001111111","100000101010101000001",…]}
GET /v1/qr/wifi — QR code that joins a Wi‑Fi network when scanned. Params: ssid (required), password, type=WPA|WEP|nopass (default WPA), hidden=0|1, plus any render params (format, size, ec, fg, bg, margin, rounded).

Request

GET /v1/qr/wifi?ssid=Cafe%20Guest&password=latte123&type=WPA&format=png&size=300

Response

(image/png, 300×300; payload WIFI:T:WPA;S:Cafe Guest;P:latte123;;)
GET /v1/qr/vcard — QR code holding a vCard 3.0 contact. Params: name (required), phone, email, org, url, plus any render params.

Request

GET /v1/qr/vcard?name=Ada%20Lovelace&phone=%2B44%2020%207946%200958&email=ada%40example.com&org=Analytical%20Engines&url=https%3A%2F%2Fexample.com

Response

(image/svg+xml; payload BEGIN:VCARD VERSION:3.0 N:Lovelace;Ada;;; FN:Ada Lovelace ORG:Analytical Engines TEL;TYPE=CELL:+44 20 7946 0958 EMAIL;TYPE=INTERNET:ada@example.com URL:https://example.com END:VCARD)

Barcode API

Code 128, EAN-13 and UPC-A barcodes as crisp SVG from one GET. Code 128 picks code sets B and C automatically and adds its check character; EAN-13 and UPC-A compute the check digit, or verify the one you send.

GET /v1/barcode — Render a barcode as SVG. Params: data (required), type=code128|ean13|upca (default code128), text=0|1 (human-readable text under the bars, default 0), scale=1..10 px per module (default 2), height=10..1000 px bar height (default 80), margin=0..50 modules of quiet zone each side (default 10), fg/bg hex colours, format=svg (the only format). code128: 1..80 printable ASCII characters. ean13: 12 digits (check digit added) or 13 (check digit verified - a wrong one is a 400). upca: 11 or 12 digits, likewise. x-barcode-text carries what was encoded, check digit included.

Request

GET /v1/barcode?data=400638133393&type=ean13&text=1

Response

<svg xmlns="http://www.w3.org/2000/svg" width="230" height="…" viewBox="…" shape-rendering="crispEdges"><rect …/><path fill="#000000" d="M18 8h2v…z…"/>…</svg>  (x-barcode-text: 4006381333931)

Markdown to PDF

POST Markdown, get a clean paginated PDF back: headings, paragraphs, bullet and numbered lists, bold, italic, inline and fenced code, links printed with their URL. Letter or A4, page numbers, selectable text.

POST /v1/pdf/markdown — Render Markdown to a PDF (application/pdf, Content-Disposition: attachment). JSON body: markdown (required), title (optional, printed at the top and set as the PDF title, ≤200 chars), page_size=letter|a4 (default letter). Supports # to ###### headings, paragraphs, -/*/+ bullet and 1. numbered lists, **bold**, *italic*, `inline code`, fenced ``` code blocks, and [links](url) printed as "text (url)". Anything else prints as its literal text. Body ≤ 200 KB (413 above), at most 400 pages. Latin-1 and Windows-1252 characters print as themselves; anything else prints as "?". x-pdf-pages gives the page count.

Request

POST /v1/pdf/markdown
{"title":"Release notes","page_size":"a4","markdown":"# Release 2.4\n\nThis release makes **exports** faster and fixes *two* bugs.\n\n- CSV export streams rows\n- `--dry-run` prints the plan\n\nSee [the changelog](https://example.com/changelog).\n\n```\nnpm install acme@2.4\n```"}

Response

<binary PDF; header %PDF-1.4; x-pdf-pages: 1>

CSV ⇄ JSON ⇄ Excel

Convert CSV to JSON and JSON to CSV with RFC 4180 quoting done right: commas, quotes and line breaks inside fields, CRLF or LF, byte-order marks, empty fields. Up to 2 MB per request. Or turn JSON rows into a real Excel workbook (.xlsx) with typed cells, a bold header and one sheet per list (up to 1 MB of JSON).

POST /v1/convert/csv-to-json — CSV to a JSON array. Send the CSV itself (text/csv; options as query params) or JSON {csv, delimiter?, header?}. header=true (default): an array of objects keyed by the first record, whose names must be unique; header=false: an array of arrays. Every value is a string. delimiter: one character (default ","; "tab" for tab). Quoted fields may hold the delimiter, quotes ("") and line breaks; CRLF, LF and CR line endings and a UTF-8 BOM are accepted; blank lines are skipped. Rejected with a 400 naming the line: a quote inside an unquoted field, text after a closing quote, an unterminated quote, and ragged rows (a record with more or fewer fields than the first - nothing is padded or dropped). Body ≤ 2 MB (413 above). x-csv-rows and x-csv-columns describe the result.

Request

POST /v1/convert/csv-to-json
content-type: text/csv

name,city
"Hopper, Grace",Arlington
Ada,London

Response

[{"name":"Hopper, Grace","city":"Arlington"},{"name":"Ada","city":"London"}]
POST /v1/convert/json-to-csv — A JSON array to RFC 4180 CSV (text/csv). Body: an array of objects, or {rows, delimiter?, header?} (options also as query params). Columns are the union of the objects' keys in first-seen order; a missing key is an empty field. Fields holding the delimiter, a quote, CR or LF are quoted with quotes doubled; records end in CRLF. Numbers and booleans are written as JSON writes them, null as empty, a nested object or array as its JSON text. An array of arrays is written as rows (padded to the widest, no header). header=false omits the header row. Body ≤ 2 MB (413 above).

Request

POST /v1/convert/json-to-csv
[{"name":"Hopper, Grace","born":1906},{"name":"Ada","city":"London"}]

Response

name,born,city
"Hopper, Grace",1906,
Ada,,London
POST /v1/convert/json-to-xlsx — JSON rows to an Excel workbook (.xlsx, Content-Disposition: attachment). Body: an array of objects (or of arrays), {rows, sheet_name?, header?}, or {sheets: [{name, rows, header?}, ...]} for up to 50 sheets; sheet_name and header also as query params. Columns are the union of the objects' keys in first-seen order, written as a bold header row that stays frozen while scrolling (header=false omits it); an array of arrays is written as rows with no header. Cells are typed from the JSON, never guessed: numbers are numbers, true/false are booleans, strings are text exactly as sent (leading zeros kept; "=..." is text, never a formula), null or a missing key is an empty cell, a nested object or array is its JSON text. Column widths follow the content. Rejected with a 400: a string over Excel's 32,767-character cell limit, more than 16,384 columns, an unpaired surrogate, a number too small for Excel to store (a subnormal, which it would read as 0), and sheet names Excel refuses (1-31 characters, none of : \ / ? * [ ], unique ignoring case). Body ≤ 1 MB (413 above). x-xlsx-sheets and x-xlsx-rows describe the result.

Request

POST /v1/convert/json-to-xlsx?sheet_name=Orders
[{"order":"A-1001","zip":"02134","total":129.5,"paid":true},{"order":"A-1002","zip":"98101","total":42}]

Response

<binary .xlsx workbook; x-xlsx-sheets: 1; x-xlsx-rows: 2>

Calendar (.ics) files

Turn a booking, appointment or event into an .ics file every calendar imports: Google, Apple, Outlook. Times in UTC from your offsets, or in a named time zone so weekly and monthly repeats keep their local time across daylight saving changes; all-day events, reminders, organizer and attendees. One GET makes an "Add to calendar" link.

POST /v1/calendar/ics — An iCalendar file (RFC 5545, text/calendar, Content-Disposition: attachment). JSON body: one event, or {name?, events: [...]} with 1 to 100. Event fields: title (required, ≤500 chars); start (required): "2026-10-05T14:00:00-07:00" (a UTC offset or Z is required - a bare time is refused - unless tz is given) or "2026-10-05" for an all-day event; tz: an IANA time zone name ("America/Los_Angeles", "Europe/London", "Australia/Sydney") for a timed event - start and end are then written in that zone's local time (DTSTART;TZID=...) with a VTIMEZONE defining it, so a repeat stays at the same local time when daylight saving time starts or ends (in UTC a weekly 9:00 meeting drifts to 8:00 or 10:00); with tz, start and end may also be a bare local time ("2026-10-05T09:00:00"), refused when the clocks skip it or show it twice. A repeat with no end in a zone whose clock changes follow no fixed yearly rule is refused with a 400 (give count or until within 20 years); an unknown zone, or tz on an all-day event, is a 400 too. end: the same kind as start - for an all-day event, the last day inclusive (the file carries RFC 5545's exclusive end, the day after); no end is a point in time, or one day when all-day. description (≤10,000), location (≤1,000), url (http/https), status=confirmed|tentative|cancelled, organizer {name?, email}, attendees [{name?, email}] (≤100), reminder_minutes (0..40,320: a pop-up that long before the start), repeat {freq=daily|weekly|monthly|yearly, interval?, count? or until?, by_day? (weekly: MO,TU,WE,TH,FR,SA,SU)}, uid (printable ASCII; by default one is derived from the event, so the same event downloaded twice updates rather than duplicates). Without tz, times are written in UTC; text is escaped and lines folded at 75 octets. Refused with a 400: an unknown field, an impossible date, an end before the start, a control character in text, a double quote in a name. Body ≤ 256 KB (413 above). x-ics-events gives the event count.

Request

POST /v1/calendar/ics
{"title":"Kitchen remodel consult","start":"2026-10-05T14:00:00-07:00","end":"2026-10-05T15:00:00-07:00","location":"1200 Market St, San Francisco, CA","description":"Bring photos of the space.\nParking behind the building.","organizer":{"name":"Northwind Studio","email":"hello@northwind.example"},"reminder_minutes":60}

Response

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Dokaz Industries//Dokaz API//EN
…
DTSTART:20261005T210000Z
DTEND:20261005T220000Z
SUMMARY:Kitchen remodel consult
…
END:VCALENDAR
GET /v1/calendar/ics — The same file from query parameters, for an "Add to calendar" link in an email or on a page: title, start, end, tz, description, location, url, uid, status, reminder_minutes (as in the POST body). One event; POST for organizer, attendees and repeat rules. Unknown parameters are refused.

Request

GET /v1/calendar/ics?title=Webinar%3A%20pricing%20Q%26A&start=2026-10-08T17:00:00Z&end=2026-10-08T18:00:00Z&url=https%3A%2F%2Fexample.com%2Fjoin&reminder_minutes=15

Response

BEGIN:VCALENDAR
…
SUMMARY:Webinar: pricing Q&A
…
END:VCALENDAR

Image metadata remover

Strip EXIF, GPS location, camera serials, XMP, IPTC, comments and embedded thumbnails from JPEG and PNG uploads before you store or publish them. The image data is copied byte for byte: identical pixels, no recompression. Up to 10 MB.

POST /v1/image/strip — Send a JPEG or PNG as the raw request body (any content-type; the format is read from the bytes). Returns the same image with its metadata removed and its compressed image data untouched, so it decodes to exactly the same pixels. Removed: EXIF (GPS, camera make and serial, timestamps, software), XMP, IPTC/Photoshop, comments, embedded thumbnails and multi-picture previews, PNG text and time chunks, and any bytes after the end of the image. Kept: what drawing the image needs (JPEG frame, scan, tables, JFIF and Adobe segments; PNG critical, colour, transparency and animation chunks) and, unless turned off, the ICC colour profile (keep_icc=0 drops it) and the orientation, rewritten as a minimal EXIF block holding only that one tag (keep_orientation=0 drops it). Refused with a 400: anything that is not a well-formed JPEG or PNG (truncated, a segment past the end, a bad PNG CRC, an unknown critical chunk). Form uploads are a 415: send the bytes. Body ≤ 10 MB (413 above). x-metadata-removed lists what was removed (exif, xmp, iptc, comment, icc, thumbnail, text, time, trailer, other; "none" if nothing), x-bytes-removed how much, x-image-orientation the orientation kept (1 = none needed).

Request

POST /v1/image/strip
content-type: image/jpeg

<the JPEG bytes>

Response

<the JPEG without its metadata; x-metadata-removed: exif,xmp,thumbnail; x-bytes-removed: 48213; x-image-orientation: 6>

Pricing

Free

$0
  • 100 calls / day
  • Every product
  • No card, no key
Just call it

Starter

$9/mo
  • 10,000 calls / month
  • Every product
  • Key issued instantly
Get Starter

Pro

$29/mo
  • 100,000 calls / month
  • Every product
  • Priority routing
Get Pro

Business

$79/mo
  • 500,000 calls / month
  • Every product
  • Email support
Get Business

Cancel any time from your Stripe receipt. Quotas are monthly, counted per key; usage is in every response's x-quota-used header.

Also: Uptime Monitor

Hourly checks of your website, an email when it goes down (confirmed by a second check, so a blip pages nobody) and when it is back, warnings before your TLS certificate or domain expires, and a monthly uptime report. $5/month for one site, $15/month for five. Uptime Monitor

Get the weekly email

New posts, new tools and what we are building, once a week. One click to leave.

We send a confirmation link first; nothing arrives unless you click it.