Pages in this manual

For developers

API reference

The routes, limits and rules of Forge's public API, built from its published description.


The public API reads one company's Forge data with plain web requests, and no AI is involved. This reference is built from Forge's published description of the API, so it lists exactly the routes, limits and rules that description states.

Key or OAuth

  • An API key is for software one company runs for itself. An owner or admin of that company creates it on the web and gives it to you. See use an API key instead.
  • OAuth is for a product that serves many Forge customers. Each contractor approves your product for their own company on Forge's own page. See register an app.

Both only read, and both read one company. A key works on the public API and the MCP server alike. An OAuth token works on the one it was issued for, so a product that uses both asks for a token for each.

Addresses

This manual was built for https://api.forge.equipment/v1. The published description names these servers, and every path below starts from one of them:

  • Production: https://api.forge.equipment
  • Staging: https://forge-backend-staging-production.up.railway.app

Read one company's Forge data: jobs, their photos and areas, scopes, job walks with their transcripts and recordings, and estimates and change orders.

Prices

Estimates and change orders answer the customer view by default: customer prices only, what the homeowner is charged. view=internal adds the company's own figures; never show those to a homeowner. When priceUnavailableReason is set: No customer price exists for this estimate. Do not compute one from cost.

Authentication

Send Authorization: Bearer <token> with either an OAuth access token issued for resource=https://api.forge.equipment/v1 (authorization code with PKCE) or a personal API key (fgo_key_...) created by a company owner or admin on the Forge web app. Every call reads the one company the grant or key belongs to. No route takes a company parameter.

Read only

There are no write routes.

Rate limits

120 requests a minute and 20,000 requests a day per grant or key. Over a limit the answer is 429 with a Retry-After header in seconds.

Paging

List routes return at most 100 items a page (default 20). Pass nextCursor back as cursor to read the next page; it is null on the last page. A cursor is bound to the route, its filters and the company.

Incremental sync

GET /v1/jobs takes updatedSince (ISO 8601): only jobs changed at or after that time. Order: updatedAt descending, then id descending; a cursor resumes exactly after the last item.

Versioning

The version is the /v1 path prefix. Within /v1 Forge only adds: new routes, new optional query parameters and new response fields. Clients must ignore response fields they do not know. Removing or renaming a field, changing its type or meaning, or adding a required parameter happens only under a new prefix (/v2), announced at least 90 days before /v1 changes.

Errors

Every error body is { errorCode, message, requestId }. Unknown query parameters are refused with 400.

Browsers

The API answers no browser origin: keys and tokens are held on a server.

Ways in

  • A personal API key, sent as a bearer token. Keys start with fgo_key_.
  • OAuth, authorization code with PKCE. Authorize at https://api.forge.equipment/oauth/authorize, exchange the code at https://api.forge.equipment/oauth/token, with scope forge.read and resource https://api.forge.equipment/v1.

Routes

GET /v1/jobs

Find jobs

Parameters

  • cursor query, string

    The nextCursor of the previous page, unchanged. Bound to this route, its filters and the company.

  • limit query, integer

    Items per page, 1 to 100. Default 20.

  • query query, string

    Case-insensitive match on the job name.

  • stage query, string: presale, pre_construction, active, closed

    The job's lifecycle phase.

  • status query, string: active, archived

  • updatedSince query, string (date-time)

    Incremental sync: only jobs changed at or after this time (ISO 8601). Order is updatedAt descending, then id descending.

Answers

  • 200 JobPage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/jobs/{id}

One job, its area list and Forge's JobTread send record

Parameters

  • id path, required, string

    A job id.

Answers

  • 200 JobDetail
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/jobs/{id}/photos

A page of one job's photos

Parameters

  • id path, required, string

    A job id.

  • cursor query, string

    The nextCursor of the previous page, unchanged. Bound to this route, its filters and the company.

  • limit query, integer

    Items per page, 1 to 100. Default 20.

  • area query, string

    An area slug or label, any case. An area that matches nothing is an empty page.

  • trade query, string

    A trade label, any case, combined with area.

  • includeHidden query, boolean

    Hidden photos are left out unless this is true.

