How the integration works

Everything this demo does is a handful of REST calls and one URL, all made as your Partner Admin service account. This page has the journey as one sequence diagram, the full call reference, the errors to handle, and the embedding contract. The interactive API reference is at app.vergeag.com/docs.

Read the source

The demo is plain HTML and a few small ES modules - no framework, no build step, no minification: what you read is exactly what runs. One file carries the integration; the rest is demo chrome. The links below open in a small viewer, since browsers do not allow pages to link to view-source:; pressing Ctrl+U (Cmd+Option+U on Mac) on any page of this app shows the same thing natively.

FileWhat it is
js/api.js The file to read. One function per REST endpoint, request and response shapes in the comments, the fetch wrapper at the bottom out of the way.
js/config.js The three URLs your product builds (path-planning deep link, embedded-mode query, auto sign-in launch URL) at the top; demo session plumbing below.
setup.html, provision.html, companion.html, embedded.html One page per journey step; the short inline script at the bottom of each page is that step's entire logic.
js/wizard.js, js/shell.js, js/log-panel.js Demo chrome: the step-by-step provisioning wizard, the sidebar behavior, and the request log (plus source.html, the little viewer these links open). Not integration code.
data/sample-boundary.js A real ~73 ha field boundary as a closed ring of [lon,lat] pairs - the shape /api/fields/save expects.

The journey, end to end

sequenceDiagram
    autonumber
    participant YU as YourApp UI
    participant YB as YourApp backend (Partner Admin key)
    participant LA as Launch Pad API
    participant LU as Launch Pad UI

    note over YB,LA: 1. Setup - verify the key
    YB->>LA: GET /api/user/current (X-API-KEY)
    LA-->>YB: user + company/org access

    note over YB,LA: 2. Provision identity & access (idempotent, keyed by YOUR IDs)
    YB->>LA: POST /api/partner/users { partnerUserId, email, ... }
    YB->>LA: POST /api/partner/companies { partnerCompanyId, name }
    YB->>LA: POST /api/partner/company-accesses { partnerUserId, partnerCompanyId }
    LA-->>YB: Launch Pad's own IDs (userId, companyId)

    note over YB,LA: 2. Provision the land (CGFFB, keyed by LAUNCH PAD IDs)
    YB->>LA: POST /api/companies/{launchPadCompanyId}/growers
    YB->>LA: POST /api/growers/{growerId}/farms
    YB->>LA: POST /api/fields/save (field + boundary ring)
    LA-->>YB: fieldId + boundaryId

    note over YU,LU: 3. The handoff (A: new window, B: iframe)
    YU->>YB: user clicks "Plan this field"
    note over YB,LA: the code is FOR that user (your partnerUserId), not your admin key
    YB->>LA: POST /api/partner/launch { userId: partnerUserId, returnUrl }
    LA-->>YB: { code, userId, launchUrl, expiresInSec: 600 }
    YB-->>YU: launchUrl - window.open (A) or iframe src (B)
    YU->>LU: browser opens /launch?code=...
    note over LU,LA: exchange the code for THAT user's session (single-use)
    LU->>LA: POST /api/partner/launch/exchange { code, userId }
    LA-->>LU: JWT session
    LU->>LU: your user lands on the boundary, signed in as themselves
            

In a production integration the API calls run from your backend (the Partner Admin key must never ship to a browser); this demo runs them client-side purely so you can watch the traffic.

REST call reference

CallPurposeNotes
GET /api/user/current Resolve the user owning the API key Good first call to validate a key. 401 = key invalid/revoked.
POST /api/partner/users Create/find a Launch Pad user from your external user ID Partner Admin key only. Idempotent on partnerUserId - YOUR ID for the person, typically a GUID/UUID, any unique string works. email/firstName/lastName required only on first creation; supply countryCode (2-letter ISO code, e.g. "US"). Response userId is Launch Pad's own ID.
POST /api/partner/companies Create/find a customer company/organization from your external company ID Partner Admin key only. Idempotent on partnerCompanyId - YOUR ID, same rules as above; supply countryCode too. Your key's user gets admin access to the company/organization. Response companyId is Launch Pad's own ID: the {launchPadCompanyId} the land calls run against.
POST /api/partner/company-accesses Grant your user access to their company/organization Partner Admin key only. Both referenced by your external IDs. Idempotent.
POST /api/companies/{launchPadCompanyId}/growers Create a grower Body {"name", "source": "api", "archived": false}. The URL takes the Launch Pad companyId returned by provisioning, not your partnerCompanyId. Keep growerId.
POST /api/growers/{growerId}/farms Create a farm Same body shape. Keep farmId.
POST /api/fields/save Create field + boundary in one call Boundary is coordinate rings of [lon,lat], not GeoJSON. Response returns field.fieldId and vBoundary.boundaryId.
POST /api/partner/launch Issue a single-use launch code (auto sign-in) Partner Admin key only. userId = your external ID for the end user; the code signs in that user, never your admin account. Response carries the code (10-min TTL) + the Launch Pad user GUID. Redirect the browser to /launch?code=…&userId=…&returnUrl=….
POST /api/partner/launch/exchange Exchange the code for a session No API key - the code is the credential. Called by Launch Pad's own /launch page, not by your integration. Single-use: replays fail.
/path-planning/boundary/{boundaryId} UI deep link (not an API call) Use it as the launch code's returnUrl; append ?embedded=true&chrome=false for iframes.

Errors and rate limits your integration should handle

StatusMeaningResponse body
401 API key missing, invalid, expired, or revoked -
402 Account is out of credits for a billable operation {"creditsAvailable", "creditsRequired", "message"}
403 Key is valid but not allowed - e.g. a key without the Partner Admin flag calling /api/partner/*. Contact Verge Ag API support to get the flag. {"correlationId", "errorCode": "NotAuthorized", "message"}
429 Rate limit exceeded {"error": {"code": "RATE_LIMIT_EXCEEDED", …}, "limit", "windowSeconds", "retryAfterSeconds"}

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers (visible in the request log on the demo pages) - watch Remaining and back off before you hit the limit; on 429, honor Retry-After.

Embedding contract

Two ways to control the embedded Launch Pad view: