GDS

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.

API base URL
https://api.grubbdigital.services
Auth portal
https://auth.grubbdigital.services

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.

  1. Your app sends the user to the GDS auth portal.
  2. The user enters their email and clicks the link we send them.
  3. GDS redirects back to your redirect_uri with a short-lived code.
  4. Your server exchanges that code, plus your client secret, for an access token.
Keep your client secret on your server. Never put it in a URL, browser JavaScript, or a mobile app. It is only ever sent in step 4.

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.

client_id required · Your preregistered client ID.
redirect_uri required · Where to send the user afterwards. Must exactly match the URL registered to your client.
response_type required · Always code.
state recommended · A random value you generate and store (e.g. in a cookie). It comes back unchanged in step 3 so you can block forged requests.
Auth URL
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:

code An authorization code. Expires after 5 minutes and can be used once.
state The value you sent in step 1.

Check that state matches the one you stored. If it doesn't, stop. Otherwise, pass the code to your server for step 4.

Example redirect
GET $redirect_uri
  ?code=Xk9v2L…
  &state=$state

4. Exchange the code

POST /oauth/token

Call this from your server. Never from a browser.

grant_type Always authorization_code.
client_idYour client ID.
client_secretYour client secret.
redirect_uriThe same redirect_uri used in step 1.
codeThe code from step 3.

Response

access_tokenSend this with API requests. Valid for 8 hours.
refresh_tokenUse this to get a new access token. Store it securely.
token_typeAlways Bearer.
expires_inSeconds until the access token expires.
user_idThe signed-in user's ID.
scopeThe scopes granted to your client.
Request
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"
Response · 200
{
  "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.

grant_typeAlways refresh_token.
client_idYour client ID.
client_secretYour client secret.
refresh_tokenYour current refresh token.

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.

Request
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.

Header
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.

StatusCodeMeaning
400invalid_requestA required parameter is missing or invalid.
401invalid_clientUnknown client, wrong secret, or revoked client.
400invalid_grantThe code or refresh token is invalid, expired or already used.
400unsupported_grant_typegrant_type isn't one we support.
429rate_limitedToo many requests. Wait and try again.
405method_not_allowedWrong HTTP method for this endpoint.
500server_errorSomething went wrong on our end.
Error response · 400
{
  "success": false,
  "error": {
    "code": "invalid_grant",
    "message": "Invalid or expired code or token"
  }
}