Answers

  • 200 PhotoPage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/photos/{id}

One photo's image

The resized copy, at most 1568 px on its long edge and 1 MB, as JPEG bytes through this API. A photo is never a link.

Parameters

  • id path, required, string

    A photo id.

  • includeHidden query, boolean

    A hidden photo is refused (PHOTO_HIDDEN) unless this is true.

Answers

  • 200 The JPEG bytes.
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 422 PHOTO_HIDDEN, PHOTO_NO_RESIZED_COPY or PHOTO_OVER_IMAGE_CAP. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/scopes/{id}

The current scope

Parameters

  • id path, required, string

    A scope document id.

  • view query, string: customer, internal

    customer (the default) leaves out the company's internal sections.

  • cursor query, string

    The nextCursor of the previous page, unchanged.

Answers

  • 200 ScopePage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/walks

Walks, sweeps and job-level recordings

Parameters

  • cursor query, string

    The nextCursor of the previous page, unchanged. Bound to this route, its filters and the company.

  • limit query, integer

    Items per page, 1 to 100. Default 20.

  • jobId query, string

    Only walks on this job.

  • from query, string (date-time)

    Recorded at or after this time (ISO 8601).

  • to query, string (date-time)

    Recorded before this time (ISO 8601).

Answers

  • 200 WalkPage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/walks/{id}/transcript

A page of one walk's transcript

Parameters

  • id path, required, string

    A walk id from GET /v1/walks.

  • cursor query, string

    The nextCursor of the previous page, unchanged.

  • fromSeconds query, number

    Start of a time range, seconds into the take.

  • toSeconds query, number

    End of a time range, seconds into the take.

  • text query, string: shown, original

    original returns a translated walk's original-language text.

  • takeId query, string

    Only this take's segments (a chapter's takeId).

Answers

  • 200 TranscriptPage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/walks/{id}/recordings

One walk's recordings and chapters

Each audio or video take as a link that works for 15 minutes (every issue is logged), and each area's chapter.

Parameters

  • id path, required, string

    A walk id from GET /v1/walks.

Answers

  • 200 WalkRecordings
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)
  • 503 A link could not be logged, so none was issued. ApiError

GET /v1/estimates

The current estimates

The current version of each estimate, drafts included and labelled. view=customer, the default, carries customer prices only: what the homeowner is charged. view=internal returns the same object with the company's own figures added beside each price, and the markup; never show those to a homeowner. When priceUnavailableReason is set: No customer price exists for this estimate. Do not compute one from cost.

Parameters

  • cursor query, string

    The nextCursor of the previous page, unchanged. Bound to this route, its filters and the company.

  • limit query, integer

    Items per page, 1 to 100. Default 20.

  • status query, string: draft, approved, sent, signed

    Only estimates with this status label.

  • updatedSince query, string (date-time)

    Incremental sync: only estimates changed at or after this time (ISO 8601). Order is updatedAt descending, then id descending.

  • view query, string: customer, internal

    customer (the default) carries customer prices only. internal adds the company's own figures beside them; never show those to a homeowner.

Answers

  • 200 EstimateList
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/estimates/{id}

One estimate

The header and totals on every page, then one page of lines; nextCursor reads the rest. view=customer, the default, carries customer prices only: what the homeowner is charged. view=internal returns the same object with the company's own figures added beside each price, and the markup; never show those to a homeowner. When priceUnavailableReason is set: No customer price exists for this estimate. Do not compute one from cost.

Parameters

  • id path, required, string

    An estimate id.

  • view query, string: customer, internal

    customer (the default) carries customer prices only. internal adds the company's own figures beside them; never show those to a homeowner.

  • cursor query, string

    The nextCursor of the previous page, unchanged.

