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:readis 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_url | The shop's site on your platform. HTTPS |
models[].id, name | Required per model. Ids are yours; a repeated id keeps the last |
models[].sqft, stories, bedrooms, bathrooms | Optional. sqft is total floor area |
models[].image_url, edit_url, view_url | Optional, HTTPS. edit_url is the Edit link the shop sees |
stats.savers_30d | People who saved a model in the last 30 days |
stats.saved_models_total | Models saved, all time |
stats.sales_count, sales_cents, currency | Sales 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" }
}
| Status | Meaning |
|---|---|
201 created | A new job for the shop |
200 existing | The same external_id again: the same job, nothing new created. Safe to retry |
202 lead_sent | The 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_approved | The shop is not approved on Gage yet |
422 | A 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
| Scope | What it allows |
|---|---|
profile:read | The shop's profile, as above. Always included |
integration:write | Show your account inside Gage: PUT /api/v1/shop/app |
jobs:write | Send 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.