Reference

ElevCert API

Push Elevation Certificate data in from your own field-collection software. Mint a bearer token in Settings › API tokens, then POST partial or completed drafts to /api/v1/certificates.

Authentication

Every request must carry a bearer token in the Authorization header. Tokens are minted per user (the firm’s account) and authenticate as that user. Two firms on the same ElevCerttenant never see each other’s rows.

Authorization: Bearer <TOKEN>
  • Missing header → 401 unauthorized.
  • Unknown / revoked token → 401 unauthorized (no existence leak).
  • More than 60 requests / minute for one token → 429 rate_limited with Retry-After seconds.

Idempotency

Every write accepts an optional Idempotency-Key header. Replays with the same key target the same draft row, so a retry on a flaky connection never duplicates the certificate. Omit the header and the server assigns a fresh id.

Idempotency-Key: job-2026-001

Endpoints

  • POST /api/v1/certificates— create or update a draft by Idempotency-Key. Returns 201 on first write, 200 on update.
  • GET /api/v1/certificates?limit=10&cursor=<id>— list the caller’s drafts, newest first. limit is clamped to [1, 50].
  • GET /api/v1/certificates/{id}— fetch one row the token user owns. 404 on wrong owner.
  • PATCH /api/v1/certificates/{id}— update an existing draft. Same shape as POST.
  • DELETE /api/v1/certificates/{id}— remove a draft. 204 on success, 404 on wrong owner.

Request shape

Every write carries a single state blob. If step is done, the server runs the master Certificateschema (which enforces cross-section refinements such as “lowest floor elevation required for AE/VE/AO/AH zones”). Otherwise it accepts the partial Wizard state(sections may be absent until filled).

The signatureMethod enum is now ["upload"]— a typed name is not a valid professional seal in most jurisdictions. ThesignatureValue must be a data:image/png or data:image/jpeg URL. A sign attempt also requires a certifier seal image to be on file for the user (uploaded in /profile) — without it the sign route returns 400 with { "error": "seal_required", "sealUploadUrl": "/profile#certifier-seal" }.

{
  "step": "A",
  "state": {
    "sectionA": {
      "result": {
        "panel": "0308",
        "suffix": "F",
        "effectiveDate": "2024-09-12",
        "zone": "AE",
        "bfe": 9.0,
        "community": "City of Cedar Bay",
        "county": "Cedar County",
        "communityId": "485012",
        "datum": "NAVD88",
        "sourceUrl": "https://hazards.fema.gov",
        "lookupLatencyMs": 120,
        "warnings": [],
        "stale": false
      },
      "address": { "street": "1428 Baycrest Dr", "city": "Cedar Bay", "state": "TX", "zip": "77566" },
      "acks": { "datumAcknowledged": true, "staleAcknowledged": true }
    }
  },
  "clientReferenceId": "job-2026-001"
}

Validation failures return 400 with { "errors": { "sectionA.address.zip": "Invalid" } }. Cross-section gates (for example Section G required when the building is in a coastal A-zone) fold into the envelope’s certificate key.

Response shape

Each row returns exactly the fields the integrator needs to round-trip a job ID and pick out the draft’s library status.

{
  "id": "ckxyz…",
  "step": "A",
  "state": { /* WizardState or Certificate */ },
  "updatedAt": "2026-08-06T15:00:00.000Z",
  "formVersion": "FF-206-FY-22-152",
  "paidAt": null,
  "hasSignature": false,
  "clientReferenceId": "job-2026-001"
}
  • formVersion is stamped at draft creation from the published FEMA FF-206-FY-22 form version. null until the daily form-version cron runs.
  • paidAt is the verification timestamp of the most recent paid Order against this draft.
  • hasSignaturetells the integrator whether a signing step has already applied against the row — useful when resuming after a pause.

Curl examples

Step A: create a new draft. The server returns 201with the row’s assigned id; capture it or replay the same Idempotency-Key.

curl -X POST https://plumbseal.polsia.app/api/v1/certificates \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: job-2026-001" \
  -d '{
  "step": "A",
  "state": {
    "sectionA": {
      "result": {
        "panel": "0308",
        "suffix": "F",
        "effectiveDate": "2024-09-12",
        "zone": "AE",
        "bfe": 9.0,
        "community": "City of Cedar Bay",
        "county": "Cedar County",
        "communityId": "485012",
        "datum": "NAVD88",
        "sourceUrl": "https://hazards.fema.gov",
        "lookupLatencyMs": 120,
        "warnings": [],
        "stale": false
      },
      "address": { "street": "1428 Baycrest Dr", "city": "Cedar Bay", "state": "TX", "zip": "77566" },
      "acks": { "datumAcknowledged": true, "staleAcknowledged": true }
    }
  },
  "clientReferenceId": "job-2026-001"
}'

Step “done”: complete the certificate. The master schema enforces every cross-section gate, so a 400 here means a real validation gap, not a transport issue.

curl -X PATCH https://plumbseal.polsia.app/api/v1/certificates/JOB_ID \
  -H "Authorization: Bearer <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
  "step": "done",
  "state": {
    "sectionA": { /* … */ },
    "sectionB": { "firmMapAction": "none", "coastalAVZone": false, "floodZoneBaseSource": "NFHL" },
    "sectionC": {
      "diagram": "1A",
      "buildingUse": "residential",
      "squareFootage": 2400,
      "highestNextHigherFloorElevation": 12.5,
      "lowestFloorElevation": 10.0,
      "baseFloodElevation": 9.0
    },
    "sectionD": { "rows": [] },
    "sectionH": {
      "licenseNumber": "LS-0042118",
      "licenseJurisdiction": "TX",
      "certifierName": "Jane Doe",
      "certifierTitle": "LicensedLandSurveyor",
      "signatureMethod": "upload",
      "signatureValue": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
    }
  }
}'

Operations notes

  • This API writes drafts only; signing and PDF export stay on the cookie-authed web flow (a firm’s pushed drafts are still reviewable and signable in /library).
  • Tokens never expire on their own. Revoking a token invalidates it immediately, and a follow-up request returns 401.