Answers

  • 200 EstimatePage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

GET /v1/change-orders/{id}

One change order

Its lines marked added, removed or changed, priced from the markup frozen when it was signed. Never the signature. view=customer, the default, carries customer prices only: what the homeowner is charged. view=internal returns the same object with the company's own figures added beside each price, and the markup; never show those to a homeowner. When priceUnavailableReason is set: No customer price exists for this estimate. Do not compute one from cost.

Parameters

  • id path, required, string

    A change order id, as GET /v1/jobs/{id} names it.

  • view query, string: customer, internal

    customer (the default) carries customer prices only. internal adds the company's own figures beside them; never show those to a homeowner.

  • cursor query, string

    The nextCursor of the previous page, unchanged.

Answers

  • 200 ChangeOrderPage
  • 400 A query parameter is unknown or not valid. ApiError
  • 401 No token or key, or a dead one. ApiError
  • 403 Refused: role, data access, billing or a browser origin. ApiError
  • 404 Not found in this company. ApiError
  • 429 Over a rate limit. Retry-After says when to try again. ApiError (header Retry-After)

Response shapes

JobPage

  • items array of Job

  • nextCursor string, may be null

  • truncated boolean

    True when the page stopped early to stay under the size cap; nextCursor resumes.

Job

  • externalLinks array of ExternalJobLink

  • id string

  • name string, may be null

  • stage string: presale, pre_construction, active, closed, may be null

  • status string: active, archived

  • createdAt string (date-time)

  • updatedAt string (date-time)

  • clientName string, may be null

  • clientEmail string, may be null

  • clientPhone string, may be null

  • siteAddress SiteAddress, may be null

  • description string, may be null

  • descriptionSource string: forge, person, may be null

  • jobType string, may be null

  • permitRequired string, may be null

  • documents array of DocumentRef

  • photoCount integer

  • walkCount integer

  • hasScope boolean

    True when a person has confirmed the job's scope: an approved scope document, a legacy scope meeting, or a locked Blueprint estimate.

SiteAddress

  • street string, may be null

  • city string, may be null

  • state string, may be null

  • zipCode string, may be null

DocumentRef

  • id string

  • documentType string: estimate, scope_meeting, scope_document

  • version integer

  • status string: draft, approved, sent, signed, may be null

  • updatedAt string (date-time)

ApiError

  • errorCode string

    A stable code for the refusal.

  • message string

    A sentence for a person.

  • requestId string, may be null

    Quote this when asking Forge for help.

JobDetail

  • job Job

  • areas array of JobArea

  • jobTread JobTreadBlock, may be null

    Forge's own record of what it sent to the JobTread job this job is linked to, read only and never read from JobTread. Check it, and JobTread itself, before creating anything Forge may already have sent. Null when Forge never linked the job to JobTread.

JobArea

  • slug string

  • label string

  • photoCount integer

    Photos filed to this area, hidden ones left out.

  • trades array of string

JobTreadBlock

  • source string: forge_send_record

    What Forge sent, not what JobTread holds now.

  • connection string: connected, needs_reconnect, not_connected

    The connection state Forge holds; never read from JobTread.

  • jobtreadJobId string

    The JobTread job id Forge linked this job to.

  • estimates array of JobTreadEstimateSend

    Newest first.

  • dailyLogs array of JobTreadDailyLogSend

    Newest first.

  • changeOrders array of JobTreadChangeOrderSend

    Newest first.

  • truncated boolean

    True when older sends were left out to keep the block bounded.

JobTreadEstimateSend

  • estimateId string

    The Forge estimate id (GET /v1/estimates/{id}).

  • version number, may be null

    The estimate version Forge sent.

  • sentAt string (date-time)

    When Forge sent it.

JobTreadDailyLogSend

  • walkId string

    The GET /v1/walks id of the walk or sweep the daily log was sent from.

  • sentAt string (date-time)

    When Forge sent it.

