Skip to navigation
OAuth

OAuth

OAuth allows external applications to access the Chariot API on behalf of a nonprofit organization

OAuth access is not self-service. Contact support@givechariot.com to register your application and receive client credentials. Include your application’s display name, logo, and callback URLs.

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 sends these credentials only to the token endpoint, using HTTP Basic Auth, and calls the rest of the Chariot API with the access tokens it gets back. There are two kinds:

  • A nonprofit access token, from the authorization code flow, acts on behalf of one nonprofit. Use it for almost every API call.
  • An application access token, from the client_credentials grant, acts on behalf of your application itself. Use it to set up webhooks.

Your client secret should be kept secure and should never be exposed to the public.

Environments

EndpointProductionSandbox
Authorizationhttps://dashboard.givechariot.com/oauth/authorizehttps://dashboard.givechariot.com/oauth/sandbox/authorize
Tokenhttps://api.givechariot.com/auth/oauth/tokenhttps://sandboxapi.givechariot.com/auth/oauth/token
OIDC configurationhttps://api.givechariot.com/.well-known/openid-configurationhttps://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.

Scopes typically take the form resource:action, where action is read or write. A write scope includes read access to the same resource.

ResourceReadWrite
Donationsdonations:readdonations:write
Propertiesproperties:readproperties:write
Depositsdeposits:readdeposits:write
Mail itemsmail_items:readmail_items:write
Lockboxeslockboxes:read
Grants and recurring grantsgrants:readgrants:write
Connectsconnects:readconnects:write
Grant requestsgrant_requests:readgrant_requests:write
Donor accountsdonor_accounts:readdonor_accounts:write
Payment sourcessources:readsources:write
Deposit addressesdeposit_addresses:readdeposit_addresses:write
Disbursementsdisbursements:readdisbursements:write
Inbound transfersinbound_transfers:readinbound_transfers:write
Verification requestsverification_requests:readverification_requests:write
Financial accountsfinancial_accounts:read
Event subscriptionsevent_subscriptions:readevent_subscriptions:write
Eventsevents:read

Other Scopes

ScopeGrants
app_installInstall your application for the nonprofit. See Delegated Access and App Installs
openidIssue an id_token and access the UserInfo endpoint. See Identity
profile, emailSame as openid
offline_accessAccepted for compatibility with OIDC clients

Deprecated Scopes

ScopeUse instead
read_onlyThe read scope for each resource you need
read_writeThe write scope for each resource you need

These scopes are deprecated and will be removed in the future.

The user who approves your application must hold every scope you request. With delegated access, the token also loses any permission that user’s role later loses.

Delegated Access and App Installs

Chariot supports two authorization models:

  • Delegated access: Your application acts on behalf of the user who authorized it. Its permissions are limited by that user’s current role.
  • App install: Your application acts independently of any individual user, with permissions approved by a nonprofit Owner or Admin.

To request an app install, include the app_install scope. Otherwise, the authorization uses delegated access.

Delegated AccessApp Install
Acts asThe authorizing userYour application
Approved byAny user who holds the requested scopesA nonprofit Owner or Admin
PermissionsLimited by the authorizing user’s current roleRemain as approved
If the authorizing user’s role changesPermissions change to matchUnaffected
If the authorizing user leaves the nonprofitAccess is revoked, and your application must be authorized againUnaffected
IdentityCan request openid, profile and email. See IdentityNot available
Revoked byThe authorizing userAny nonprofit Owner or Admin

Which Should I Use?

Use delegated access when your application should act on behalf of a specific user. Use an app install for integrations that should keep working after the person who authorized them leaves the nonprofit.

Because an app install isn’t associated with a user, app_install can’t be combined with openid, profile or email. Requesting them together returns an invalid_scope error.

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:

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 };
}

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
ParameterRequiredDescription
client_idYesYour application’s client ID
redirect_uriYesURL to redirect back to (must match a registered URI). See Redirect URI Requirements
response_typeYesMust be code
scopeYesSpace-separated list of requested scopes, percent-encoded (%20) as a single query parameter value
stateRecommendedAn opaque value to prevent CSRF attacks
nonceYesCryptographically random string to prevent replay attacks. See Identity
code_challengeYesBase64url-encoded SHA-256 hash of the code verifier
code_challenge_methodYesMust 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%20donations:read
&state=RANDOM_CSRF_TOKEN
&code_challenge=YOUR_CODE_CHALLENGE
&code_challenge_method=S256

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:

https://example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_CSRF_TOKEN

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 -X POST https://api.givechariot.com/auth/oauth/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d "grant_type=authorization_code" \
-d "code=<AUTHORIZATION_CODE>" \
-d "code_verifier=<YOUR_CODE_VERIFIER>"

Response:

{
"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 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.

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 -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:

{
"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

TokenLifetime
Authorization code1 minute
Access token15 minutes
Refresh token31 days (sliding), 365 days max

Webhooks

Receive webhooks from every nonprofit that installs your application by creating application-level Event Subscriptions.

Unlike subscriptions associated with a specific nonprofit, application-level subscriptions receive events from all nonprofits that have authorized your application through an app install with the events:read scope, including those that install it in the future.

Each event includes an organization_id identifying the nonprofit that generated it. When a nonprofit revokes your application’s access, you’ll stop receiving its events without affecting events from other nonprofits. We recommend subscribing to connection.revoked to be notified when this happens.

You only need to create each subscription once, using your application’s own access token.

The event_subscriptions:write scope is not available through delegated access. Requesting it through a delegated authorization returns an invalid_scope error.

Step 1: Get an Application Access Token

Use the client_credentials grant to obtain an access token for your application. Authenticate using HTTP Basic Auth:

curl -X POST https://api.givechariot.com/auth/oauth/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d "grant_type=client_credentials" \
-d "scope=event_subscriptions:write"

Step 2: Create an Event Subscription

Call Create Event Subscription using your application access token.

Create one subscription for each event category you want to receive:

curl -X POST https://api.givechariot.com/v1/event_subscriptions \
-H "Authorization: Bearer APPLICATION_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhook",
"category": "grant.created",
"signing_secret": "YOUR_SIGNING_SECRET"
}'

Error Handling

Common errors during the token exchange:

HTTP StatusCause
400Malformed request (invalid parameters, missing fields)
401Invalid authorization code, expired token, or bad client credentials
412User lacks the required permissions for the requested scopes

Redirect URI Requirements

Redirect URIs are validated when your application is registered and again at authorization time. Each URI must meet the following requirements:

RuleRequirement
SchemeMust use https
HostMust include a hostname
PathMust include a path beyond / (e.g., /callback)
No query parametersQuery strings are not allowed
No fragmentsURL fragments (#) are not allowed
No user infoCredentials in the URL (e.g., user:pass@host) are not allowed
LengthMust be 255 characters or fewer
Max countYou 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