Introduction
The GDS API lets approved apps sign users in with their Grubb Digital Services account and act on their behalf.
All requests must use HTTPS. Request bodies can be sent as application/x-www-form-urlencoded or JSON. Responses are always JSON.
Authentication
GDS uses the OAuth 2.0 authorization code flow with passwordless email sign-in. Users never type a password; they get a one-time link by email instead.
- Your app sends the user to the GDS auth portal.
- The user enters their email and clicks the link we send them.
- GDS redirects back to your redirect_uri with a short-lived code.
- Your server exchanges that code, plus your client secret, for an access token.
Clients can't currently be created through the API. You must use a preregistered client.
// The full flow
Your app ──redirect──▶ auth.grubbdigital.services
│
▼ user enters email
📧 magic link
│
▼ user clicks
Your redirect_uri ◀── ?code=…&state=…
│
▼ server-to-server
POST api.grubbdigital.services/oauth/token
│
▼
{ access_token, refresh_token }
1. Redirect the user
Send the user to the auth portal with these query parameters.
https://auth.grubbdigital.services/ ?client_id=$client_id &redirect_uri=$redirect_uri &response_type=code &state=$state
URL-encode each value.
2. User signs in
GDS handles this step. The user enters their email address and we send them a sign-in link.
- Links expire after 15 minutes and work once.
- Links are limited to 5 per email address every 15 minutes.
- For privacy, the portal shows the same message whether or not the email has an account.
3. Handle the callback
Once the user clicks the link, GDS redirects them to your redirect_uri with these query parameters:
Check that state matches the one you stored. If it doesn't, stop. Otherwise, pass the code to your server for step 4.
GET $redirect_uri ?code=Xk9v2L… &state=$state
4. Exchange the code
POST /oauth/token
Call this from your server. Never from a browser.
Response
curl -X POST https://api.grubbdigital.services/oauth/token \ -d "grant_type=authorization_code" \ -d "client_id=$client_id" \ -d "client_secret=$client_secret" \ -d "redirect_uri=$redirect_uri" \ -d "code=$code"
{
"access_token": "q8Jf3…",
"refresh_token": "Z1m0a…",
"token_type": "Bearer",
"expires_in": 28800,
"user_id": "usr_…",
"scope": "…"
}
Refresh a token
POST /oauth/token
When an access token expires, get a new one without asking the user to sign in again.
The response has the same shape as step 4. Refresh tokens rotate: each refresh returns a new one and the old one stops working, so save the new token every time.
curl -X POST https://api.grubbdigital.services/oauth/token \ -d "grant_type=refresh_token" \ -d "client_id=$client_id" \ -d "client_secret=$client_secret" \ -d "refresh_token=$refresh_token"
Authenticated requests
Send the access token in the Authorization header on every request to an endpoint that needs a signed-in user.
If the token is missing, invalid or expired, you'll get a 401. Refresh it and try again.
Authorization: Bearer $access_token
Errors
Errors return an HTTP error status and a JSON body with a machine-readable code and a human-readable message.
{
"success": false,
"error": {
"code": "invalid_grant",
"message": "Invalid or expired code or token"
}
}