JobTreadChangeOrderSend

  • changeOrderId string

    The Forge change order id (GET /v1/change-orders/{id}).

  • sentAt string (date-time)

    When Forge sent it.

PhotoPage

  • items array of Photo

  • nextCursor string, may be null

  • truncated boolean

  • filter PhotoFilter

  • omitted array of PhotoOmission

    Photos left off this page, with the reason.

Photo

  • id string

  • jobId string

  • source string: meeting, task

  • caption string, may be null

  • takenAt string (date-time), may be null

  • phase string: presale, pre_construction, active, closed, may be null

  • scopeLineItemId string, may be null

  • walkId string, may be null

    The GET /v1/walks id of the walk it was taken on.

  • takeId string, may be null

    The transcript take it was taken during.

  • offsetSeconds number, may be null

    Seconds into the take.

  • area PhotoArea, may be null

  • trades array of string

  • filedBy string: timeline, person, may be null

  • confidence string: high, medium, low, may be null

  • filingStatus string: auto, confirmed, unfiled, may be null

  • hidden boolean

    Hidden photos appear only with includeHidden=true.

  • isFrame boolean

    True for a frame pulled from a walk video, not a photo a person took. Frames stay off every customer document until a person promotes one.

PhotoArea

  • slug string

  • label string

PhotoFilter

  • area string, may be null

  • trade string, may be null

  • includeHidden boolean

PhotoOmission

  • photoId string

  • reason string: NO_RESIZED_COPY, RESIZED_COPY_CHECK_FAILED

ScopePage

  • id string

  • jobId string

  • version integer

  • status string: draft, approved, sent, signed

  • documentType string: scope_meeting, scope_document

  • view string: customer, internal

  • sections array of ScopeSection

  • nextCursor string, may be null

  • truncated boolean

ScopeSection

  • key string

  • title string

  • items array of object

  • continues boolean

    True when this section continues on the next page.

WalkPage

  • items array of Walk

  • nextCursor string, may be null

  • truncated boolean

Walk

  • id string

    wm_ a walk, ws_ a sweep, wj_ a job-level recording.

  • kind string: walk, sweep, legacy_job_recording

  • jobIds array of string

  • recordedAt string (date-time)

  • walkType string, may be null

  • jobPhase string: presale, pre_construction, active, closed, may be null

  • takeCount integer

  • durationSeconds number, may be null

  • transcriptState string: ready, transcribing, failed, none

  • hasTranscript boolean

  • hasAudio boolean

  • hasVideo boolean

  • hasAreas boolean

    At least one take has an area timeline (chapters).

  • has3D boolean

    A ready 3D area exists for this walk. False while 3D capture is switched off.

TranscriptPage

  • walkId string

  • kind string: walk, sweep, legacy_job_recording

  • state string: ready, transcribing, failed, none

  • language string, may be null

  • translated boolean

  • text string: shown, original

  • segments array of TranscriptSegment

  • recordings array of RecordingLink, may be null

    First page only.

  • chapters array of TranscriptChapter, may be null

    First page only.

  • lineMoments array of LineMoment, may be null

    First page only. Where each scope line of the job's active scope was said in this walk (seconds into its take, read from the transcript). Empty when no line has a fresh moment: a moment is withheld once the scope changed since it was stamped. A line said in two takes appears once, in the earliest.

  • areasSource string: timeline, none, may be null

    First page only. 'timeline': chapters came from the walk's area timeline. 'none': the walk has no area chapters (no area named, or no timeline).

  • threeD array of ThreeDLink, may be null

    First page only. The walk's 3D areas with their states; a ready one carries a 15 minute viewer link when this connection's consent names 3D. Empty when the walk has none or 3D is switched off.

  • nextCursor string, may be null

  • truncated boolean

