Plain Markdown ↗  ·  OpenAPI ↗

UpParser Blog API — instructions for AI agents

Use this API to upload images and create, read, update, publish, unpublish, or delete blog articles. No browser automation or login is needed.

Production base URL: https://upparser.com

Human-readable documentation: /docs/blog-api (redirects to the English documentation page).

Plain Markdown documentation: /docs/blog-api.md

OpenAPI 3.1 specification: /api/blog/openapi.json

Authentication

Every management request requires this header:

Authorization: Bearer YOUR_BLOG_API_KEY

The owner supplies one key with full blog permissions. Obtain it through the owner's secure environment or secret mechanism. Never include it in articles, public links, documentation, source control, or query parameters. Never send it to the media domain or a third-party URL. Public article pages and documentation do not require the key.

Set UPPARSER_BLOG_API_KEY in your environment before running the examples below. Do not replace it with the owner's document-parsing API key; the two credentials are independent.

Quick start

  1. Optionally upload a featured image with POST /api/v1/blog/assets.
  2. Choose one English slug for the entire article, for example pdf-to-excel-guide.
  3. Send PUT /api/v1/blog/posts/pdf-to-excel-guide with the complete content for one language.
  4. Use status: "published" to publish immediately, or "draft" to keep it private.
  5. Add translations by sending the same URL with a different locale.

The public URLs are /en/pdf-to-excel-guide, /zh/pdf-to-excel-guide, etc. The language-specific indexes are /en/blog, /zh/blog, etc. Missing translations return 404; the site does not publish automatic translations or copy the English body under other languages.

Upload an image

curl --fail-with-body https://upparser.com/api/v1/blog/assets \
  -H "Authorization: Bearer $UPPARSER_BLOG_API_KEY" \
  -F '[email protected]'

Send multipart/form-data with exactly one file field. Let your HTTP client set the multipart boundary. Maximum file size: 10 MiB. Supported inputs: static JPEG, PNG, WebP and AVIF. SVG, GIF, animated and invalid images are rejected. Actual image data is decoded; the filename is not trusted. The image is resized to fit within 2400 × 2400 pixels, orientation is corrected, metadata is removed and output is compressed to WebP.

Example response (HTTP 201):

{
  "key": "CONTENT_SHA256.webp",
  "url": "https://media.upparser.com/CONTENT_SHA256.webp",
  "width": 1600,
  "height": 900,
  "bytes": 84210,
  "contentType": "image/webp"
}

Use url in featuredImage.url or Markdown image syntax. Repeated identical optimized images share a content-addressed URL. Uploading alone does not attach an image to an article. Production images are public immediately; do not upload confidential material. Updating an image means uploading the new file and updating the article URL. The API does not delete image assets; article deletion leaves images available for other articles.

Create or replace a language version

PUT /api/v1/blog/posts/{slug} creates an article if needed, or replaces the specified language version. Other language versions remain unchanged. Repeating the same PUT does not create duplicate articles.

Slugs: 1–120 characters, lowercase ASCII letters, digits and single hyphens between words; no leading/trailing hyphen. Existing website route names such as blog, tools, pricing, docs, login and language codes are reserved. Slugs cannot be renamed with this endpoint: a different slug creates a different article.

curl --fail-with-body -X PUT \
  https://upparser.com/api/v1/blog/posts/pdf-to-excel-guide \
  -H "Authorization: Bearer $UPPARSER_BLOG_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @article.json

Example article.json:

{
  "locale": "en",
  "title": "How to turn a PDF into an Excel spreadsheet",
  "excerpt": "A practical guide to extracting structured data from PDFs.",
  "markdown": "## Start with your document\n\nUpload a PDF to begin.\n\n<CTA title=\"Try it with your own PDF\" text=\"Turn a document into structured data.\" buttonText=\"Get started\" href=\"https://app.upparser.com\" />\n\n<YouTube id=\"dQw4w9WgXcQ\" title=\"Video walkthrough\" />",
  "status": "published",
  "author": "UpParser",
  "featuredImage": {
    "url": "https://media.upparser.com/CONTENT_SHA256.webp",
    "alt": "An invoice alongside its extracted spreadsheet"
  },
  "tags": ["PDF", "Automation"],
  "seoTitle": "PDF to Excel: a practical guide",
  "seoDescription": "Learn how to extract PDF data into a spreadsheet with UpParser."
}
FieldRules
localeRequired: en, zh, es, de, fr
titleRequired, 1–200 characters
markdownRequired, 1–150,000 characters
excerptUp to 600 characters; default empty
statusdraft or published; default draft
author1–100 characters; default UpParser
featuredImage{ "url": "HTTPS_OR_SITE_RELATIVE_URL", "alt": "description" } or null; default null
tagsUp to 12 strings, each 1–50 characters; default empty
seoTitleUp to 200 characters; falls back to article title
seoDescriptionUp to 600 characters; falls back to excerpt

Unknown fields are rejected. The total JSON request body must be under 800,000 bytes. This is a complete replacement, not a partial update: omitted optional fields reset to their defaults. When editing an existing article, GET the language version first, modify it, and submit only the writable fields above. Do not submit server-managed slug, timestamps or publishedAt to PUT. If multiple writers edit the same translation, the last successful write wins.

Response (HTTP 200):

