Docs / Partners

Connect with Gage

Let fabricators connect their Gage account to your app with OAuth, read their shop profile, show your account inside Gage, and send them jobs.

Fabricators can connect their Gage account to your app. Once connected, your app reads the shop's profile, and your app appears in the shop's Gage sidebar with a link straight to it. With the extra scopes below, your app can also show the account it runs for the shop inside Gage (its site link, models and numbers), and send the shop jobs from buyers on that site. The shop can disconnect at any time from its Integrations page, which revokes your access at once.

It is standard OAuth 2.0, the authorization code flow with PKCE. Gage is the authorization server.

#1. Get registered

Gage registers your app and gives you a client_id and a client_secret (shown once; keep it on your server). Send us:

  • Redirect URIs: where Gage sends the shop back after it approves, e.g. https://app.example.com/oauth/gage/callback. HTTPS only (localhost is allowed for development).
  • Signup URL: where a fabricator lands when it presses Connect on your card in Gage. Your sign-in or signup, which then starts the flow below. Gage adds return_to, the app's page in Gage (e.g. https://buildwithgage.com/fabricators/integrations/ekstra): once the shop is connected and its account is set up on your side, send the fabricator there. They see your app connected, with your account on it.
  • Launch URL: where the link in the shop's Gage sidebar goes once it is connected.
  • Icon: a square image, 256px or larger.
  • Scopes you need (see the table below). profile:read is always included.

#2. Send the shop to Gage

From your signup or settings page, redirect the fabricator to:

https://buildwithgage.com/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fgage%2Fcallback
  &scope=profile:read%20integration:write%20jobs:write
  &state=RANDOM_VALUE_YOU_CHECK_ON_RETURN
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

The fabricator signs in to Gage if they are not already, sees your app, their shop, and what you will be able to see, and presses Connect. Gage then sends them to your redirect_uri with ?code=...&state=..., or ?error=access_denied&state=... if they cancel. Check that state is the value you sent.

#3. Exchange the code

Within ten minutes, from your server:

curl -X POST https://buildwithgage.com/api/oauth/token \
  -u "$GAGE_CLIENT_ID:$GAGE_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri="https://app.example.com/oauth/gage/callback" \
  -d code_verifier="$VERIFIER"
{ "access_token": "gat_...", "token_type": "Bearer", "scope": "profile:read", "shop_id": "fab_op_..." }

A code works once. Store the token against the shop: it does not expire, and stops working when the shop disconnects your app. A public client with no secret authenticates with code_verifier alone.

#4. Read the shop

curl https://buildwithgage.com/api/v1/shop -H "Authorization: Bearer $TOKEN"
{
  "id": "fab_op_58da27f7-...",
  "company_name": "Steel Co",
  "location": { "city": "Salt Lake City", "state": "UT", "zip": "84104" },
  "logo_url": "https://...",
  "facility_image_url": "https://...",
  "project_types": ["single_family"],
  "capabilities": {
    "accepting_jobs": true,
    "max_job_sqft": null,
    "weekly_capacity_sqft": 6000,
    "complete_homes": true,
    "complete_commercial": false
  },
  "approved": true
}

401 means the token is not valid or the shop disconnected: send the shop back through step 2. The profile is the shop's public face only; your app cannot see its prices, jobs, customers or billing.

#5. Show your account inside Gage

With integration:write, push the account you run for the shop. Gage shows it read-only on your app's page in the shop's Integrations; every model links back to you to edit.

curl -X PUT https://buildwithgage.com/api/v1/shop/app \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "account_url": "https://steelco.ekstra.com",
    "models": [
      {
        "id": "mdl_123",
        "name": "The Juniper",
        "sqft": 1840,
        "stories": 2,
        "bedrooms": 3,
        "bathrooms": 2.5,
        "image_url": "https://cdn.ekstra.com/juniper.jpg",
        "edit_url": "https://app.ekstra.com/models/mdl_123/edit",
        "view_url": "https://steelco.ekstra.com/models/juniper",
        "updated_at": "2026-09-30T15:00:00Z"
      }
    ],
    "stats": { "savers_30d": 41, "saved_models_total": 212, "sales_count": 3, "sales_cents": 18450000, "currency": "usd" }
  }'

Every part is optional, and a part you send replaces what Gage had: send account_url and models when the account is created and whenever a model changes (the full list each time, up to 500), and stats on a schedule, such as nightly. null clears account_url. Gage answers with what it now shows; GET the same URL to read it back.

Field
account_urlThe shop's site on your platform. HTTPS
models[].id, nameRequired per model. Ids are yours; a repeated id keeps the last
models[].sqft, stories, bedrooms, bathroomsOptional. sqft is total floor area
models[].image_url, edit_url, view_urlOptional, HTTPS. edit_url is the Edit link the shop sees
stats.savers_30dPeople who saved a model in the last 30 days
stats.saved_models_totalModels saved, all time
stats.sales_count, sales_cents, currencySales on your platform for this shop

Counts only: send no buyer names or emails in stats.

#6. Send the shop a job

With jobs:write, when a buyer on the shop's site wants to build a model, send it. It becomes the shop's own job in Gage, priced at the shop's rates for the build site, and never goes to another fabricator. The shop is emailed and sees it in its Jobs. Gage does not email the buyer: you do, and you can give them the project_url, where they follow the job.

curl -X POST https://buildwithgage.com/api/v1/shop/jobs \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "external_id": "ekstra_order_9812",
    "customer": { "name": "Dana Ruiz", "email": "dana@example.com", "phone": "801-555-0100" },
    "model": { "id": "mdl_123", "name": "The Juniper", "url": "https://steelco.ekstra.com/models/juniper" },
    "sqft": 1840,
    "stories": 2,
    "delivery_zip": "84043",
    "message": "Hoping to break ground in spring."
  }'
{
  "status": "created",
  "ref_number": "GGE-2026-4821",
  "project_url": "https://buildwithgage.com/project/…",
  "estimate": { "low": 71200, "high": 78900, "currency": "usd" }
}
StatusMeaning
201 createdA new job for the shop
200 existingThe same external_id again: the same job, nothing new created. Safe to retry
202 lead_sentThe shop has no active Gage plan (reason: "no_plan") or has not set its prices ("no_rates"). Nothing is stored; the shop was emailed the buyer's details. Keep the lead on your side
409 shop_not_approvedThe shop is not approved on Gage yet
422A field is missing or wrong; param names it. delivery_zip is a 5-digit US ZIP

sqft is total floor area across all stories.

#Scopes

ScopeWhat it allows
profile:readThe shop's profile, as above. Always included
integration:writeShow your account inside Gage: PUT /api/v1/shop/app
jobs:writeSend the shop jobs: POST /api/v1/shop/jobs

The shop sees each scope on the approval screen. A scope must be enabled for your app by Gage before you can ask for it; to add one later, send the shop through step 2 again with the new scope. Tokens issued before carry only the scopes they were issued with.

#Revoking

To give up a token (a user disconnects on your side), POST https://buildwithgage.com/api/oauth/revoke with token=.... It always answers 200.