TranscriptSegment

  • start number, may be null

  • end number, may be null

  • text string

  • takeId string, may be null

  • jobId string, may be null

  • category string, may be null

  • decision string, may be null

  • proposedTodo ProposedTodo, may be null

  • segmentId string, may be null

    A sweep segment's id; null on a walk.

  • synced SegmentSync, may be null

    The other system's object this sweep segment was marked synced with.

ProposedTodo

  • title string

  • description string

  • trade string

  • priority string, may be null

SegmentSync

  • system string

  • externalObjectId string

TranscriptChapter

  • areaSlug string

  • areaLabel string

  • takeId string

  • startSeconds number

  • endSeconds number

LineMoment

  • scopeItemId string

  • areaSlug string, may be null

  • takeId string

  • atSeconds number

WalkRecordings

EstimateList

  • items array of EstimateSummary

  • nextCursor string, may be null

  • truncated boolean

EstimateSummary

  • id string

  • jobId string

  • version integer

  • status string: draft, approved, sent, signed

  • updatedAt string (date-time)

  • view string: customer

  • priceUnavailableReason string: RATE_BASIS_REVIEW_REQUIRED, MARKUP_NOT_SET, LEGACY_SIGNED_PRICE_UNAVAILABLE, COMPANY_PRICING_UNAVAILABLE, may be null

    Set when no customer price exists; every price field is then null. No customer price exists for this estimate. Do not compute one from cost.

  • totals EstimateTotals

EstimateTotals

  • customerSubtotal number, may be null

    The sum of the line totals in the contract total, before tax.

  • taxLines array of EstimateTaxLine

  • customerTotal number, may be null

    What the homeowner is charged, tax included.

EstimateTaxLine

  • displayLabel string

  • ratePct number

  • amount number, may be null

    Null when prices are refused.

EstimatePage

EstimateHeader

  • id string

  • kind string: estimate

  • jobId string

  • version integer

  • status string: draft, approved, sent, signed

    Drafts are included and labelled draft.

  • view string: customer

  • updatedAt string (date-time)

  • priceUnavailableReason string: RATE_BASIS_REVIEW_REQUIRED, MARKUP_NOT_SET, LEGACY_SIGNED_PRICE_UNAVAILABLE, COMPANY_PRICING_UNAVAILABLE, may be null

    Set when no customer price exists; every price field is then null. No customer price exists for this estimate. Do not compute one from cost.

EstimateLine

  • pricingNeedsReview boolean

    True when quantity and rate evidence need review. Prices are withheld until resolved.

  • lineId string, may be null

  • title string, may be null

  • section string, may be null

  • subgroup string, may be null

  • bucket string: standard, conditional, optional, client_supplied

    client_supplied lines are $0; optional lines sit outside the contract total.

  • rowType string, may be null

  • trade string, may be null

  • description string, may be null

  • qty number, may be null

  • unit string, may be null

  • customerUnitPrice number, may be null

  • customerLineTotal number, may be null

ChangeOrderPage

ChangeOrderHeader

  • id string

  • kind string: change_order

  • jobId string

  • number integer

  • title string

  • status string: draft, pending_signature, signed, rejected, void, may be null

  • signedAt string (date-time), may be null

  • signatureMethod string: esig, verbal_recorded, paper_uploaded, may be null

  • description string, may be null

    The text the client signed.

  • view string: customer

  • priceUnavailableReason string: RATE_BASIS_REVIEW_REQUIRED, MARKUP_NOT_SET, LEGACY_SIGNED_PRICE_UNAVAILABLE, COMPANY_PRICING_UNAVAILABLE, may be null

    Set when no customer price exists; every price field is then null. No customer price exists for this estimate. Do not compute one from cost.

ChangeOrderLine

  • lineId string, may be null

  • title string, may be null

  • description string, may be null

  • trade string, may be null

  • qty number, may be null

  • unit string, may be null

  • change string: added, removed, changed, may be null

  • customerUnitPrice number, may be null

  • customerLineTotal number, may be null

    A removed line is a credit (negative).