Geração de notícia sob assinatura · redações e publishers

Para desenvolvedores

Documentação

Referência da API, do webhook, do feed RSS e das integrações prontas com WordPress e Ghost. O conteúdo técnico abaixo está em inglês, padrão de mercado pra documentação de API.

Autenticação

Every request is authenticated with a Bearer API key, scoped to a single brand (client_id). Generate or rotate your key from the dashboard — Settings → API key. The raw key is shown once, at generation time; only its hash is stored on our side, so there is no way to retrieve a lost key — only rotate to a new one.

Request header
Authorization: Bearer naito_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

All endpoints below are relative to the API base URL:

https://api.naito.news

Session-based dashboard requests use a short-lived JWT instead of an API key — that path is internal to the Naito dashboard, not something you should implement against.

Artigos

Articles are delivered three ways: this REST API (pull), the webhook (push), or the RSS feed — pick whichever fits your CMS. All three return the exact same fields.

GET/articles

Paginated list, newest first. Query params: page (default 1), limit (default 20), category, date_from, date_to (ISO dates).

GET/articles/{id}

A single article, with three extra fields the list endpoint omits for performance: claim_source_pairs (the claim-to-source provenance trail — every factual statement mapped to the exact source excerpt that supports it), trending_client_count (how many other Naito clients published on the same source headline — null, not 0, on plans below Professional), and related_articles (internal-linking suggestions — same shape as in the webhook payload, see below).

200 OK
{
  "id": "68f2a1c9000e77b3a4c1",
  "client_id": "68e9...",
  "title": "European Central Bank holds rates for the third meeting in a row",
  "meta_title": "ECB holds rates steady | Example",
  "meta_description": "The ECB kept its policy rate unchanged, matching market expectations.",
  "slug": "ecb-holds-rates-third-meeting",
  "executive_summary": "The European Central Bank held its main rate steady...",
  "key_points": ["Rate held for the 3rd consecutive meeting", "..."],
  "body_marked": "The European Central Bank held its <hl>main refinancing rate</hl> at 3.5%...",
  "body_plain": "The European Central Bank held its main refinancing rate at 3.5%...",
  "reading_time_minutes": 2,
  "tags": ["monetary-policy", "eurozone"],
  "category": "economy",
  "sources": ["reuters.com", "ecb.europa.eu"],
  "image": {
    "url": "https://.../files/xyz/view?token=...",
    "alt_text": "ECB headquarters building in Frankfurt",
    "caption": "The European Central Bank headquarters in Frankfurt.",
    "generation_method": "ai_generated",
    "credit_line": "Image generated by Naito"
  },
  "structured_data": { "@context": "https://schema.org", "@type": "NewsArticle", "...": "..." },
  "social_snippet": { "og_description": "...", "twitter_text": "..." },
  "newsletter_snippet": "The ECB kept rates unchanged today, and here's what it means for...",
  "push_notification": { "title": "ECB holds rates steady", "body": "Third consecutive hold, as markets expected." },
  "social_pack": {
    "twitter_thread": ["The ECB just held rates steady for a third straight meeting. Here's what that means — 🧵", "..."],
    "linkedin_post": "The European Central Bank held its main rate steady today, matching market expectations...",
    "instagram_caption": "ECB holds rates steady — third meeting in a row. What it means for your wallet 👇

#ECB #economy"
  },
  "content_confidence": "high",
  "recommended_status": "published",
  "origin": "monitored",
  "sponsor_name": null,
  "cta_url": null,
  "cta_label": null,
  "ai_disclosure": { "text": "This content was produced with the help of artificial intelligence and reviewed by our editorial team.", "human_reviewed": true },
  "generated_at": "2026-09-04T14:02:11.000+00:00",
  "expires_at": "2026-12-03T14:02:11.000+00:00",
  "claim_source_pairs": [
    { "claim": "The ECB held its main rate at 3.5%", "source_excerpts": ["the ECB kept its benchmark rate at 3.5%"] }
  ],
  "related_articles": [
    { "id": "68f19a...", "title": "ECB signals rate cuts are off the table for 2026", "slug": "ecb-rate-cuts-off-table-2026" }
  ]
}

