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.
| Credential | Prefix | Reaches | Lifetime |
|---|---|---|---|
| API key | ptk_ | One project | Until deleted |
| OAuth access token | ptat_ | Every project of the user | 1 hour |
| OAuth refresh token | ptrt_ | Gets new tokens | 60 days, single use |
| Authorization code | ptcode_ | Exchanged for tokens | 5 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; callGET /api/v1/projectsto 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_inactiveerror 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
| Endpoint | Spec | Returns |
|---|---|---|
| /.well-known/oauth-protected-resource | RFC 9728 | The MCP resource and its authorization server |
| /.well-known/oauth-authorization-server | RFC 8414 | Authorization server metadata |
| /.well-known/openid-configuration | — | Same metadata, for clients that only look here (no ID tokens) |
{
"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
/api/oauth/registerRFC 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 ascursor://. No fragments, no user:password, nojavascript:/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_codeandrefresh_token.
{
"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
/oauth/authorizeSend 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
/api/oauth/tokenAccepts 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=…{
"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_…invalid_grant and the user has to authorize again.Revocation
/api/oauth/revokeRFC 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.