Dynamic client registration (RFC 7591). Unauthenticated; do it once per app.
Body
Body| Field | Type | Description |
|---|
redirect_uris* | string[] | At most 10. https URLs, or loopback-address URLs for a native app (RFC 8252 §7.3). Matched exactly at authorize time. |
client_name* | string | Shown to the person on the consent page. |
scope | string | Space-separated scopes the client may ask for, from the catalogue on the OAuth guide. An authorize request that names none asks for these. |
client_uri | string | An https link to the app, shown on the consent page. |
logo_uri | string | An https image, shown on the consent page. |
›5 more fields (grant_types, response_types, token_endpoint_auth_method, software_id, software_version)
Body, less common| Field | Type | Description |
|---|
grant_types | string[] | authorization_code and refresh_token, which is also the default. |
response_types | string[] | Only ["code"]. |
token_endpoint_auth_method | string | Only none: every client is a public client. |
software_id | string | Your own identifier for the app, stored and returned. |
software_version | string | Your own version string, stored and returned. |
curl -X POST "https://api.rasket.com/oauth/register" \
-H "User-Agent: acme-billing/1.0" \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback"],
"scope": "emails:send emails:read"
}'
const response = await fetch("https://api.rasket.com/oauth/register", {
method: "POST",
headers: {
"User-Agent": "acme-billing/1.0",
"Content-Type": "application/json",
},
body: JSON.stringify({
client_name: "Acme Invoices",
redirect_uris: ["https://invoices.acme.example/rasket/callback"],
scope: "emails:send emails:read"
}),
});
const data = await response.json();
import os
import requests
response = requests.post(
"https://api.rasket.com/oauth/register",
headers={
"User-Agent": "acme-billing/1.0",
},
json={
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback"],
"scope": "emails:send emails:read"
},
)
print(response.json())
Response 201
{
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"client_id_issued_at": 1789205400,
"registration_client_uri": "/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "emails:send emails:read",
"registration_access_token": "rkor_…"
}
registration_access_token is shown exactly once. Store it: it is the only credential for reading, replacing or deleting this client.- Public clients only. A
token_endpoint_auth_method other than none, or response_types other than code, is 400 invalid_client_metadata; a redirect URI that is not https or loopback is 400 invalid_redirect_uri. - At most 10 registrations per IP address per hour and 1,000 a day across the platform; past either,
429. - Errors from this route are RFC 7591's
{ error, error_description }, not the Rasket error body.
Send the person's browser here. It comes back to your redirect URI with a code.
Query parameters
Query parameters| Field | Type | Description |
|---|
response_type* | string | Always code. |
client_id* | string | Your rkoc_ identifier. |
redirect_uri* | string | Exactly one of the client's registered redirect URIs. |
code_challenge* | string | BASE64URL(SHA256(code_verifier)): 43 characters. |
code_challenge_method* | string | Always S256. plain is refused. |
state* | string | An unguessable value you check when the browser comes back. Returned unchanged; at most 512 characters. |
scope | string | Space-separated. The client's registered scopes when omitted. |
›1 more field (resource)
Query parameters, less common| Field | Type | Description |
|---|
resource | string | An RFC 8707 resource indicator. When sent, it must name this API. |
curl -X GET "https://api.rasket.com/oauth/authorize?response_type=code&client_id=rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C&redirect_uri=https%3A%2F%2Finvoices.acme.example%2Frasket%2Fcallback&scope=emails%3Asend+emails%3Aread&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256" \
-H "User-Agent: acme-billing/1.0"
const response = await fetch("https://api.rasket.com/oauth/authorize?response_type=code&client_id=rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C&redirect_uri=https%3A%2F%2Finvoices.acme.example%2Frasket%2Fcallback&scope=emails%3Asend+emails%3Aread&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256", {
method: "GET",
headers: {
"User-Agent": "acme-billing/1.0",
},
});
console.log(response.status);
import os
import requests
response = requests.get(
"https://api.rasket.com/oauth/authorize?response_type=code&client_id=rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C&redirect_uri=https%3A%2F%2Finvoices.acme.example%2Frasket%2Fcallback&scope=emails%3Asend+emails%3Aread&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256",
headers={
"User-Agent": "acme-billing/1.0",
},
)
print(response.status_code)
- This is a URL to open in the person's browser, not a call your server makes: the
302 goes to our consent page, which asks them to sign in, pick one team and approve the scopes. Server-side, fetch and requests would follow it to a login page. - Approval redirects to
redirect_uri?code=rkc_…&state=…. Refusal, or any other failure, redirects there with error, error_description and your state. - An unknown or disabled
client_id, or a redirect_uri the client never registered, never redirects: it answers 400 with an error page, because sending the browser to an unverified address is how codes leak (RFC 6749 §4.1.2.1).
Swap an authorization code for tokens, or rotate a refresh token.
Body
Body| Field | Type | Description |
|---|
grant_type* | string | authorization_code or refresh_token. |
client_id* | string | Your client identifier. |
code | string | authorization_code only: the rkc_ code from the redirect. |
redirect_uri | string | authorization_code only: the same URI the authorize request named. |
code_verifier | string | authorization_code only: the verifier whose hash you sent as the challenge. |
refresh_token | string | refresh_token only: the rkr_ token. |
›1 more field (scope)
Body, less common| Field | Type | Description |
|---|
scope | string | refresh_token only: a narrower set. It can never widen the grant. |
curl -X POST "https://api.rasket.com/oauth/token" \
-H "User-Agent: acme-billing/1.0" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"code": "rkc_…",
"redirect_uri": "https://invoices.acme.example/rasket/callback",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}'
const response = await fetch("https://api.rasket.com/oauth/token", {
method: "POST",
headers: {
"User-Agent": "acme-billing/1.0",
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_type: "authorization_code",
client_id: "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
code: "rkc_…",
redirect_uri: "https://invoices.acme.example/rasket/callback",
code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}),
});
const data = await response.json();
import os
import requests
response = requests.post(
"https://api.rasket.com/oauth/token",
headers={
"User-Agent": "acme-billing/1.0",
},
json={
"grant_type": "authorization_code",
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"code": "rkc_…",
"redirect_uri": "https://invoices.acme.example/rasket/callback",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
},
)
print(response.json())
Response 200
{
"access_token": "rko_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rkr_…",
"scope": "emails:send emails:read"
}
- The body may be
application/x-www-form-urlencoded — what a conforming OAuth library sends — or JSON, as shown. - A refresh returns a new pair and revokes the refresh token you presented. Presenting a code, or a rotated refresh token, a second time revokes the whole grant and every token under it.
- Errors are RFC 6749 §5.2's
{ error, error_description }: 400 with invalid_request, invalid_grant, unauthorized_client, unsupported_grant_type or invalid_scope, and 401 invalid_client for an unknown or disabled client. - The response carries
Cache-Control: no-store.
RFC 7009. Sign a person out of your app without revoking the grant.
Body
Body| Field | Type | Description |
|---|
token* | string | The access or refresh token to revoke. |
client_id* | string | The client the token was issued to. |
›1 more field (token_type_hint)
Body, less common| Field | Type | Description |
|---|
token_type_hint | string | access_token or refresh_token. Accepted and ignored. |
curl -X POST "https://api.rasket.com/oauth/revoke" \
-H "User-Agent: acme-billing/1.0" \
-H "Content-Type: application/json" \
-d '{
"token": "rkr_…",
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C"
}'
const response = await fetch("https://api.rasket.com/oauth/revoke", {
method: "POST",
headers: {
"User-Agent": "acme-billing/1.0",
"Content-Type": "application/json",
},
body: JSON.stringify({
token: "rkr_…",
client_id: "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C"
}),
});
console.log(response.status);
import os
import requests
response = requests.post(
"https://api.rasket.com/oauth/revoke",
headers={
"User-Agent": "acme-billing/1.0",
},
json={
"token": "rkr_…",
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C"
},
)
print(response.status_code)
- An access token is revoked alone. A refresh token takes its whole family with it, and every access token of the same grant.
- The grant itself stays live. Only the team revoking it, or the client being deleted, ends a grant.
- A token that is unknown, expired, already revoked or another client's still answers
200 with an empty body, as RFC 7009 §2.2 requires. - Form-encoded or JSON, as for the token endpoint.
RFC 7592. The metadata stored for your client.
Path parameters
Path parameters| Field | Type | Description |
|---|
client_id* | string | The rkoc_ identifier registration returned. |
Headers
Headers| Field | Type | Description |
|---|
Authorization* | string | Bearer rkor_… — the registration access token, shown once when the client was registered. An API key is refused. |
curl -X GET "https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C" \
-H "Authorization: Bearer $RASKET_REGISTRATION_TOKEN" \
-H "User-Agent: acme-billing/1.0"
const response = await fetch("https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.RASKET_REGISTRATION_TOKEN}`,
"User-Agent": "acme-billing/1.0",
},
});
const data = await response.json();
import os
import requests
response = requests.get(
"https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
headers={
"Authorization": f"Bearer {os.environ['RASKET_REGISTRATION_TOKEN']}",
"User-Agent": "acme-billing/1.0",
},
)
print(response.json())
Response 200
{
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"client_id_issued_at": 1789205400,
"registration_client_uri": "/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "emails:send emails:read"
}
- A missing, malformed or unknown registration token is
401 with WWW-Authenticate: Bearer error="invalid_token".
RFC 7592. A full replacement: a field you leave out is cleared.
Path parameters
Path parameters| Field | Type | Description |
|---|
client_id* | string | The rkoc_ identifier registration returned. |
Headers
Headers| Field | Type | Description |
|---|
Authorization* | string | Bearer rkor_… — the registration access token, shown once when the client was registered. An API key is refused. |
Body
Body| Field | Type | Description |
|---|
redirect_uris* | string[] | At most 10. https URLs, or loopback-address URLs for a native app (RFC 8252 §7.3). Matched exactly at authorize time. |
client_name* | string | Shown to the person on the consent page. |
scope | string | Space-separated scopes the client may ask for, from the catalogue on the OAuth guide. An authorize request that names none asks for these. |
client_uri | string | An https link to the app, shown on the consent page. |
logo_uri | string | An https image, shown on the consent page. |
›5 more fields (grant_types, response_types, token_endpoint_auth_method, software_id, software_version)
Body, less common| Field | Type | Description |
|---|
grant_types | string[] | authorization_code and refresh_token, which is also the default. |
response_types | string[] | Only ["code"]. |
token_endpoint_auth_method | string | Only none: every client is a public client. |
software_id | string | Your own identifier for the app, stored and returned. |
software_version | string | Your own version string, stored and returned. |
curl -X PUT "https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C" \
-H "Authorization: Bearer $RASKET_REGISTRATION_TOKEN" \
-H "User-Agent: acme-billing/1.0" \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback", "https://invoices.acme.example/rasket/callback-eu"],
"scope": "emails:send emails:read"
}'
const response = await fetch("https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C", {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.RASKET_REGISTRATION_TOKEN}`,
"User-Agent": "acme-billing/1.0",
"Content-Type": "application/json",
},
body: JSON.stringify({
client_name: "Acme Invoices",
redirect_uris: ["https://invoices.acme.example/rasket/callback", "https://invoices.acme.example/rasket/callback-eu"],
scope: "emails:send emails:read"
}),
});
const data = await response.json();
import os
import requests
response = requests.put(
"https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
headers={
"Authorization": f"Bearer {os.environ['RASKET_REGISTRATION_TOKEN']}",
"User-Agent": "acme-billing/1.0",
},
json={
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback", "https://invoices.acme.example/rasket/callback-eu"],
"scope": "emails:send emails:read"
},
)
print(response.json())
Response 200
{
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"client_id_issued_at": 1789205400,
"registration_client_uri": "/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"client_name": "Acme Invoices",
"redirect_uris": ["https://invoices.acme.example/rasket/callback", "https://invoices.acme.example/rasket/callback-eu"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "emails:send emails:read"
}
- The registration access token is not rotated: a client has nowhere to learn a new one.
RFC 7592. Revokes every grant the client holds, on every team.
Path parameters
Path parameters| Field | Type | Description |
|---|
client_id* | string | The rkoc_ identifier registration returned. |
Headers
Headers| Field | Type | Description |
|---|
Authorization* | string | Bearer rkor_… — the registration access token, shown once when the client was registered. An API key is refused. |
curl -X DELETE "https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C" \
-H "Authorization: Bearer $RASKET_REGISTRATION_TOKEN" \
-H "User-Agent: acme-billing/1.0"
const response = await fetch("https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.RASKET_REGISTRATION_TOKEN}`,
"User-Agent": "acme-billing/1.0",
},
});
console.log(response.status);
import os
import requests
response = requests.delete(
"https://api.rasket.com/oauth/register/rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
headers={
"Authorization": f"Bearer {os.environ['RASKET_REGISTRATION_TOKEN']}",
"User-Agent": "acme-billing/1.0",
},
)
print(response.status_code)
- Every grant is revoked with
revoked_reason client, and every token under them stops working at once. - Each team still sees its grant, revoked, in
GET /oauth/grants: the history is kept.
Every app this team has connected, live and revoked, newest first.
Query parameters
Query parameters| Field | Type | Description |
|---|
limit | integer | How many items to return, 1–100. Defaults to 20. |
after | string | Return the page that follows this item ID. Mutually exclusive with before. |
before | string | Return the page that precedes this item ID. Mutually exclusive with after. |
curl -X GET "https://api.rasket.com/oauth/grants?limit=20" \
-H "Authorization: Bearer $RASKET_API_KEY" \
-H "User-Agent: acme-billing/1.0"
const response = await fetch("https://api.rasket.com/oauth/grants?limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
"User-Agent": "acme-billing/1.0",
},
});
const data = await response.json();
import os
import requests
response = requests.get(
"https://api.rasket.com/oauth/grants?limit=20",
headers={
"Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
"User-Agent": "acme-billing/1.0",
},
)
print(response.json())
Response 200
{
"object": "list",
"has_more": false,
"data": [
{
"id": "0199e0a4-6b2d-7c1e-8f3a-2d5b9c7e1a40",
"client_id": "rkoc_7Fq2Lm9pXw3Kd8Vn1Zt5Rb4C",
"scopes": ["emails:send", "emails:read"],
"created_at": "2026-09-12T09:30:00.000Z",
"revoked_at": null,
"revoked_reason": null,
"client": {
"name": "Acme Invoices",
"logo_uri": null
}
}
]
}
- A revoked grant stays in the list with
revoked_at and revoked_reason — user, client, code_reuse, refresh_reuse or operator — so an app never silently disappears. - Needs a
full_access key. An OAuth access token is refused here whatever its scopes: no scope reaches the grants, so a connected app cannot see or manage connected apps.
Disconnect an app: the grant and every token issued under it.
Path parameters
Path parameters| Field | Type | Description |
|---|
oauth_grant_id* | string | The grant's ID. |
curl -X DELETE "https://api.rasket.com/oauth/grants/0199e0a4-6b2d-7c1e-8f3a-2d5b9c7e1a40" \
-H "Authorization: Bearer $RASKET_API_KEY" \
-H "User-Agent: acme-billing/1.0"
const response = await fetch("https://api.rasket.com/oauth/grants/0199e0a4-6b2d-7c1e-8f3a-2d5b9c7e1a40", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
"User-Agent": "acme-billing/1.0",
},
});
const { id } = await response.json();
import os
import requests
response = requests.delete(
"https://api.rasket.com/oauth/grants/0199e0a4-6b2d-7c1e-8f3a-2d5b9c7e1a40",
headers={
"Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
"User-Agent": "acme-billing/1.0",
},
)
id = response.json()["id"]
Response 200
{
"object": "oauth_grant",
"id": "0199e0a4-6b2d-7c1e-8f3a-2d5b9c7e1a40",
"revoked_at": "2026-09-12T14:05:12.000Z",
"revoked_reason": "user"
}
- The app's next call with any of its tokens is
401 missing_api_key. - Idempotent: revoking a revoked grant answers the revocation it already has.
- Needs a
full_access key; an OAuth access token is refused.