executive_summary is the article's subtitle (a.k.a. dek or standfirst) — one or two sentences meant to be displayed right below the headline, above the body, expanding on it without repeating it. It's intentionally short: don't confuse it with a full abstract. The WordPress plugin already renders it there.

image.url is a signed, temporary URL (default 72h TTL) — the underlying storage is private, so download and re-host the image on your own CMS rather than hotlinking it.

social_pack is ready-to-post copy for social media, generated from the same article — an X/Twitter thread, a LinkedIn post, and an Instagram caption. It pairs naturally with image.square_url for the Instagram post. Naito only generates the text — publishing to each platform is still up to you.

structured_data is ready-to-embed JSON-LD. An article generated with format: "faq" (see On-demand) carries an extra FAQPage entity alongside NewsArticle, nested under an @graph array ({ "@context": "...", "@graph": [{ "@type": "NewsArticle", ... }, { "@type": "FAQPage", "mainEntity": [...] }] }) instead of the flat shape shown above — check for @graph before assuming a top-level @type.

Sob demanda

On top of passive monitoring, you can request an article on any topic explicitly — Naito searches for real sources and synthesizes, it never invents facts beyond what the sources say.

POST/articles/on-demand
Request body
{
  "topic": "AI adoption trends in Brazilian newsrooms",
  "category": "tech",
  "urls": ["https://example.com/background-article"],
  "format": "explainer",
  "voice_override": { "tone": "more casual than usual for this one" }
}

Everything except topic is optional. urls (up to 3) are your own source material, fetched and extracted the same way a monitored headline would be. format picks a structure — one of explainer, list, faq, timeline, how_to, brief, comparison — omit it for a standard news write-up. voice_override lets you tweak tone/audience/angle/depth/language/banned or required terms/custom instructions for this one request only, without touching your account-wide voice profile.

202 Accepted
{
  "request_id": "68f2...",
  "status": "pending",
  "topic": "AI adoption trends in Brazilian newsrooms",
  "category": "tech",
  "format": "explainer",
  "billing": { "will_be_included": true, "remaining_included_requests_today": 4 },
  "submitted_at": "2026-09-06T12:00:00.000+00:00"
}
GET/articles/on-demand

Your recent on-demand requests and their status.

GET/articles/on-demand/{request_id}
200 OK
{
  "request_id": "68f2...",
  "status": "done",
  "topic": "AI adoption trends in Brazilian newsrooms",
  "category": "tech",
  "format": "explainer",
  "article_id": "68f2a1c9000e77b3a4c1",
  "error_message": null,
  "created_at": "2026-09-06T12:00:00.000+00:00",
  "updated_at": "2026-09-06T12:02:41.000+00:00"
}

status is one of pending, processing, done or failed (with error_message set — most commonly, no real relevant source could be found for the topic). Requests are processed within a few minutes, not instantly — poll this endpoint or just wait for the webhook/RSS delivery once it’s done.

Branded Content

Sponsored content written in your newsroom’s voice, from material your sponsor gives you directly — a prompt and/or a file (PDF/TXT/MD) — never from an external news source. Billed against its own monthly quota, separate from on-demand’s daily one; not included by default on any plan (get in touch to enable it on yours).

POST/articles/branded-content

Sent as multipart/form-data, not JSON — the file field makes that necessary:

Request (multipart/form-data fields)
sponsor_name: "Example Brand"
prompt: "Announcing our new carbon-neutral shipping program..."
file: (optional) briefing.pdf
category: "business"
format: "faq"
voice_override: {"tone": "more upbeat than usual for this one"}

At least one of prompt or file is required — sponsor_name always is. A PDF’s embedded images are considered as candidates for the article’s cover photo (the largest one above a resolution floor wins); Naito only falls back to generating one when none qualify. format, category and voice_override work exactly like on-demand’s — faq tends to do best for AI-search discovery.

