Developer API

Customer site integration API

Your site pulls the posts Trina has ready, publishes them itself, and reports the live URL back. Trina then checks the live page, the same way it checks any post you publish.

How it works

  1. Issue a key in the console: Settings → Channel setup → Customer site integration API. Only the brand owner can issue or revoke keys.
  2. Your server calls GET /posts?status=ready and then GET /posts/{id} for the Markdown and HTML body.
  3. Publish the post on your site.
  4. Call POST /posts/{id}/published with the live URL. The post moves to published and Trina starts checking it.

Authentication

Send the key as a Bearer token on every request. A key belongs to one brand and only ever sees that brand's posts.

Authorization: Bearer trn_live_…
  • The full key is shown once, when it is issued. Trina stores only a hash, so a lost key cannot be recovered - revoke it and issue a new one.
  • Keep the key on your server (for example in an environment variable). Never put it in browser code or a public repository.
  • A revoked key stops working immediately (401). A brand can have up to 5 active keys. The same limits apply on every plan.

Endpoints

Base URL: https://trina.app/api/v1

List postsGET /posts

Newest first, without the body. Query parameters (all optional):

statusready (default) or published
sinceISO 8601 date-time. ready: created at or after it. published: published at or after it.
channelblog (default) - your own site
limit1–100, default 50
curl -s "https://trina.app/api/v1/posts?status=ready&since=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer $TRINA_API_KEY"
{
  "posts": [
    {
      "id": "d-2026-10-16-blog-1",
      "title": "치아 미백, 집에서 해도 될까",
      "status": "ready",
      "channel": "blog",
      "excerpt": "치아 미백을 집에서 할 때 확인할 것들을 정리했습니다…",
      "cover_image_url": "https://…/cover.png",
      "seo_score": 82,
      "scheduled_for": null,
      "created_at": "2026-10-16T01:00:00.000Z",
      "published_at": null,
      "post_url": null,
      "verify_state": null
    }
  ]
}

Get one postGET /posts/{id}

Returns the body as markdown and as html. The HTML carries inline styles so it reads well without extra CSS; raw HTML inside a draft is dropped, never passed through.

curl -s "https://trina.app/api/v1/posts/d-2026-10-16-blog-1" \
  -H "Authorization: Bearer $TRINA_API_KEY"
{
  "post": {
    "id": "d-2026-10-16-blog-1",
    "title": "치아 미백, 집에서 해도 될까",
    "status": "ready",
    "markdown": "## 집에서 하는 미백…",
    "html": "<h2 style=\"…\">집에서 하는 미백…</h2>…",
    "canonical_url": null,
    "verify_note": null,
    "verify_at": null,
    "…": "plus every field from the list"
  }
}

Report the published URLPOST /posts/{id}/published

Send the full https:// URL of the live page. The post is marked published (post_url, published_at) and verify_state becomes checking; Trina then checks the page and sets it to confirmed or mismatch. Sending a new URL later replaces the old one.

curl -s -X POST "https://trina.app/api/v1/posts/d-2026-10-16-blog-1/published" \
  -H "Authorization: Bearer $TRINA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.example.com/blog/teeth-whitening-at-home"}'
{
  "post": {
    "id": "d-2026-10-16-blog-1",
    "status": "published",
    "published_at": "2026-10-16T05:12:44.120Z",
    "post_url": "https://www.example.com/blog/teeth-whitening-at-home",
    "verify_state": "checking",
    "…": "…"
  }
}

Post fields

idPost id, used in the paths above
titleTitle
statusready or published
excerptPlain-text summary for a meta description (about 160 characters)
cover_image_urlCover image URL, or null
scheduled_forPlanned publish date (YYYY-MM-DD), or null. Publish on or after it.
post_urlThe URL you reported, or null
verify_statenull · checking · confirmed · mismatch · not_found · wrong_channel
markdown / htmlBody (single-post response only)

Errors

Errors are JSON: { "error": "code", "hint"?: "…" }.

400invalid_status · invalid_since · invalid_channel · invalid_limit · invalid_json · url_not_https
401missing_api_key · invalid_api_key (unknown, malformed or revoked key)
403forbidden - the post belongs to another brand
404post_not_found
422wrong_channel - the URL's host belongs to a different channel (e.g. a Naver link for a blog post)
500internal_error - retry later
HTTP/1.1 403
{ "error": "forbidden", "hint": "This post belongs to another brand." }

Coming next

Push delivery (a webhook to your site when a post is ready) is planned. Until then, poll GET /posts?status=ready on a schedule - every 15–60 minutes is plenty.