Partner apps (OAuth)
Apps that act for other users: register, exchange tokens, disconnect.
List your partner apps #
/v1/partner-apps app sessionThe partner apps the signed-in user registered, oldest first, disabled ones included (disabled: true).
More about this
Client secrets are never shown again.
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl "https://cirrus.pocketful.in/v1/partner-apps" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://cirrus.pocketful.in/v1/partner-apps",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/partner-apps', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": [
{
"client_id": "cp_NyW7jEES7vNrNQLnkQVu",
"owner": "user-01M3PGPS02F2C5YKT5ZPGX3G7Z",
"name": "Charts Co",
"verified": false,
"logo_url": "https://partner.example/logo.png",
"redirect_uris": [
"https://partner.example/callback"
],
"scopes": [
"read",
"orders"
],
"created_at": "2026-09-29T12:02:21.867180+00:00",
"disabled": false
}
],
"error": null
}Register a partner app #
/v1/partner-apps app sessionRegisters an OAuth client owned by the signed-in user (at most 10 active apps per user).
More about this
The response carries client_secret once: store it now, it cannot be shown again. New apps are unverified.
Body fields
- name string required
Name shown to users on the consent screen (1-80 characters).
- redirect_uris string[] required
1-10 redirect URIs, matched exactly (no prefixes, no wildcards):
https://, orhttp://localhost/http://127.0.0.1for development; no fragment, at most 512 characters. - scopes string[] required
The most a user can grant the app (at least one).
One of: read, orders, triggers
- logo_url string | null
HTTPS URL of the app's logo.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | malformed_json | The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types). |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | own_session_required | Credentials can only be created from the user's own signed-in session. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 422 | invalid_request | The request is well-formed but not valid; |
| 422 | invalid_scope | OAuth: a requested scope is unknown or not allowed for the app. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://cirrus.pocketful.in/v1/partner-apps" \
-H "Authorization: token $CIRRUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Charts Co",
"redirect_uris": [
"https://partner.example/callback"
],
"scopes": [
"read",
"orders"
],
"logo_url": "https://partner.example/logo.png"
}'import os
import requests
response = requests.post(
"https://cirrus.pocketful.in/v1/partner-apps",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
json={
"name": "Charts Co",
"redirect_uris": [
"https://partner.example/callback",
],
"scopes": [
"read",
"orders",
],
"logo_url": "https://partner.example/logo.png",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/partner-apps', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Charts Co",
"redirect_uris": [
"https://partner.example/callback"
],
"scopes": [
"read",
"orders"
],
"logo_url": "https://partner.example/logo.png"
}),
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"client_id": "cp_NyW7jEES7vNrNQLnkQVu",
"owner": "user-01M3PGPS02F2C5YKT5ZPGX3G7Z",
"name": "Charts Co",
"verified": false,
"logo_url": "https://partner.example/logo.png",
"redirect_uris": [
"https://partner.example/callback"
],
"scopes": [
"read",
"orders"
],
"created_at": "2026-09-29T12:02:21.867180+00:00",
"disabled": false,
"client_secret": "cps_HGHO7T2LTif6fzJQekv73YfbEqDF3gxxxiuB447BQgenTFyH",
"note": "Store the client secret now: it is not shown again."
},
"error": null
}Set a partner app's webhook #
/v1/partner-apps/{client_id} app sessionSets, changes, turns off or removes the webhook of one of the user's own active apps.
More about this
For every user connected to the app it gets order_update and trade for orders the app placed, account_alert for the user's accounts (also with kind account_removed when an account is no longer linked) and connection_revoked when a connection ends. Each body also carries username (top level): the bodies are PartnerWebhookOrderUpdate, PartnerWebhookTrade, PartnerWebhookAccountAlert (kinds PartnerWebhookAlertKind) and WebhookConnectionRevoked, together PartnerWebhookBody. Signing, retries and turning off after repeated failures are as for a user's webhooks (see webhooks). Deliveries to partner apps start only once the server turns on partner webhooks; until then this route answers 503. The first webhook_url creates the webhook and returns webhook_secret once; webhook_url: null removes it. Own session only.
Parameters
- client_id path · string required
The app's client id (
cp_...).
Body fields
- webhook_url string | null
The receiver: an
https://URL on a public host (private, loopback and link-local addresses are refused). The first one creates the webhook and its secret;nullremoves the webhook. - webhook_events string[] | null
Events to send, at least one of
order_update,trade,account_alert,connection_revoked(all four when a webhook is created without this).One of: order_update, trade, account_alert, connection_revoked
- webhook_enabled boolean | null
falseturns deliveries off;trueturns them back on (also after the webhook was turned off for failing).
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | malformed_json | The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types). |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | own_session_required | Credentials can only be created from the user's own signed-in session. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | unknown_app | Unknown or disabled partner app. |
| 409 | conflict | A partner app's webhook was changed at the same time as this request; retry. |
| 422 | invalid_request | The request is well-formed but not valid; |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X PATCH "https://cirrus.pocketful.in/v1/partner-apps/<client_id>" \
-H "Authorization: token $CIRRUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://partner.example/hook",
"webhook_events": [
"order_update",
"trade",
"account_alert",
"connection_revoked"
]
}'import os
import requests
response = requests.patch(
"https://cirrus.pocketful.in/v1/partner-apps/<client_id>",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
json={
"webhook_url": "https://partner.example/hook",
"webhook_events": [
"order_update",
"trade",
"account_alert",
"connection_revoked",
],
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/partner-apps/<client_id>', {
method: 'PATCH',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"webhook_url": "https://partner.example/hook",
"webhook_events": [
"order_update",
"trade",
"account_alert",
"connection_revoked"
]
}),
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"client_id": "cp_nTYrRysBvVEizO0aMNCX",
"owner": "user-01M4Z65SRSKA2K7SGT33094P5V",
"name": "Charts Co",
"verified": false,
"logo_url": "https://partner.example/logo.png",
"redirect_uris": [
"https://partner.example/callback"
],
"scopes": [
"read",
"orders"
],
"created_at": "2026-10-15T07:07:11.535867+00:00",
"disabled": false,
"webhook": {
"url": "https://partner.example/hook",
"events": [
"order_update",
"trade",
"account_alert",
"connection_revoked"
],
"enabled": true,
"disabled_reason": null,
"consecutive_failures": 0,
"last_success_at": null,
"last_failure_at": null,
"previous_secret_expires_at": null,
"secret_rotated_at": null
},
"webhook_secret": "whsec_…",
"note": "Store the webhook secret now: it is not shown again."
},
"error": null
}Disable a partner app #
/v1/partner-apps/{client_id} app sessionDisables one of the user's own apps: every token issued to it stops working at once, for every user who connected it.
More about this
Cannot be undone.
Parameters
- client_id path · string required
The app's client id (
cp_...).
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | unknown_app | Unknown or disabled partner app. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X DELETE "https://cirrus.pocketful.in/v1/partner-apps/<client_id>" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.delete(
"https://cirrus.pocketful.in/v1/partner-apps/<client_id>",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/partner-apps/<client_id>', {
method: 'DELETE',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"client_id": "cp_NyW7jEES7vNrNQLnkQVu",
"disabled": true
},
"error": null
}Rotate a partner app's client secret #
/v1/partner-apps/{client_id}/rotate-secret app sessionIssues a new client secret (shown once).
More about this
The previous secret keeps working until previous_secret_expires_at (24 hours), so the partner can roll the new one out without downtime; rotating again replaces it. Own session only.
Parameters
- client_id path · string required
The app's client id (
cp_...).
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | own_session_required | Credentials can only be created from the user's own signed-in session. |
| 404 | unknown_app | Unknown or disabled partner app. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://cirrus.pocketful.in/v1/partner-apps/<client_id>/rotate-secret" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.post(
"https://cirrus.pocketful.in/v1/partner-apps/<client_id>/rotate-secret",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/partner-apps/<client_id>/rotate-secret', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"client_id": "cp_thoAw2SKE8Q8mv2sno0m",
"client_secret": "cps_eu0PNR6P8S3lBc6aiWLr7fHtwZjdBiNSF9jmPF6zd9G1zosO",
"previous_secret_expires_at": "2026-09-30T12:02:21Z"
},
"error": null
}Rotate a partner app's webhook secret #
/v1/partner-apps/{client_id}/rotate-webhook-secret app sessionIssues a new webhook signing secret (shown once).
More about this
Deliveries are signed with both the new and the previous secret until previous_secret_expires_at (24 hours), so the receiver can switch without downtime. Own session only. Answers 503 until the server turns on partner webhooks.
Parameters
- client_id path · string required
The app's client id (
cp_...).
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | own_session_required | Credentials can only be created from the user's own signed-in session. |
| 404 | unknown_app | Unknown or disabled partner app. |
| 409 | conflict | A partner app's webhook was changed at the same time as this request; retry. |
| 422 | invalid_request | The request is well-formed but not valid; |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://cirrus.pocketful.in/v1/partner-apps/<client_id>/rotate-webhook-secret" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.post(
"https://cirrus.pocketful.in/v1/partner-apps/<client_id>/rotate-webhook-secret",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/partner-apps/<client_id>/rotate-webhook-secret', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"client_id": "cp_nTYrRysBvVEizO0aMNCX",
"webhook_secret": "whsec_…",
"previous_secret_expires_at": "2026-10-16T07:07:11.542Z"
},
"error": null
}Exchange a code or refresh token for tokens #
/v1/oauth/token no authThe partner's server-to-server token endpoint (RFC 6749).
More about this
No API credential: the client authenticates with client_id + client_secret in the body or with HTTP Basic.
grant_type=authorization_code: exchanges the code from the redirect (single use, valid 60 seconds) with the sameredirect_uriand the PKCEcode_verifier.grant_type=refresh_token: rotates the refresh token. Each refresh token works once; presenting a used one again revokes the whole connection (a theft signal), except within 30 seconds of its rotation, which returns the same new pair (so concurrent refreshes are safe).
Access tokens last one day (expires_in: 86400), refresh tokens 90 days. Responses (success and error) are not the usual envelope: success is the RFC 6749 token response, errors are {"error", "error_description"}. Every response carries Cache-Control: no-store. After too many failed client or grant checks from one source (counted with failed API-key attempts), requests get 429 slow_down with Retry-After until the window is over.
Parameters
- Authorization header · string
Optional HTTP Basic client authentication:
Basic base64(client_id:client_secret).
Body fields
- grant_type string required
authorization_codeorrefresh_token; missing or anything else isunsupported_grant_type.One of: authorization_code, refresh_token
- code string | null
authorization_code: the code from the redirect (oc_...). - redirect_uri string | null
authorization_code: the sameredirect_urithe code was issued for. - code_verifier string | null
authorization_code: the PKCE verifier (43-128 characters ofA-Z a-z 0-9 - . _ ~) whose S256 challenge was sent to authorize. - refresh_token string | null
refresh_token: the latest refresh token (pr_...). - client_id string | null
The app's client id (unless sent with HTTP Basic).
- client_secret string | null
The app's client secret (unless sent with HTTP Basic).
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request | Unreadable body or a missing field ( |
| 400 | unsupported_grant_type |
|
| 400 | invalid_grant | The code is invalid, expired or already used, was issued to another client or |
| 401 | invalid_client | No client credentials, an unknown or disabled app, or a wrong secret. |
| 429 | slow_down | Too many failed attempts from this source; retry after |
| 503 | temporarily_unavailable | Partner access is off on this server or its store is temporarily unavailable; retry. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://cirrus.pocketful.in/v1/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "oc_3zb5bRr1InWe3ipR7EXinezj5kpxNWDpk9piRJNB",
"redirect_uri": "https://partner.example/callback",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"client_id": "cp_3Oq3YXcuepv3Qx5Z2iC2",
"client_secret": "cps_dG69y0yLbVBxwXqsWbWbrQk1aP0sEuT7mYw2HcN4vZ8LxJfR"
}'import os
import requests
response = requests.post(
"https://cirrus.pocketful.in/v1/oauth/token",
json={
"grant_type": "authorization_code",
"code": "oc_3zb5bRr1InWe3ipR7EXinezj5kpxNWDpk9piRJNB",
"redirect_uri": "https://partner.example/callback",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"client_id": "cp_3Oq3YXcuepv3Qx5Z2iC2",
"client_secret": "cps_dG69y0yLbVBxwXqsWbWbrQk1aP0sEuT7mYw2HcN4vZ8LxJfR",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"grant_type": "authorization_code",
"code": "oc_3zb5bRr1InWe3ipR7EXinezj5kpxNWDpk9piRJNB",
"redirect_uri": "https://partner.example/callback",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"client_id": "cp_3Oq3YXcuepv3Qx5Z2iC2",
"client_secret": "cps_dG69y0yLbVBxwXqsWbWbrQk1aP0sEuT7mYw2HcN4vZ8LxJfR"
}),
});
const body = await response.json();
console.log(response.status, body);{
"access_token": "pa_skuF0MRH41ytp5qLfnrdbtWjm4mXpHG8VsfirtfNG9T25WFF",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "pr_X0I0VMDKFSkae48wpL4Yd6PSF7mLh12LbmwW3vHeZpcmgMf83zEOgHlj",
"scope": "read orders"
}List connected partner apps #
/v1/connections app sessionThe partner apps the user has granted access to (live connections only).
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl "https://cirrus.pocketful.in/v1/connections" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://cirrus.pocketful.in/v1/connections",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/connections', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": [
{
"client_id": "cp_Ay2T4dfVLByf9ct7XzAH",
"app_name": "Charts Co",
"verified": false,
"scopes": [
"read",
"orders"
],
"connected_at": "2026-09-29T12:02:21.837659+00:00",
"updated_at": "2026-09-29T12:02:21.837659+00:00"
}
],
"error": null
}Disconnect a partner app #
/v1/connections/{client_id} app sessionRevokes the user's connection to the app: every access token dies at once and every refresh token is deleted.
More about this
The partner must ask the user to connect again.
Parameters
- client_id path · string required
The app's client id (
cp_...).
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | connection_not_found | No connected partner app with this client id. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X DELETE "https://cirrus.pocketful.in/v1/connections/<client_id>" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.delete(
"https://cirrus.pocketful.in/v1/connections/<client_id>",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://cirrus.pocketful.in/v1/connections/<client_id>', {
method: 'DELETE',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"client_id": "cp_Ay2T4dfVLByf9ct7XzAH",
"revoked": true
},
"error": null
}