Speak, friend, and enter.
All Basecamp 4 API requests are authenticated by passing along an OAuth 2 token.
We use OAuth 2 so your users can authorize your application to use Basecamp on their behalf without sharing their credentials.
- Grab an OAuth 2 library.
- Register your app at launchpad.37signals.com/integrations. You'll be assigned a
client_idandclient_secret. You'll need to provide aredirect_uri: a URL where we can send a verification code. Just enter a dummy URL likehttp://myapp.com/oauthif you're not ready for this yet. - Configure your OAuth 2 library with your
client_id,client_secret, andredirect_uri. Tell it to usehttps://launchpad.37signals.com/authorization/newto request authorization andhttps://launchpad.37signals.com/authorization/tokento get access tokens. - Try making an authorized request to
https://launchpad.37signals.com/authorization.jsonto dig in and test it out! Check out the Get authorization endpoint for a description of what this returns.
If you're developing your own OAuth 2 client, here's the full flow.
TL;DR request access, receive a verification code, trade it for an access token.
https://launchpad.37signals.com/authorization/new?response_type=code&client_id=your-client-id&redirect_uri=your-redirect-uri
We authenticate their Basecamp ID and ask whether it's ok to give access to your app.
We redirect the user back to your redirect_uri with a time-limited verification code.
Your app makes a backchannel POST to trade the verification code for an access token:
POST https://launchpad.37signals.com/authorization/token
Required parameters:
grant_type=authorization_codeclient_id=your-client-idredirect_uri=your-redirect-uriclient_secret=your-client-secretcode=verification-code
The response includes an access token and a refresh token:
{
"access_token": "BAhbB0kiAbB7ImNsaWVudF9pZCI6IjEyM...",
"token_type": "Bearer",
"expires_in": 1209600,
"refresh_token": "BAhbB0kiAbB7ImNsaWVudF9pZCI6IjEyM..."
}Set the Authorization request header:
Authorization: Bearer YOUR_OAUTH_TOKEN
Make an authorized request to https://launchpad.37signals.com/authorization.json to find the user's identity and available accounts (see Get authorization).
The legacy parameters type=web_server (for authorization and token exchange) and type=refresh (for token refresh) are still accepted but not recommended. Use the standard response_type and grant_type parameters documented above instead.
GET https://launchpad.37signals.com/authorization.jsonwill return the authorization details for the authenticated user.
This endpoint requires the Authorization: Bearer YOUR_OAUTH_TOKEN header and returns:
- An
expires_attimestamp for when this token will expire (access tokens have a 2-week lifetime) - An
identitywith the user's name and email (not used for identifying users within Basecamp—use the Get person endpoints for that) - A list of
accountsthe user can access, withhreffields for making API requests
This should be the first request after obtaining a user's authorization token. Pick the account with "product": "bc3" and use its href as the base URL for Basecamp 4 API requests.
{
"expires_at": "2026-02-16T12:00:00-06:00",
"identity": {
"id": 9999999,
"first_name": "Jason",
"last_name": "Fried",
"email_address": "jason@basecamp.com"
},
"accounts": [
{
"product": "bc3",
"id": 99999999,
"name": "Honcho Design",
"href": "https://3.basecampapi.com/99999999",
"app_href": "https://3.basecamp.com/99999999"
}
]
}curl -H "Authorization: Bearer $ACCESS_TOKEN" \
-A 'MyApp (yourname@example.com)' \
https://launchpad.37signals.com/authorization.jsonNote on authenticating users via OAuth: We don't recommend using the OAuth API to authenticate users in third-party services (e.g. a "Login with Basecamp" button). We don't verify email addresses, so an attacker could gain access using email addresses they don't own.
Access tokens expire after 2 weeks. Use the refresh token to request a new access token without requiring the user to re-authorize:
POST https://launchpad.37signals.com/authorization/token
Required parameters:
grant_type=refresh_tokenrefresh_token=your-current-refresh-tokenclient_id=your-client-idclient_secret=your-client-secret
The response has the same format as the initial token exchange:
{
"access_token": "BAhbB0kiAbB7ImNsaWVudF9pZCI6IjEyM...",
"token_type": "Bearer",
"expires_in": 1209600,
"refresh_token": "BAhbB0kiAbB7ImNsaWVudF9pZCI6IjEyM..."
}