ONPChat API v1
FA EN AR

ONPChat API

Publish to your own channels, groups, classes and santrals — from any app or server.

https://api.onpchat.ir

Download openapi.yaml

An OpenAPI 3.1 file: import it straight into Postman or Insomnia, or generate an SDK for your language from it.

curl https://api.onpchat.ir/v1/me \
  -H "Authorization: Bearer $ONP_TOKEN"

Quick start

  1. Get a token. Sign in with an account that administers the space, then open:

    The id includes its prefix and must be URL-encoded — channel #1786551855736 becomes %231786551855736. If no token exists yet, one is created right there.

  2. Check the token.
  3. Send a message.
1
https://onpchat.ir/api/space/api-token?entity_id=<id>
curl -X POST https://api.onpchat.ir/v1/channels/%231786551855736/messages \
  -H "Authorization: Bearer $ONP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Hello from the API"}'

Authentication

Each space has its own token, valid only for that space. Send it in the header:

Cookies and ?token= are not accepted. This API is for servers, not browsers, and sends no CORS headers. Never put the token in client-side JavaScript — any visitor could take it and post as your space. If a token leaks, revoke it from the same panel; a new one replaces it immediately.
Header
Authorization: Bearer onp_2e09e8f0bb35088c795e5f75dc...

Response shape

Branch on error.code, not on message. The message is for humans and may change; the code will not.

Response
{ "ok": true,  "result": { ... } }

{ "ok": false, "error": {
    "code": "invalid_token",
    "message": "..."
} }

Spaces

There are four kinds of space, each with its own prefix. The prefix is part of the id and must be URL-encoded.

KindIn pathPrefixSender visible?
Channelchannels#%23No — posted as the channel
Groupgroups@%40Yes
Classclasses%%25Yes
Santralsantrals&%26Yes

Endpoints

MethodPathWhat it does
GET/v1/meWhich space this token belongs to
GET/v1/{type}/{id}Space details
GET/v1/{type}/{id}/messagesMessage history
POST/v1/{type}/{id}/messagesPublish a message

History

Newest first. Pagination uses seq, not time — a monotonic counter that gives a definite order even for two messages sent in the same millisecond. Pass next_cursor as the next page's before_seq; null means you have reached the end.

Request
GET /v1/channels/%231786551855736/messages
      ?limit=30&before_seq=42
Response
{
  "ok": true,
  "result": {
    "messages": [
      {
        "id": "a1b2c3",
        "text": "...",
        "seq": 42,
        "timestamp": 1788301362311
      }
    ],
    "next_cursor": 41
  }
}

Cards (rich posts)

Instead of raw text you can send a card — the same thing the app builds under «New card»: cover, title, text and link in one clean frame.

Send card instead of text. They are mutually exclusive; text that accompanies the card goes in caption.

FieldRequiredDescription
urlyesWhere the card links to
titlenoCard title
descriptionnoText inside the card
imagenoCover — must be a full http/https URL
captionnoText outside the card, in your own voice

The image must be hosted somewhere reachable from the internet; uploads are not in this version. Relative URLs are rejected, because the card renders on the reader's phone where such a path resolves to nothing.

curl -X POST https://api.onpchat.ir/v1/groups/%401786579451315/messages \
  -H "Authorization: Bearer $ONP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "card": {
      "url": "https://example.com/post",
      "title": "...",
      "description": "...",
      "image": "https://example.com/cover.jpg",
      "caption": "..."
    }
  }'

Don't send duplicates

If a request times out you cannot tell whether it landed. Send an Idempotency-Key so a retry does not create a second message:

Same key, same message. The key also becomes the message's id.

Header
Idempotency-Key: order-2026-09-02-001

Rate limits

Per token, per minute:

OperationLimit
Publish20
Read120

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. On 429 you also get Retry-After — wait that long, not less.

Response
HTTP/2 429
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
Retry-After: 37

Errors

HTTPerror.codeMeaning
400empty_messagetext is empty
400bad_requestInvalid body or parameter
401missing_tokenNo Authorization header
401invalid_tokenToken not recognised
401revoked_tokenToken has been revoked
403entity_mismatchThis token belongs to another space
403wrong_entity_kindSpace kind in the path does not match the token
403missing_scopeToken lacks this scope
403sender_not_memberThe token's creator is no longer a member
403read_onlyThe channel is read-only
404entity_not_foundSpace not found
413message_too_longText longer than 8000 characters
429rate_limitedRate limit reached
Getting sender_not_member? The token is bound to the admin who created it. If that person left the space, publishing stops. Fix: have a current admin regenerate the token from the panel.

Not in this version

Sending images and files, deleting messages via the API, and webhooks (being notified of space events) are not here yet. Tokens already carry a scopes field, so when webhooks arrive your token will not be invalidated.