Ocoya
Developer

Publish to anything you own.

A Custom destination is a network Ocoya does not need to know about. Give it a URL and it receives your finished post as JSON, the same way a post goes to Instagram, except the other end is yours.

What it is for

The destination for everything else.

Most of what you publish goes to a network with a logo. This is for the rest.

Tune in
Your own blog

Articles written in Ocoya arrive at your own site or CMS, whatever it is built with.

Connecting it

A URL, a name, and usually one header.

You own both ends, so there is no sign-in screen to click through. Paste the address, add any header your endpoint expects, and the destination shows up in the composer beside your other channels.

  • Any public URL that accepts a POST
  • Up to 10 headers, for an API key or a token
  • Appears in the composer like any other channel
NameOptional
My blog

How this destination is labelled in your channel list.

Publish URLRequired
https://example.com/hooks/content

Every scheduled post is sent here as a POST with a JSON body.

Delete URLOptional
https://example.com/hooks/content/delete

Called only when you delete a post in Ocoya. Leave it empty to keep your copy.

HeadersOptional
Authorization: Bearer ••••••

Sent with every request, for an endpoint that expects an API key or a token.

Connect destination
The payload

One POST, with just what a blog needs.

The body is JSON: the title, the caption exactly as you wrote it (Markdown for an article), one cover image, and an article's slug, excerpt, tags and SEO fields when it has them. Nothing else, so there is nothing to ignore.

POSTContent-Type: application/json
{
"event": "post.published",
"publishedAt": "2026-11-04T09:00:00.000Z",
"post": {
"id": "cm4x2k9a0001",
"title": "Why your content is not converting",
"caption": "## The three pillars\n\n…",
"image": "https://…/cover.png",
"options": {
"markdownMode": true,
"slug": "why-your-content-is-not-converting",
"excerpt": "…",
"seoTitle": "…",
"seoDescription": "…",
"tags": ["content", "strategy"],
"readingMinutes": 6
}
}
}
Read the payload reference

The composer preview shows this exact body before you schedule, except the id and the time.

Your answer

Reply 2xx with an id and a url.

Ocoya waits up to 30 seconds for the answer. A post counts as published when you reply with a 2xx status and JSON that has both an id and a url. The url becomes the post's link, and the id is what Ocoya sends back if you delete the post.

POSTyour endpoint
200 { "id": "a1b2c3", "url": "https://…" }
Published
POSTyour endpoint
200 { }
Failed: no id or url. It may already exist, so check first.
POSTyour endpoint
400 { "error": "title is missing" }
Failed. Ocoya shows you the message your endpoint sent.
POSTyour endpoint
no answer in 30 s
Failed. Not retried, so nothing is sent twice.
24 htry 1+5 min+30 min+2 h+6 h+12 h{ }503, waiting200 OK
When it is busy

A busy endpoint gets another chance.

If your endpoint answers 429, 502, 503 or 504, Ocoya waits and tries again on its own: after 5 minutes, then 30 minutes, 2 hours, 6 hours and 12 hours. While it waits, the post stays scheduled.

  • It stops when a post would go out more than 24 hours late.
  • Other errors and timeouts are not retried. A timeout may already have published, and a second try could post it twice.
  • The post id is the same on every try.
Deleting

Delete it there when you delete it here.

Add an optional Delete URL. When you delete a post in Ocoya, it sends a post.deleted request there with the id your endpoint gave you at publish. Leave it empty and your copy stays.

When it was published, your reply{ "id": "a1b2c3", "url": "https://…" }
You delete the post in Ocoya
The request to your Delete URL
POST /hooks/content/delete
{
  "event": "post.deleted",
  "id": "a1b2c3",
  "deletedAt": "2026-11-04T10:12:00.000Z",
  "post": { … }
}
Your copy
Safety

It is your endpoint, dialled carefully.

A destination is an address Ocoya connects to from its own network, so it is treated like one.

192.168.0.12localhostapi.yourshop.comRefusedAllowed
  • Private and internal addresses are refused.
  • The connection is pinned to the public address your host resolved to.
  • A redirect is followed only after the new address passes the same check, up to 3 times.
  • Header values stay on our servers. The browser only sees their names.
In the composer

It sits in the composer like any other channel.

Your destination shows up beside Instagram and LinkedIn. Pick it, set the day and the hour, and it is sent at the minute you chose, along with the rest.

  • Schedule it
  • See it on the calendar
  • Point a blog campaign at it

The composer preview shows the exact request body first.

Questions

Good to know before you connect.

RXAccept a POST with a JSON body and reply with a 2xx status and JSON that has an id and a url. It must be reachable from the internet, because Ocoya calls it from its own servers.

RXAdd a header such as Authorization or X-API-Key under Advanced options and check it in your code. Ocoya sends it with every request. Requests are not signed.

RXIf it answers 429, 502, 503 or 504, Ocoya tries again after 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, as long as the post would not be more than 24 hours late. Other errors fail right away and show you the status and your endpoint's message.

RXNo. Ocoya only connects to public addresses. Private and internal addresses are refused.

RXIt sends one image, the first still of the post, as a link. Video is not sent.

Publish it wherever it belongs.

Connect a destination in a minute and point your next campaign at it.