Search

Search the docs…

API

Authentication

Every request carries a bearer credential: a project API key for your own scripts, or an OAuth access token when a user authorizes an AI client or your app.

CredentialPrefixReachesLifetime
API keyptk_One projectUntil deleted
OAuth access tokenptat_Every project of the user1 hour
OAuth refresh tokenptrt_Gets new tokens60 days, single use
Authorization codeptcode_Exchanged for tokens5 minutes, single use

API keys

Create and delete keys in the app under Connect AI → Project keys (Conectar IA → Claves de proyecto) (up to 20 per project). The key is shown once; Postika only stores its SHA-256 hash. Send it in the Authorization header, or in X-Api-Key if your tool can't set bearer tokens.

curl https://postika.app/api/v1/projects \
  -H "Authorization: Bearer ptk_…"

A key is bound to its project, so project_id is never required with a key. Delete a key in the app to revoke it immediately.

OAuth 2.1

Postika is its own authorization server for the MCP server and the REST API. It implements the authorization code grant with PKCE S256 required, dynamic client registration and rotating refresh tokens. One scope exists: mcp.

  • MCP clients (Claude, ChatGPT, Cursor, VS Code…) do all of this automatically. You only need this section to build your own client or an OAuth app.
  • An access token reaches every project the user belongs to. When the user has more than one, pass project_id; call GET /api/v1/projects to list them.
  • The subscription is checked per project on every request: what counts is the subscription of the project's owner, not the one of whoever connected. A token keeps working on active projects and gets a clear subscription_inactive error on the others (see Errors). With an API key the same rule applies to the key's project.
  • What the token can do inside a project follows the user's role: editors' scheduled or immediate posts are saved as pending approval, and only owners and admins can approve. API keys act as admins.

Discovery

EndpointSpecReturns
/.well-known/oauth-protected-resourceRFC 9728The MCP resource and its authorization server
/.well-known/oauth-authorization-serverRFC 8414Authorization server metadata
/.well-known/openid-configuration—Same metadata, for clients that only look here (no ID tokens)
GET /.well-known/oauth-authorization-server
{
  "issuer": "https://postika.app",
  "authorization_endpoint": "https://postika.app/oauth/authorize",
  "token_endpoint": "https://postika.app/api/oauth/token",
  "registration_endpoint": "https://postika.app/api/oauth/register",
  "revocation_endpoint": "https://postika.app/api/oauth/revoke",
  "response_types_supported": [
    "code"
  ],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ],
  "token_endpoint_auth_methods_supported": [
    "none",
    "client_secret_post",
    "client_secret_basic"
  ],
  "scopes_supported": [
    "mcp"
  ]
}

Dynamic client registration

POST/api/oauth/register

RFC 7591. Public clients register themselves without a secret (token_endpoint_auth_method: none, PKCE does the rest). Apps you create in the Developers area get a secret instead. Registration grants no access by itself: the user still has to click Allow (Permitir). It is limited to 20 registrations per IP per hour (429 after that), and clients that never complete a connection are deleted after 7 days.

redirect_urisstring[]required
1 to 5 URIs, max 500 characters each. https://, http://localhost / 127.0.0.1 / [::1], or a custom app scheme such as cursor://. No fragments, no user:password, no javascript:/data:.
client_namestring
Shown to the user on the consent screen, marked as self-reported. Control and direction characters are stripped; up to 60 characters.
grant_typesstring[]
Optional. Only authorization_code and refresh_token.
201 Created
{
  "client_id": "ptc_9Qm2…",
  "client_name": "My agent",
  "redirect_uris": [
    "http://127.0.0.1:33418/callback"
  ],
  "grant_types": [
    "authorization_code",
    "refresh_token"
  ],
  "response_types": [
    "code"
  ],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1762336800
}

Authorization request

GET/oauth/authorize

Send the user's browser here. Postika asks them to sign in, shows the consent screen and redirects back.

response_typestringrequired
Always code.
client_idstringrequired
Your ptc_ client ID.
redirect_uristringrequired
One of the registered URIs. For loopback URIs any port is accepted if host and path match.
code_challengestringrequired
BASE64URL(SHA-256(code_verifier)), 43 to 128 characters.
code_challenge_methodstringrequired
Must be S256.
statestring
Returned unchanged. Use it to prevent CSRF.
resourcestring
RFC 8707. Optional; if sent it must be https://postika.app/api/mcp.

The consent screen shows the client name (marked as not verified), the host the browser will return to, the signed-in email, the projects the token will reach (all of the user's, including future ones) and what it can do. The decision is a same-origin server action, so another site can't submit it for the user, and the page can't be framed.

On approval the user lands on redirect_uri?code=ptcode_…&state=…&iss=…. If they decline: error=access_denied. The code is valid for 5 minutes and works once; it is bound to the client, the redirect URI and the PKCE challenge. If a used code is presented again, every token issued from it is revoked.

Token exchange

POST/api/oauth/token

Accepts application/x-www-form-urlencoded or JSON. client_id is always required. Clients with a secret authenticate with client_secret_post or HTTP Basic (client_secret_basic). An optional resource must be the MCP URL (invalid_target otherwise).

curl https://postika.app/api/oauth/token \
  -d grant_type=authorization_code \
  -d code=ptcode_… \
  -d client_id=ptc_… \
  -d redirect_uri=https://example.com/callback \
  -d code_verifier=…
200 OK
{
  "access_token": "ptat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "ptrt_…",
  "scope": "mcp"
}

Generating the PKCE pair

import { createHash, randomBytes } from "node:crypto"

const verifier = randomBytes(32).toString("base64url")
const challenge = createHash("sha256").update(verifier).digest("base64url")

Refresh and rotation

Access tokens last one hour. Exchange the refresh token for a new pair before or after it expires; client_id is required and must be the client the token was issued to (a token sent by another client is rejected and left untouched). Refresh tokens rotate: each one works once, and using it immediately invalidates the previous access token too.

curl https://postika.app/api/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=ptrt_… \
  -d client_id=ptc_…
Store the new refresh token atomically. If two workers refresh with the same token, the second gets invalid_grant and the user has to authorize again.

Revocation

POST/api/oauth/revoke

RFC 7009. Send token= with an access or refresh token, form-encoded. The whole pair is revoked. If you also send client_id (and the secret, for apps that have one), only that client's tokens are touched. The endpoint answers 200 whether or not the token existed. Users can also disconnect any AI in Connect AI → Connected AIs (Conectar IA → IAs conectadas).

curl https://postika.app/api/oauth/revoke -d token=ptrt_… \
  -d client_id=ptc_…

Security notes

  • Keys, tokens, codes and secrets are stored only as SHA-256 hashes; secrets and PKCE are compared in constant time.
  • The API and the MCP server only accept bearer credentials, never cookies, so CORS is open and cross-site requests can't act for a signed-in user.
  • Rate limits: 120 requests per minute per key or token, and 60 per minute per IP for requests without a valid credential.
  • Every API and MCP call is logged with the tool name, project and result code (never the content) for 90 days.