{
  "post": { "slug": "pdf-to-excel-guide", "locale": "en", "title": "...", "markdown": "...", "status": "published", "createdAt": "...", "updatedAt": "...", "publishedAt": "..." },
  "url": "https://upparser.com/en/pdf-to-excel-guide",
  "cacheRefresh": "requested"
}

url is null for drafts. Timestamps are assigned by the server. The first publication date is preserved when editing or republishing. The same slug has independently managed language versions.

POST /api/v1/blog/posts is an alternative: send the same JSON plus a slug field. It also creates or replaces one language version.

Read and list articles

All these endpoints require the same Bearer key and return drafts as well as published content. Responses are not publicly cached.

  • GET /api/v1/blog/posts?limit=25 returns { "posts": [...], "nextCursor": "slug-or-null" }. List items contain language metadata, not full Markdown. Default limit 25, maximum 100. Follow ?cursor=NEXT_CURSOR&limit=25 until nextCursor is null. Results are in ascending slug order.
  • GET /api/v1/blog/posts/{slug} returns { "post": { "slug": "...", "translations": { "en": {...}, "zh": {...} }, ... } } with metadata for existing languages.
  • GET /api/v1/blog/posts/{slug}?locale=en returns { "post": {...} } with the complete English version including markdown.

Markdown and rich content

Standard Markdown and GitHub-style tables, lists, task lists, links, images, blockquotes and fenced code blocks are supported. Supply a code-fence language such as python, javascript, json or bash for highlighting.

print("This is displayed as highlighted code, not executed.")

For interactive presentation, use the built-in components below. Put each component in its own block, with a blank line before and after. Attributes must be literal single- or double-quoted strings. Do not place a component inside a code fence unless you want to show its source.

Call to action

<CTA
  title="Turn documents into useful data"
  text="Try UpParser with your own PDF."
  buttonText="Get started"
  href="https://app.upparser.com"
/>

title, buttonText and href are required; text is optional. Links must use HTTPS or a site-relative path starting with one /.

YouTube video

<YouTube id="dQw4w9WgXcQ" title="Video walkthrough" />

Use the 11-character video ID, not a full YouTube URL. title is required for accessibility. Videos use a responsive, lazy-loaded privacy-enhanced YouTube embed.

Only these two component names and their documented attributes are interpreted. JavaScript expressions, imports, arbitrary JSX, scripts and raw HTML/iframes are not executed or rendered. Invalid components are omitted. Use standard Markdown for other content. Links and images only allow HTTPS or site-relative URLs (links may also use local #anchor fragments).

Unpublish or delete

To unpublish, PUT the complete language version again with status: "draft". The public page returns 404 after cached copies refresh. Editing a published language with status: "published" replaces its live content; there is no separate approval stage or version history.

DELETE /api/v1/blog/posts/{slug}?locale=zh permanently deletes only the Chinese version.

DELETE /api/v1/blog/posts/{slug} permanently deletes the article in every language. Repeating DELETE is safe. Images are retained. Ask the owner before deleting content unless deletion is already part of their request.

SEO and caching

Published pages include server-rendered content, canonical URLs, available-language hreflang links, social metadata and BlogPosting structured data. Published language versions appear in /sitemap.xml. Drafts are excluded from public article pages, lists and the sitemap.

Blog pages and database reads use a 60-second ISR/data-cache revalidation interval. Writes request invalidation on the instance handling the request. On multiple Cloud Run instances, another instance can briefly serve older content; revalidation is traffic-driven and 60 seconds is not a strict global publication deadline. Existing browser/client-router or downstream caches may also retain a prior response. Images have content-hashed URLs with one-year immutable caching.

cacheRefresh: "requested" means invalidation was requested, not that every edge/browser has refreshed. "failed" means the article was saved but invalidation failed; retry the PUT or allow normal revalidation. There is no whole-site HTML CDN cache configured by this feature. Keep authenticated API requests outside any CDN cache rules.

Errors

Errors use { "error": "code" }; validation errors also include issues with field paths and messages.

StatusMeaning
400invalidRequest, invalidLimit, invalidCursor, fileRequired, invalidImage, invalidMultipart
401unauthorized: missing or invalid blog key
404notFound: missing article/language
413size or imageTooLarge: request exceeds limits
503blogNotConfigured, blogMediaNotConfigured, or serviceUnavailable

Correct 400 errors before retrying. Do not repeatedly retry a 401 with the same credential. Transient 503 responses can be retried with backoff. A PUT is safe to retry after a network failure because the slug and locale identify the same version.

Search discovery and indexing notifications

Article translations are independent. Publish translated titles, excerpts, bodies, SEO titles, SEO descriptions, and image alt text in the requested language. Missing language versions return 404; they are never advertised as published translations.

/sitemap.xml is a sitemap index. Follow its child sitemaps to find every canonical public URL. Blog shards include only published translations, truthful modification timestamps, image URLs, and reciprocal hreflang links. Publication changes invalidate the blog discovery cache. /llms.txt links to per-language reading indexes.

Write/delete responses include indexNow: disabled, unchanged, submitted, or failed. The site owner can enable production IndexNow notifications. submitted acknowledges receipt only, not indexing. failed does not undo a successful content write; the site operator can retry the logged public URLs. Treat cacheRefresh: "failed" separately from persistence: use an idempotent PUT to request a fresh invalidation.