Home / Docs / OAuth for apps
OAuth for apps

Sign in with Postozo in your app

Postozo is an OAuth 2.1 authorization server. AI apps use it to connect to the MCP server, and your own app can use it so people never paste a key.

Developer docs: Overview, API guide, API reference, MCP server, Webhooks, CLI, OAuth for apps. Machine readable: openapi.json.

1. Discovery

Everything starts from the metadata. MCP apps find it through the 401 answer of /mcp.

AddressWhat
https://app.postozo.com/.well-known/oauth-authorization-serverAuthorization server metadata (RFC 8414)
https://app.postozo.com/.well-known/oauth-protected-resourceProtected resource metadata for the MCP server (RFC 9728). Also at /.well-known/oauth-protected-resource/mcp.
{
  "issuer": "https://app.postozo.com",
  "authorization_endpoint": "https://app.postozo.com/oauth/authorize",
  "token_endpoint": "https://app.postozo.com/oauth/token",
  "registration_endpoint": "https://app.postozo.com/oauth/register",
  "revocation_endpoint": "https://app.postozo.com/oauth/revoke",
  "device_authorization_endpoint": "https://app.postozo.com/oauth/device/code",
  "response_types_supported": [
    "code"
  ],
  "grant_types_supported": [
    "authorization_code",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ],
  "token_endpoint_auth_methods_supported": [
    "none",
    "client_secret_post"
  ],
  "scopes_supported": [
    "mcp"
  ]
}

2. Register your app

Dynamic client registration (RFC 7591). No account is needed. Limit: 30 an hour per IP address.

curl -X POST "https://app.postozo.com/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{"client_name": "My app", "redirect_uris": ["https://example.com/callback"]}'
{
  "client_id": "plc_rb3RYj6DyEktL7Jb",
  "client_name": "My app",
  "redirect_uris": [
    "https://example.com/callback"
  ],
  "grant_types": [
    "authorization_code"
  ],
  "response_types": [
    "code"
  ],
  "token_endpoint_auth_method": "none",
  "client_id_issued_at": 1790706315
}
  • redirect_uris is required. Each must be https, http only on localhost, 127.0.0.1 or [::1], or your app's own scheme, for example cursor://..., vscode://..., claude://... or com.example.app:/callback. http on any other host is refused, and so are other web and script schemes (ws:, wss:, ftp:, file:, javascript:, data:, blob: and others) and addresses with a # part. A refused address gets 400 {"error": "invalid_redirect_uri", "error_description": "http is only allowed on localhost, 127.0.0.1 or [::1]. Use https: http://example.com/callback", "msg": "..."}.
  • client_name is shown on the Allow screen (up to 100 characters).
  • The default is a public app with no secret (token_endpoint_auth_method: "none"). Send "token_endpoint_auth_method": "client_secret_post" to get a client_secret (pls_...), shown only in this answer.
  • The answer is 201. Save the client_id; there is no endpoint to read or change it later. Register again to change the redirect addresses.

3. Send the person to sign in (PKCE)

Make a random code_verifier, and its code_challenge: the SHA-256 of the verifier, base64url without padding. Then open:

https://app.postozo.com/oauth/authorize?response_type=code&client_id=plc_rb3RYj6DyEktL7Jb&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&code_challenge=CHALLENGE&code_challenge_method=S256&state=RANDOM
  • response_type must be code; code_challenge is required and the method must be S256; redirect_uri must be 1 of the registered addresses, exactly.
  • The person signs in if needed, sees "Allow My app to use Postozo?" with what it can do, and presses Allow or Cancel.
  • Allow sends them to redirect_uri?code=...&state=.... Cancel sends error=access_denied. Always check state.
  • The code works once and for 5 minutes.

4. Get the token

curl -X POST "https://app.postozo.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=CODE&client_id=plc_rb3RYj6DyEktL7Jb&redirect_uri=https://example.com/callback&code_verifier=VERIFIER"
{
  "access_token": "pz_oat_...",
  "token_type": "Bearer",
  "scope": "mcp"
}

The body can be form data or JSON. Apps with a secret add client_secret. The answer has Cache-Control: no-store. Errors are standard OAuth JSON with msg added (the same text as error_description), for example 400 {"error": "invalid_grant", "error_description": "PKCE check failed", "msg": "PKCE check failed"}. A failed attempt uses up the code, so start again from step 3. Limit: 300 token requests an hour per IP address.

5. Use the token

curl "https://app.postozo.com/public/v1/integrations" -H "Authorization: Bearer pz_oat_..."

The token works on the public API and the MCP server. It acts as the person who approved, with their role in the workspace they were in. It has no expiry date.

Scopes

There is 1 scope, mcp, and every token gets it. It means: see the workspace's channels and posts and schedule posts, as the person could in the app. There are no read-only tokens yet; to limit what a token can do, have a person with a smaller role (for example an editor) approve it.

Revoke

  • The person: Settings, Approved Apps, Revoke. It works at once.
  • Your app: POST /oauth/revoke with {"token": "pz_oat_..."} (JSON or form). The answer is always 200 {}, also for unknown tokens.

Device flow (for apps without a browser)

This is how the CLI signs in. No registration is needed.

curl -X POST "https://app.postozo.com/oauth/device/code"
{
  "device_code": "LcQTPvnhPPykIrjAIF9_ZVKZCIQfjXbKEU-WUTt_H2w",
  "user_code": "GRGG-WWDV",
  "verification_uri": "https://app.postozo.com/device",
  "verification_uri_complete": "https://app.postozo.com/device?code=GRGG-WWDV",
  "expires_in": 600,
  "interval": 3
}

Show the person the link and the code. Then ask for the token every interval seconds:

curl -X POST "https://app.postozo.com/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "LcQTPvnhPPykIrjAIF9_ZVKZCIQfjXbKEU-WUTt_H2w"}'
AnswerMeaning
400 authorization_pendingNot approved yet. Ask again after the interval.
400 access_deniedThe person pressed Deny.
400 expired_tokenThe 10 minutes are over. Start again.
400 invalid_grantUnknown device code, or the token was already collected.
200{ "access_token": "pz_cli_...", "token_type": "Bearer", "scope": "mcp" }. The token shows as "Command line" in Approved Apps.

Limit: 30 device codes an hour per IP address.

FAQ

Questions

Do I need to register my app by hand?

No. Register it with 1 request to /oauth/register. There is no developer console.

Are there refresh tokens?

No. Access tokens do not expire. They stop working when the person revokes the app in Settings, Approved Apps, or when you call /oauth/revoke.

What can a token do?

Everything the person can do in that workspace with their role, through the public API and the MCP server. There is 1 scope: mcp.

Which workspace does the token use?

The workspace the person was in when they pressed Allow. To use another workspace, they switch in the app and sign in to your app again.

Is PKCE required?

Yes, with S256, for every app, including apps with a client secret.

Get your API key

Start a 7-day free trial. The API, MCP server, webhooks and CLI are in every plan.

Create your free account