Work with this as data
Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/granite-md:granite-md-auth-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
OpenAPI Specification
openapi: 3.2.0
info:
title: Granite Auth API
version: 1.0.0
summary: REST API for Granite clients, bots, and integrations.
description: 'Conventions (apply to every operation; see PLAN.md 3.2 and 3.6):
- **Base URL** `https://api.granite.md`. All public operations live under `/v1`.
After Phase 6 ships, `/v1` changes are additive only.
- **Errors** use RFC 9457 problem details (`application/problem+json`, schema `Problem`).
The `type` URI is `https://granite.md/problems/`; `code` refines it for programmatic handling.
Every response carries `X-Request-ID`, also echoed as `request_id` in problems.
- **Pagination** is cursor based. List operations accept `cursor` and `limit` and return
`{ "items": [...], "next_cursor": "..." }`. `next_cursor` is absent on the last page.
Cursors are opaque; clients must not parse or build them.
- **Idempotency**: non-idempotent `POST` operations accept an `Idempotency-Key` header
(1-255 printable ASCII). Repeating a request with the same key and body within 24 hours
returns the original response; the same key with a different body returns `409` with
code `idempotency_key_reused`.
- **IDs** are UUIDv7 strings. **Timestamps** are RFC 3339 in UTC.
- **Authentication**: `Authorization: Bearer ` with a session access token,
a personal API token (`gr_pat_...`), or an OAuth access token. Scopes are listed per operation.
- **Rate limits**: each personal API token may make 600 requests a minute. Over the limit
the API answers `429` with code `token_rate_limited` and a `Retry-After` header (seconds).'
license:
name: Proprietary
identifier: LicenseRef-Proprietary
servers:
- url: https://api.granite.md
description: Production
- url: http://localhost:8080
description: Local development
security:
- bearerAuth: []
tags:
- name: Auth
description: Sign-up, login, sessions, and password reset.
paths:
/v1/auth/signup:
post:
operationId: signup
tags:
- Auth
summary: Create an account
description: 'Creates an unverified account and emails a verification link, or with `confirm: code`
(the mobile apps) a 6-digit code to send to `POST /v1/auth/signup/confirm`. The response
is the same whether or not the email is already registered: a verified account''s owner
is notified instead, and an account not yet confirmed gets a new link or code (earlier
ones stop working) whose confirmation also sets this request''s password and name.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SignupRequest'
responses:
'202':
$ref: '#/components/responses/Accepted'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/signup/confirm:
post:
operationId: confirmSignup
tags:
- Auth
summary: Confirm a sign-up with the emailed code and sign in
description: 'Confirms the email address with the 6-digit code from a `confirm: code` sign-up (or
resend) and signs in on this device in the same step, so the password is not typed
again. A code works once, for 15 minutes, and stops working after 5 wrong tries. A
wrong, expired, or used-up code and an unknown email all answer 400 with code
`invalid_signup_code`.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SignupConfirmRequest'
responses:
'200':
description: Email confirmed and signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/verify-email:
post:
operationId: verifyEmail
tags:
- Auth
summary: Confirm an email address
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TokenRequest'
responses:
'204':
description: Email confirmed.
default:
$ref: '#/components/responses/Error'
/v1/auth/verify-email/resend:
post:
operationId: resendVerification
tags:
- Auth
summary: Send a new verification email
description: 'Sends a new link, or with `confirm: code` a new 6-digit code (earlier codes stop
working). Always answers 202, whether or not the email has an unverified account.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmailConfirmRequest'
responses:
'202':
$ref: '#/components/responses/Accepted'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/login:
post:
operationId: login
tags:
- Auth
summary: Sign in
description: 'Checks the password and starts a session for the device. After repeated failures the
email is locked for a while (429), whether or not it has an account.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'202':
description: Password accepted; the account needs a second factor. Continue with `POST /v1/auth/login/2fa`.
content:
application/json:
schema:
$ref: '#/components/schemas/MFAChallenge'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/sso/start:
post:
operationId: startSSO
tags:
- Auth
summary: Start single sign-on
description: 'Finds the workspace that verified the email''s domain and set up SSO, and returns the
identity provider''s sign-in URL. Open it in the browser; the provider returns to the
web app''s `/sso/callback`, which finishes with `POST /v1/auth/sso/finish`.
With `client=mobile` (the phone app, which opens the page in the system browser), the
`state` (OpenID Connect) or `handoff` code (SAML) starts with `app.`: the web callback
then hands it to the phone app in a `granite://sso-callback` link instead of finishing
itself, and the app finishes.'
security: []
parameters:
- name: client
in: query
required: false
description: Who finishes the sign-in; `mobile` for the phone app.
schema:
type: string
enum:
- web
- mobile
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
maxLength: 320
responses:
'200':
description: Where to sign in.
content:
application/json:
schema:
type: object
required:
- authorize_url
properties:
authorize_url:
type: string
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/sso/finish:
post:
operationId: finishSSO
tags:
- Auth
summary: Finish single sign-on
description: 'Exchanges the code from the identity provider and starts a session. People new to the
workspace join it with the connection''s role. Two-step verification is left to the
identity provider.'
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- device
description: The OpenID Connect `state` and `code`, or the SAML `handoff` code.
properties:
state:
type: string
maxLength: 200
code:
type: string
maxLength: 4000
handoff:
type: string
maxLength: 200
device:
$ref: '#/components/schemas/DeviceInfo'
responses:
'200':
description: Signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
default:
$ref: '#/components/responses/Error'
/v1/auth/refresh:
post:
operationId: refreshSession
tags:
- Auth
summary: Get new tokens
description: 'Exchanges a refresh token for a new access token and a new refresh token. Each refresh
token works once; presenting a used one ends every session of that sign-in
(code `refresh_token_reused`). Clients must not refresh concurrently.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: New tokens.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
default:
$ref: '#/components/responses/Error'
/v1/auth/logout:
post:
operationId: logout
tags:
- Auth
summary: Sign out this device
responses:
'204':
description: Signed out.
default:
$ref: '#/components/responses/Error'
/v1/auth/password-reset:
post:
operationId: requestPasswordReset
tags:
- Auth
summary: Email a password reset link or code
description: 'Emails a reset link, or with `confirm: code` (the mobile apps) a 6-digit code for
`POST /v1/auth/password-reset/code/confirm` (earlier codes stop working). Always
answers 202, whether or not the email has an account.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmailConfirmRequest'
responses:
'202':
$ref: '#/components/responses/Accepted'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/password-reset/confirm:
post:
operationId: confirmPasswordReset
tags:
- Auth
summary: Set a new password
description: Sets the password from a reset link and signs out every device.
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordResetConfirmRequest'
responses:
'204':
description: Password changed.
default:
$ref: '#/components/responses/Error'
/v1/auth/password-reset/code/check:
post:
operationId: checkPasswordResetCode
tags:
- Auth
summary: Check an emailed reset code
description: 'Says whether a reset code is right before the person chooses a new password. The code
is not used up; a wrong one counts as one of its 5 tries. A wrong, expired, or used-up
code and an unknown email all answer 400 with code `invalid_reset_code`.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmailCodeRequest'
responses:
'204':
description: The code is right.
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/password-reset/code/confirm:
post:
operationId: confirmPasswordResetCode
tags:
- Auth
summary: Set a new password with an emailed code and sign in
description: 'Sets the new password with the 6-digit code from a `confirm: code` reset request, signs
out every other session, and signs in on this device. Accounts with two-step
verification get a challenge to finish with `POST /v1/auth/login/2fa`, as at sign-in.
A code works once, for 15 minutes, and stops working after 5 wrong tries; failures
answer 400 with code `invalid_reset_code`.'
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordResetCodeConfirmRequest'
responses:
'200':
description: Password changed and signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'202':
description: Password changed; the account needs a second factor. Continue with `POST /v1/auth/login/2fa`.
content:
application/json:
schema:
$ref: '#/components/schemas/MFAChallenge'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/login/2fa:
post:
operationId: loginSecondFactor
tags:
- Auth
summary: Finish signing in with a second factor
description: Send either `code` (from the authenticator app) or `recovery_code`. Each works once.
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SecondFactorRequest'
responses:
'200':
description: Signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/step-up:
post:
operationId: stepUp
tags:
- Auth
summary: Confirm the password for sensitive actions
description: 'Re-checks the password (and the authenticator code when two-factor authentication is on).
For the next 10 minutes this session may change two-factor settings. Other sensitive
actions answer 403 with code `step_up_required` until this is done.'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StepUpRequest'
responses:
'204':
description: Confirmed.
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
/v1/auth/app-handoff:
post:
operationId: createAppHandoff
tags:
- Auth
summary: Hand a sign-in to the desktop app
description: 'For the desktop app''s sign-in through the browser (Google, single sign-on). The app opens
the web app with a PKCE challenge; once signed in, the web app calls this and passes the
one-time code to the app in a `granite://auth` link. The app redeems it with
`POST /v1/auth/app-handoff/finish`. Needs a session, not an API token. A session from
single sign-on hands the app a single sign-on session for the same workspace.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- code_challenge
properties:
code_challenge:
type: string
description: base64url(SHA-256(code_verifier)) without padding (RFC 7636 S256).
minLength: 43
maxLength: 43
responses:
'200':
description: A one-time code, valid for two minutes.
content:
application/json:
schema:
type: object
required:
- code
- expires_in
properties:
code:
type: string
expires_in:
type: integer
description: Seconds the code stays valid.
default:
$ref: '#/components/responses/Error'
/v1/auth/app-handoff/finish:
post:
operationId: finishAppHandoff
tags:
- Auth
summary: Redeem a sign-in handed to the app
description: 'Exchanges the one-time code from `POST /v1/auth/app-handoff` and the app''s PKCE verifier
for a session on the app''s device. A code works once, also when the verifier is wrong.'
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- code
- code_verifier
- device
properties:
code:
type: string
maxLength: 200
code_verifier:
type: string
minLength: 43
maxLength: 128
device:
$ref: '#/components/schemas/DeviceInfo'
responses:
'200':
description: Signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
default:
$ref: '#/components/responses/Error'
/v1/auth/oidc/{provider}:
post:
operationId: socialLogin
tags:
- Auth
summary: Sign in with Google or Apple
description: 'Send the ID token from the provider''s sign-in SDK. Before signing in, the client makes a
random `nonce`, passes hex(SHA-256(nonce)) to the provider, and sends the raw nonce here.
A first sign-in with a verified email links to the account with that email or creates
one. Returns 202 when the account has two-factor authentication on.'
security: []
parameters:
- name: provider
in: path
required: true
schema:
type: string
enum:
- google
- apple
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SocialLoginRequest'
responses:
'200':
description: Signed in.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'202':
description: Continue with `POST /v1/auth/login/2fa`.
content:
application/json:
schema:
$ref: '#/components/schemas/MFAChallenge'
'429':
$ref: '#/components/responses/TooManyRequests'
default:
$ref: '#/components/responses/Error'
components:
headers:
RequestID:
description: Identifier of this request, for support and log correlation.
schema:
type: string
RetryAfter:
description: Seconds to wait before retrying.
schema:
type: integer
schemas:
TokenRequest:
type: object
required:
- token
properties:
token:
type: string
maxLength: 200
PasswordResetCodeConfirmRequest:
type: object
required:
- email
- code
- password
- device
properties:
email:
type: string
format: email
maxLength: 254
code:
type: string
maxLength: 20
description: The 6 digits from the email.
password:
type: string
minLength: 10
maxLength: 256
writeOnly: true
description: 'Must also be hard to guess: passwords built from common words, the person''s name or email, dates, repeats, or keyboard runs are refused (field "password").'
device:
$ref: '#/components/schemas/DeviceInfo'
FieldError:
type: object
required:
- field
- detail
properties:
field:
type: string
description: JSON path of the invalid field, for example `ops[2].path`.
detail:
type: string
MFAChallenge:
type: object
required:
- mfa_token
- expires_in
- methods
properties:
mfa_token:
type: string
expires_in:
type: integer
description: Seconds left to send the second factor.
methods:
type: array
items:
type: string
enum:
- totp
- recovery_code
User:
type: object
required:
- id
- email
- name
- email_verified
- two_factor_enabled
- created_at
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
email_verified:
type: boolean
two_factor_enabled:
type: boolean
created_at:
type: string
format: date-time
avatar_url:
type: string
description: The profile photo's path on the API server (`/v1/avatars/{id}`); absent without one.
SignupRequest:
type: object
required:
- email
- password
properties:
email:
type: string
format: email
maxLength: 254
password:
type: string
minLength: 10
maxLength: 256
writeOnly: true
description: 'Must also be hard to guess: passwords built from common words, the person''s name or email, dates, repeats, or keyboard runs are refused (field "password").'
name:
type: string
maxLength: 200
workspace_name:
type: string
minLength: 1
maxLength: 100
description: 'Signs up for a business account: a new account also gets a workspace of this name,
owned by it, on the 30-day Business trial. Ignored when the email already has an account.
'
confirm:
$ref: '#/components/schemas/EmailConfirmMode'
Problem:
type: object
description: RFC 9457 problem details.
required:
- type
- title
- status
properties:
type:
type: string
format: uri
examples:
- https://granite.md/problems/conflict
title:
type: string
status:
type: integer
format: int32
detail:
type: string
instance:
type: string
code:
type: string
description: Machine-readable refinement of `type`, for example `version_conflict`.
request_id:
type: string
errors:
type: array
description: Per-field validation errors.
items:
$ref: '#/components/schemas/FieldError'
Accepted:
type: object
required:
- status
properties:
status:
type: string
enum:
- accepted
PasswordResetConfirmRequest:
type: object
required:
- token
- password
properties:
token:
type: string
maxLength: 200
password:
type: string
minLength: 10
maxLength: 256
writeOnly: true
description: 'Must also be hard to guess: passwords built from common words, the person''s name or email, dates, repeats, or keyboard runs are refused (field "password").'
LoginRequest:
type: object
required:
- email
- password
- device
properties:
email:
type: string
format: email
maxLength: 254
password:
type: string
maxLength: 256
writeOnly: true
device:
$ref: '#/components/schemas/DeviceInfo'
TokenResponse:
type: object
required:
- access_token
- token_type
- expires_in
- refresh_token
- refresh_expires_in
- device_id
- user
properties:
access_token:
type: string
token_type:
type: string
enum:
- Bearer
expires_in:
type: integer
description: Seconds until the access token expires.
refresh_token:
type: string
description: Single-use. Store it in the OS keychain.
refresh_expires_in:
type: integer
device_id:
type: string
format: uuid
user:
$ref: '#/components/schemas/User'
DeviceInfo:
type: object
required:
- platform
properties:
id:
type: string
format: uuid
description: Device ID from an earlier sign-in, to keep the same device record.
name:
type: string
maxLength: 100
examples:
- Fuad's MacBook
platform:
type: string
enum:
- web
- macos
- windows
- ios
- android
- cli
- other
app_version:
type: string
maxLength: 50
SocialLoginRequest:
type: object
required:
- id_token
- nonce
- device
properties:
id_token:
type: string
maxLength: 8000
nonce:
type: string
minLength: 16
maxLength: 200
name:
type: string
maxLength: 200
description: Apple gives the user's name to the app only on the first sign-in.
authorization_code:
type: string
maxLength: 2000
description: 'Apple only: the authorization code Apple returned with the ID token. The server
exchanges it for a refresh token, kept encrypted, so that deleting the account can
revoke Granite''s access to the person''s Apple ID (App Store Review Guideline 5.1.1(v)).
A failed exchange does not stop the sign-in.
'
workspace_name:
type: string
minLength: 1
maxLength: 100
description: 'Signs up for a business account (with a company''s Google Workspace account, say): when
this sign-in creates the account, it also gets a workspace of this name, owned by it,
on the 30-day Business trial. Ignored for an existing account.
'
device:
$ref: '#/components/schemas/DeviceInfo'
EmailConfirmMode:
type: string
enum:
- link
- code
default: link
description: 'How the email proves the address: a link to open (`link`, the web) or a 6-digit code to
type into the app (`code`, the mobile apps).
'
EmailCodeRequest:
type: object
required:
- email
- code
properties:
email:
type: string
format: email
maxLength: 254
code:
type: string
maxLength: 20
description: The 6 digits from the email.
StepUpRequest:
type: object
required:
- password
properties:
password:
type: string
maxLength: 256
writeOnly: true
code:
type: string
maxLength: 10
description: Authenticator code, required when two-factor authentication is on.
EmailConfirmRequest:
type: object
required:
- email
properties:
email:
type: string
format: email
maxLength: 254
confirm:
$ref: '#/components/schemas/EmailConfirmMode'
RefreshRequest:
type: object
required:
- refresh_token
properties:
refresh_token:
type: string
maxLength: 200
SignupConfirmRequest:
type: object
required:
- email
- code
- device
properties:
email:
type: string
format: email
maxLength: 254
code:
type: string
maxLength: 20
description: The 6 digits from the email.
device:
$ref: '#/components/schemas/DeviceInfo'
SecondFactorRequest:
type: object
required:
- mfa_token
properties:
mfa_token:
type: string
maxLength: 2000
code:
type: string
maxLength: 10
recovery_code:
type: string
maxLength: 40
responses:
Accepted:
description: Request accepted. The same answer is given whether or not an account exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Accepted'
TooManyRequests:
description: Rate limit exceeded.
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
X-Request-ID:
$ref: '#/components/headers/RequestID'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
Error:
description: Error response.
headers:
X-Request-ID:
$ref: '#/components/headers/RequestID'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: Session access token, personal API token, or OAuth access token.