> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.givechariot.com/v2026-04-01/guides/oauth/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.givechariot.com/_mcp/server. # OAuth > **Warning** > > OAuth access is not self-service. Contact > [support@givechariot.com](mailto:support@givechariot.com) to register your > application and receive client credentials. Include your application's display > name, logo, callback URLs, and optionally the IP addresses to whitelist. ## How It Works Chariot implements the **OAuth 2.0 Authorization Code** flow with **PKCE** (Proof Key for Code Exchange): 1. Your application generates a PKCE code verifier and code challenge 2. Your application redirects a nonprofit user to Chariot's authorization page with the code challenge 3. The user reviews the requested permissions and approves access 4. Chariot redirects back to your application with an authorization code 5. Your server exchanges the code and the code verifier for access and refresh tokens 6. You use the access token to call the Chariot API on behalf of the organization ## Authentication When Chariot registers your application, you'll receive a `client_id` and `client_secret`. Your server uses these credentials (via HTTP Basic Auth) to authenticate with Chariot during the OAuth token exchange and refresh flows. Once you have an access token, your server uses that token — not the client credentials — to call the Chariot API on behalf of the nonprofit. Your client secret should be kept secure and should never be exposed to the public. ## Environments | Endpoint | Production | Sandbox | | ------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------- | | Authorization | `https://dashboard.givechariot.com/oauth/authorize` | `https://dashboard.givechariot.com/oauth/sandbox/authorize` | | Token | `https://api.givechariot.com/auth/oauth/token` | `https://sandboxapi.givechariot.com/auth/oauth/token` | | OIDC configuration | `https://api.givechariot.com/.well-known/openid-configuration` | `https://sandboxapi.givechariot.com/.well-known/openid-configuration` | Note that each environment issues its own `client_id` and `client_secret`. This means that sandbox credentials cannot be used in production and vice versa. Additional OAuth 2.0 and OIDC metadata can be found at the environment's well-known discovery document. ## Scopes Scopes define what your application can access. Specify multiple as a URL encoded space-separated string when requesting authorization. The scopes your client is allowed to request are fixed during client registration. | Scope | Grants | | ---------------- | -------------------------------------------------------------------------------------------- | | `read_only` | Read access across every resource in the Chariot API | | `read_write` | Read and write access across every resource in the Chariot API | | `openid` | Issue an `id_token` and access the UserInfo endpoint. See [Identity](/guides/oauth/identity) | | `offline_access` | Accepted for compatibility with OIDC clients | `read_only` and `read_write` are aggregates — they grant their level of access across every resource, so you do not have to enumerate a scope per resource or request new ones as the API grows. Neither grants Chariot's internal administrative operations. > **Note** > > A token only receives the permissions your client and the authorization have > in common, narrowed by the consenting user's role where the authorization > belongs to a user. Request `read_only` alongside `read_write` if you serve > nonprofits whose users hold read-only roles — a read-only user cannot approve > a request for `read_write` alone. ## Authorization Subjects An authorization belongs either to the user who approved it or to the nonprofit organization they approved it for. Which one applies is fixed when your application is registered. | | **User** | **Organization** | | ------------------------------------ | ---------------------------------------------------------------------- | ----------------------------------- | | Permissions | The user's current role narrows the token on every call | Stay as approved | | If the approver's role changes | Access narrows with it | Unaffected | | If the approver leaves the nonprofit | Access stops | Unaffected | | Identity | `openid` returns an `id_token`. See [Identity](/guides/oauth/identity) | No `id_token` | | Revoked by | The user who approved it | Any Owner or Admin at the nonprofit | Applications are registered against a user by default. Ask us about an organization authorization if yours is a server-to-server integration meant to outlive the person who sets it up. ## Authorization Code Flow ### Step 1: Generate a PKCE Code Verifier and Challenge Before redirecting the user, generate a cryptographically random code verifier (43-128 characters, using `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`), then compute its SHA-256 hash and base64url-encode it to create the code challenge: **`NodeJS`** ```javascript NodeJS const crypto = require("crypto"); function generatePKCE() { const codeVerifier = crypto.randomBytes(32).toString("base64url"); const codeChallenge = crypto .createHash("sha256") .update(codeVerifier) .digest("base64url"); return { codeVerifier, codeChallenge }; } ``` **`Python`** ```python Python import os import hashlib import base64 def generate_pkce(): code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode() code_challenge = ( base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()) .rstrip(b"=") .decode() ) return code_verifier, code_challenge ``` **`Go`** ```go Go package main import ( "crypto/rand" "crypto/sha256" "encoding/base64" ) func generatePKCE() (string, string) { b := make([]byte, 32) rand.Read(b) codeVerifier := base64.RawURLEncoding.EncodeToString(b) h := sha256.Sum256([]byte(codeVerifier)) codeChallenge := base64.RawURLEncoding.EncodeToString(h[:]) return codeVerifier, codeChallenge } ``` **`Java`** ```java Java import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Base64; public static String[] generatePKCE() throws Exception { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[32]; random.nextBytes(bytes); String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); byte[] hash = MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes("US-ASCII")); String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash); return new String[]{codeVerifier, codeChallenge}; } ``` Store the `codeVerifier` — you'll need it in Step 5. ### Step 2: Redirect to Chariot Direct the user's browser to Chariot's authorization endpoint: ``` GET https://dashboard.givechariot.com/oauth/authorize ``` | Parameter | Required | Description | | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | | `client_id` | Yes | Your application's client ID | | `redirect_uri` | Yes | URL to redirect back to (must match a registered URI). See [Redirect URI Requirements](#redirect-uri-requirements) | | `response_type` | Yes | Must be `code` | | `scope` | Yes | Space-separated list of requested scopes, percent-encoded (`%20`) as a single query parameter value | | `state` | Recommended | An opaque value to prevent CSRF attacks | | `nonce` | Yes | Cryptographically random string to prevent replay attacks. See [Identity](/guides/oauth/identity) | | `code_challenge` | Yes | Base64url-encoded SHA-256 hash of the code verifier | | `code_challenge_method` | Yes | Must be `S256` | **Example:** ``` https://dashboard.givechariot.com/oauth/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=https://example.com/callback &response_type=code &scope=openid%20read_write &state=RANDOM_CSRF_TOKEN &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256 ``` ### Step 3: User Provides Consent Chariot presents the nonprofit user with a consent screen showing your application name, logo, and the permissions being requested. ### Step 4: Receive the Authorization Code After approval, Chariot redirects back to your [`redirect_uri`](#redirect-uri-requirements): ``` https://example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_CSRF_TOKEN ``` > **Warning** > > Always verify that the returned `state` matches the value you sent in Step 2 > to prevent CSRF attacks. Authorization codes expire shortly after they were issued and can only be used once. ### Step 5: Exchange the Code for Tokens Make a server-side `POST` request to the token endpoint, authenticating with HTTP Basic Auth. Include the `code_verifier` you generated in Step 1: ```curl curl -X POST https://api.givechariot.com/auth/oauth/token \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -d "grant_type=authorization_code" \ -d "code=" \ -d "code_verifier=" ``` **Response:** ```json { "access_token": "ACCESS_TOKEN", "refresh_token": "REFRESH_TOKEN", "expires_in": 900, "token_type": "Bearer" } ``` ## Using Access Tokens Include the access token in the `Authorization` header of your API requests: ```curl curl https://api.givechariot.com/v1/donations \ -H "Authorization: Bearer ACCESS_TOKEN" ``` Access tokens expire after **15 minutes**. Use the refresh token to obtain a new one. ## Refreshing Tokens When an access token expires, exchange your refresh token for a new token pair. > **Warning** > > Refresh tokens are single-use. After a successful exchange, you must use the > new refresh token returned in the response. Reusing a previously exchanged > refresh token may result in authorization revocation, requiring the nonprofit > to re-authorize your application. ```curl curl -X POST https://api.givechariot.com/auth/oauth/token \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -d "grant_type=refresh_token" \ -d "refresh_token=REFRESH_TOKEN" ``` **Response:** ```json { "access_token": "NEW_ACCESS_TOKEN", "refresh_token": "NEW_REFRESH_TOKEN", "expires_in": 900, "token_type": "Bearer" } ``` Refresh tokens use a **sliding window** expiration: * Each use extends the refresh token's lifetime by **31 days** * The absolute maximum lifetime is **365 days**, and refreshing does not extend it * After 365 days, the user must re-authorize your application ## Token Lifetimes | Token | Lifetime | | ------------------ | ------------------------------- | | Authorization code | 1 minute | | Access token | 15 minutes | | Refresh token | 31 days (sliding), 365 days max | ## Error Handling Common errors during the token exchange: | HTTP Status | Cause | | ----------- | -------------------------------------------------------------------- | | `400` | Malformed request (invalid parameters, missing fields) | | `401` | Invalid authorization code, expired token, or bad client credentials | | `412` | User lacks the required permissions for the requested scopes | ## IP Whitelisting Some APIs restrict access to a set of registered IP addresses in addition to OAuth. Where this applies, the check runs before the token is validated — a request from an unregistered address receives a `403 Forbidden` even with a valid access token: ```json { "type": "about:blank", "title": "You have insufficient permissions to perform this action.", "status": 403, "detail": "Your IP address is not in the allowed CIDR range to access this resource." } ``` Addresses are registered as CIDR ranges when your application is set up. Contact [support@givechariot.com](mailto:support@givechariot.com) to add or change them. ## Redirect URI Requirements Redirect URIs are validated when your application is registered and again at authorization time. Each URI must meet the following requirements: | Rule | Requirement | | ----------------------- | --------------------------------------------------------------- | | **Scheme** | Must use `https` | | **Host** | Must include a hostname | | **Path** | Must include a path beyond `/` (e.g., `/callback`) | | **No query parameters** | Query strings are not allowed | | **No fragments** | URL fragments (`#`) are not allowed | | **No user info** | Credentials in the URL (e.g., `user:pass@host`) are not allowed | | **Length** | Must be 255 characters or fewer | | **Max count** | You can register up to 5 redirect URIs per client | At authorization time, the `redirect_uri` parameter must **exactly match** one of your registered URIs. Scheme and host are compared case-insensitively, but the path must match exactly. Wildcard or pattern matching is not supported. **Valid examples:** ``` https://example.com/callback https://example.com/auth/chariot/callback https://example.com:443/callback ``` **Invalid examples:** ``` http://example.com/callback # HTTP not allowed in production https://example.com # Missing path https://example.com/ # Root path not allowed https://example.com/cb?foo=bar # Query parameters not allowed ``` > OAuth allows external applications to access the Chariot API on behalf of a nonprofit organization