202 Accepted
{
  "request_id": "68f2...",
  "status": "pending",
  "sponsor_name": "Example Brand",
  "category": "business",
  "format": "faq",
  "billing": { "will_be_included": true, "remaining_included_requests_this_month": 4 },
  "submitted_at": "2026-09-06T12:00:00.000+00:00"
}
GET/articles/branded-content

Your recent Branded Content requests and their status.

GET/articles/branded-content/{request_id}

Same shape as on-demand’s status endpoint, plus seed_file_type ("pdf", "text" or null).

The generated article’s structured_data uses schema.org’s AdvertiserContentArticle type instead of NewsArticle, with a sponsor property naming sponsor_name — the correct way to declare sponsored content to search engines and AI crawlers (it doesn’t hurt discoverability; mislabeling paid content as regular news would actually violate Google’s structured data policies). The webhook/REST payload also carries a plain is_branded_content boolean and sponsor_name — the WordPress plugin uses these to always show a disclosure notice at the top of the post, with no setting to turn it off.

When the request includes a destination URL, cta_url and cta_label carry a sponsor call-to-action link — the WordPress plugin renders it as a real <a href>, marked rel="sponsored", near the end of the article. The model never writes this link itself — it is always built from these two fields, both null when no destination URL was provided.

YouTube

Turns a YouTube video into an article by extracting its transcript — public captions when available (free), a Whisper-transcribed audio fallback otherwise (in practice the common case: real newscast clips usually have no public captions at all). Billed against its own monthly quota, separate from on-demand and Branded Content's — not included by default on Trial/Plus (every video is billed as overage there); the Whisper fallback itself is only available from the Essencial tier up — lower tiers still work whenever a video happens to already have captions.

POST/articles/youtube
Request (application/json)
{
  "youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "category": "tech",
  "format": "explainer",
  "voice_override": { "tone": "more upbeat than usual for this one" }
}

youtube_url accepts any of the three real URL shapes (watch?v=, youtu.be/, /shorts/). format, category and voice_override work exactly like on-demand's.

202 Accepted
{
  "request_id": "68f2...",
  "status": "pending",
  "youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "category": "tech",
  "format": "explainer",
  "billing": { "will_be_included": true, "remaining_included_requests_this_month": 4 },
  "submitted_at": "2026-09-29T12:00:00.000+00:00"
}
GET/articles/youtube

Your recent YouTube video requests and their status.

GET/articles/youtube/{request_id}

Same shape as on-demand's status endpoint, plus three observability fields: extraction_method ("captions" or "whisper"), video_duration_seconds, and was_truncated. Every plan has a real per-video duration cap (15 to 120 minutes depending on tier, never unlimited — even Enterprise keeps the 120-minute ceiling, by design, so a single pathologically long video can never blow past a predictable cost). A video over the cap is never rejected: Naito transcribes and generates from just the beginning, and was_truncated tells you it happened.

Resumo de newsletter

Aggregates already-generated articles from a period into a digest ready to compile into your own newsletter tool (Mailchimp, Substack, etc.) — zero additional generation cost, it just reorganizes title and newsletter_snippet from articles you already have. It intentionally does not generate a subject line, intro, or a "read more" link — those are left to you: a subject/intro written by an LLM couldn't reliably match your account's output language, and Naito never learns the final URL an article gets published at on your own CMS.

GET/newsletter-digest

days is one of 7, 14, or 30 (default 7). Articles are grouped by category, most recent first within each group.

200 OK
{
  "period_days": 7,
  "period_from": "2026-09-03",
  "period_to": "2026-09-10",
  "article_count": 3,
  "categories": [
    {
      "category": "economy",
      "articles": [
        {
          "id": "68f2a1c9000e77b3a4c1",
          "title": "European Central Bank holds rates for the third meeting in a row",
          "newsletter_snippet": "The ECB kept rates unchanged today, and here's what it means for...",
          "category": "economy",
          "generated_at": "2026-09-04T14:02:11.000+00:00"
        }
      ]
    }
  ]
}

Categorias

GET/categories

The categories enabled for your account, with their display label.

