Speak, friend, and enter.
All Basecamp 5 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 5 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.
GET https://3.basecampapi.com/authorization.jsonwill return the authorization details for a token, served by Basecamp itself.
Basecamp serves its own authorization document at the API root — no account prefix — for any token it accepts: a Basecamp-issued OAuth token or personal access token, or a legacy Launchpad-issued token. It mirrors Launchpad's document above, with a few deliberate differences:
identitycarries onlyid. The name and email fields are omitted: they were never suitable for identifying users within Basecamp (see the note on the Launchpad document above) — use the Get person endpoints.- Each account carries a
resourceindicator (urn:bc:account:<id>, RFC 8707) instead of Launchpad'sproductandapp_href. Pass it as theresourceparameter when requesting a token scoped to that account. A client that reads both documents must treatproductandapp_hrefas optional and select accounts byhreforresource. scopeis present for every Basecamp-issued token — OAuth and personal access tokens alike. Legacy Launchpad-issued tokens predate scopes, so a missingscopeis not an error.expires_atis an ISO 8601 timestamp, as on Launchpad.
{
"identity": {
"id": 9999999
},
"accounts": [
{
"id": 99999999,
"name": "Honcho Design",
"href": "https://3.basecampapi.com/99999999",
"resource": "urn:bc:account:99999999"
}
],
"scope": "full",
"expires_at": "2026-02-16T12:00:00.000Z"
}curl -H "Authorization: Bearer $ACCESS_TOKEN" \
-A 'MyApp (yourname@example.com)' \
https://3.basecampapi.com/authorization.jsonThis example presents a bearer token. A DPoP-bound token authenticates here
the same way it does at every other endpoint: Authorization: DPoP <token>
plus its DPoP proof header — the Bearer scheme is rejected for bound
tokens (RFC 9449 §7.1).
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..."
}