200 OK
[
  { "code": "tech", "label": "Technology" },
  { "code": "economy", "label": "Economy" }
]

Perfil

GET/me

Your plan, quota, categories, and voice profile — useful for building your own usage dashboards.

200 OK
{
  "id": "68e9...",
  "name": "Example Times",
  "plan": "profissional",
  "max_categories": 5,
  "daily_article_limit": 40,
  "categories": ["tech", "economy"],
  "voice_profile": {
    "tone": "Direct and analytical, no sensationalism",
    "target_audience": "Tech and business professionals",
    "editorial_angle": "Practical impact for decision-makers",
    "depth_level": "Medium",
    "language": "en",
    "banned_terms": [],
    "required_terms": [],
    "custom_instructions": ""
  },
  "unreserved_daily_quota": 12
}

Webhook

If you’d rather have articles pushed to you than poll for them, set a webhook URL in Settings → Webhook. Naito calls it with POST whenever an article is created or updated — same fields as GET /articles/{id}, nested slightly differently.

Headers
X-Naito-Event: article.created
X-Naito-Signature: sha256=<hex>

X-Naito-Event is article.created or article.updated (Naito re-delivers an article when its source content changes — never a duplicate for the same story). X-Naito-Signature is an HMAC-SHA256 of the exact request body, signed with your webhook secret (also in Settings). Verify it before trusting the payload:

Verify (Python)
import hmac, hashlib

expected = "sha256=" + hmac.new(
    webhook_secret.encode(), request_body_bytes, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-Naito-Signature"]):
    raise Exception("invalid signature")
Verify (Node.js)
const crypto = require("crypto");

const expected = "sha256=" + crypto
  .createHmac("sha256", webhookSecret)
  .update(rawBody)
  .digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
  throw new Error("invalid signature");
}

Payload — same content as the REST article object, plus a CMS-ready recommendation:

POST body
{
  "id": "68f2a1c9000e77b3a4c1",
  "client_id": "68e9...",
  "client_name": "The Daily Times",
  "source_headline_uuid": "5b3f...",
  "title": "European Central Bank holds rates for the third meeting in a row",
  "meta_title": "...",
  "meta_description": "...",
  "slug": "ecb-holds-rates-third-meeting",
  "executive_summary": "...",
  "key_points": ["..."],
  "body_marked": "...",
  "body_plain": "...",
  "reading_time_minutes": 2,
  "tags": ["monetary-policy", "eurozone"],
  "category": "economy",
  "sources": ["reuters.com"],
  "image": { "url": "https://...", "alt_text": "...", "caption": "...", "generation_method": "ai_generated", "credit_line": "...", "square_url": "https://...", "portrait_url": "https://..." },
  "structured_data": { "@context": "https://schema.org", "@type": "NewsArticle", "...": "..." },
  "social_snippet": { "og_description": "...", "twitter_text": "..." },
  "newsletter_snippet": "...",
  "push_notification": { "title": "...", "body": "..." },
  "social_pack": {
    "twitter_thread": ["...", "..."],
    "linkedin_post": "...",
    "instagram_caption": "..."
  },
  "content_confidence": "high",
  "recommended_status": "published",
  "trending_client_count": 0,
  "claim_source_pairs": [
    { "claim": "The ECB held its main rate at 3.5%", "source_excerpts": ["the ECB kept its benchmark rate at 3.5%"] }
  ],
  "related_articles": [
    { "id": "68f19a...", "title": "ECB signals rate cuts are off the table for 2026", "slug": "ecb-rate-cuts-off-table-2026" }
  ],
  "is_branded_content": false,
  "sponsor_name": null,
  "cta_url": null,
  "cta_label": null,
  "ai_disclosure": { "text": "This content was produced with the help of artificial intelligence and reviewed by our editorial team.", "human_reviewed": true },
  "generated_at": "2026-09-04T14:02:11.000+00:00",
  "expires_at": "2026-12-03T14:02:11.000+00:00"
}

recommended_status is “draft” when content_confidence is “low” — a ready-made signal for your own editorial queue, so you don’t have to reimplement that interpretation yourself. Image URLs are signed and temporary; download and re-host them on delivery. AI-generated images carry the IPTCDigitalSourceType tag (TrainedAlgorithmicMedia) inside the file itself, as Google recommends — re-host the original file when you can, since re-encoding it can strip that metadata (the image’s credit_line always says it is AI-generated either way).

related_articles is up to 3 suggestions for internal linking — same category, same account, most recently published — calculated once at generation time. Each entry is only a candidate: resolve id or slug against whatever you already published on your own CMS, and skip any that never made it there. The WordPress plugin and Ghost integration already do this automatically.

claim_source_pairs maps each factual statement in the article to the exact source excerpt that supports it — worth rendering visibly on the page (even collapsed by default), not just kept as internal metadata: direct source citations are one of the strongest signals AI answer engines (ChatGPT, Perplexity, Google AI Overviews) use to decide what to cite. The WordPress plugin already does this (a collapsed "Sources" section at the end of the post).

is_branded_content and sponsor_name are set on articles created via Branded Content; every other article carries false/null.

ai_disclosure is an optional “how this content was created” line, turned on in the dashboard (Settings → AI-use notice) — null when it is off, which is the default. When present, render text at the end of the article (the WordPress plugin and Ghost integration already do). The text is localized to the article’s language unless you wrote your own, and human_reviewed is true only for articles that actually went through the Review queue — so it is safe to show the “reviewed” wording as delivered. Google recommends considering this kind of context for readers; it is a choice, not a requirement.

Feed RSS

A plain RSS 2.0 feed, if that’s simpler for your stack than a webhook or the REST API. Your feed URL (with its own access token) is in Settings.

GET/feed/{feed_token}.xml
Response (RSS 2.0)
<rss version="2.0">
  <channel>
    <title>Example Times — Naito</title>
    <link>https://api.naito.news/feed/xxxx.xml</link>
    <item>
      <title>European Central Bank holds rates for the third meeting in a row</title>
      <link>https://api.naito.news/articles/68f2a1c9000e77b3a4c1</link>
      <guid isPermaLink="false">naito:article:68f2a1c9000e77b3a4c1</guid>
      <pubDate>Fri, 04 Sep 2026 14:02:11 GMT</pubDate>
      <category>economy</category>
      <description>The ECB held its main rate steady, matching market expectations.</description>
    </item>
  </channel>
</rss>

No authentication header needed — the token in the URL is what grants access. Treat it like a secret.

Pixel de eventos

A tiny public pixel your published page calls to let Naito measure real reach per article and per category — this is what powers the aggregate benchmarks in your dashboard (how a category performs across all Naito publishers, not just yours). No auth: it’s called from the reader’s own browser.

POST/events/pageview
Request body
{ "article_id": "68f2a1c9000e77b3a4c1" }
POST/events/engagement
Request body
{
  "article_id": "68f2a1c9000e77b3a4c1",
  "read_seconds": 47,
  "scroll_pct": 82
}

Send the pageview on load, and one engagement call when the reader leaves or finishes the article.

Integrações prontas

WordPress

Install the Naito Connector plugin, go to Settings → Naito, paste your webhook secret, and copy the plugin’s REST URL back into your Naito dashboard’s webhook field. From then on, every delivery creates or updates a post automatically — category, tags, featured image, and Yoast/RankMath SEO fields (whichever you have installed) all filled in. Already-published posts are never silently downgraded back to draft on update. Tested live against WordPress and Newspack, the WordPress distribution built for independent newsrooms.

Ghost

No plugin needed — Naito talks directly to your Ghost Admin API. In Ghost Admin, go to Settings → Integrations → Add custom integration, name it “Naito”, and paste the generated Admin API Key into your Naito dashboard. Delivery matches on the article’s slug, so updates land on the same post instead of duplicating it, and a manually-published post is never reverted to draft by an update.

Neither channel requires the other — use webhook delivery to your own CMS, the Ghost integration, the RSS feed, or the REST API on their own or side by side.

Precisa de algo que não está aqui? Fale com a gente