Partner API reference · v1
This is the contract for a partner system that sends documents and boundaries into CE Canvas projects. If you are a practitioner setting up Scopomap, you want the Scopomap page instead.
Machine-readable contract: partner-v1.json (OpenAPI 3.1). The server validates requests against the same definitions this file documents; a test in our build fails if the two disagree.
1. The model
An organisation admin installs your integration in CE Canvas and creates a credential. The credential carries a set of scopes and a project grant (all of the organisation’s projects, or a chosen list). Every request you make runs as that credential inside our database: row-level security decides what it can see and change, exactly as it does for a person. Revoking the credential cuts access on the next request.
Nothing you send changes a project’s area or its census figures without a practitioner acting inside CE Canvas.
2. Base URL and authentication
| Environment | Base URL |
|---|---|
| Production (Australia) | https://app.cecanvas.com/api/v1 |
| Staging (test tenant) | https://staging.cecanvas.com/api/v1 |
Send the credential on every request:
Authorization: Bearer cec_live_…Credentials are shown once at creation and cannot be retrieved. If one leaks, the organisation revokes it and creates another.
Scopes. A credential holds any of projects:read, resources:read, resources:write, spatial:write. Each route needs one; a missing scope is 403 insufficient_scope naming the scope.
Project grant. The credential sees exactly the projects it was granted. An ungranted project does not resolve — it is 404 not_found, not 403, so a credential cannot probe for projects it was not given.
3. Conventions
Ids
Every id is a type prefix plus a UUID: org_…, prj_…, res_…, sf_…. Treat them as opaque. An id of the wrong kind on a route is a 404.
Correlation
Every response carries X-Request-Id. Send your own (8–128 characters of A-Z a-z 0-9 _ . : -) to have it echoed back; otherwise one is generated. Quote it when you ask us about a request.
Errors
One envelope, always:
{
"type": "duplicate_external_id",
"title": "This external id already exists for the credential",
"status": 409,
"detail": "Something with this external id was already sent. Reuse it rather than creating a duplicate.",
"request_id": "0b3c…",
"existing_id": "res_5b1e…"
}type is stable and meant for code to branch on. Type-specific extras (existing_id, required_scope, issues, limit) sit beside the standard fields. Internal errors carry no detail.
| type | status | when |
|---|---|---|
unauthorized | 401 | Missing, unknown, revoked or disabled credential |
insufficient_scope | 403 | Credential lacks the scope the route needs |
feature_disabled | 403 | Boundaries sent to an organisation without Project Area enabled |
not_found | 404 | Project, resource or boundary not granted or nonexistent |
validation_failed | 422 | Body failed the schema (issues lists the first 20) |
payload_too_large | 413 | Body over 2 MB |
limit_exceeded | 429 | A per-credential ceiling (see §7) |
idempotency_key_required | 400 | Create without an Idempotency-Key |
idempotency_conflict | 409 | Same key, different body |
idempotency_in_progress | 409 | Original request still running; Retry-After: 1 |
duplicate_external_id | 409 | source.external_id already delivered under a different key |
invalid_state | 409 | e.g. upload-complete before anything was uploaded |
upload_verification_failed | 422 | Bytes did not match the declaration |
internal_error | 500 | Ours. Retry with the same key. |
Idempotency
Every create (POST) requires an Idempotency-Key header (8–128 characters), unique per logical request — for example scopomap:report:9c21:v3.
- Same key, same body → the original response, with
Idempotent-Replayed: true. For a resource still awaiting its bytes,uploadis a fresh signed target. - Same key, different body →
409 idempotency_conflict. - Same key while the first attempt is running →
409 idempotency_in_progress; retry after a second. - A key whose attempt failed is released, so a corrected retry under the same key works.
- Keys expire after 24 hours.
Separately from keys, source.external_id is the thing’s identity on your side. A second create with an external id you already delivered is 409 duplicate_external_id with the existing id, however many keys you rotate.
Pagination
List routes return { "data": [...], "next_cursor": "…" | null }. Pass next_cursor as ?cursor= for the next page. Page size is 50.
4. Projects
GET /projects
The projects this credential may write to. capabilities tells you what is possible right now for each: resources_write reflects the credential’s scope; spatial_write also requires the organisation to have Project Area enabled.
GET /projects/{project_id}
Adds has_project_area — whether the project already has a resolved area. Useful for what you say in your UI; it does not change what you may send.
curl -s https://staging.cecanvas.com/api/v1/projects \
-H "Authorization: Bearer $CEC_CREDENTIAL"5. Documents (resources)
Three steps. Bytes never pass through the CE Canvas application server on the way in; on completion we read the stored object once to verify it.
5.1 POST /projects/{project_id}/resources — declare
{
"title": "Snapshot report — Foreshore renewal",
"filename": "foreshore-snapshot.pdf",
"content_type": "application/pdf",
"content_length": 1842033,
"sha256": "9f2c…",
"category": "report",
"include_in_ai": true,
"source": {
"external_id": "snapshot_12345",
"generated_at": "2026-09-01T02:10:00Z",
"url": "https://scopomap.example/reports/12345"
}
}Accepted content_type: application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document, text/plain — the types we can extract text from. Maximum 50 MB. sha256 is the lower-case hex digest of the exact bytes you will upload.
Response 201 is the resource status (§5.4) plus:
"upload": {
"method": "PUT",
"url": "https://…/storage/v1/object/upload/sign/…",
"headers": { "content-type": "application/pdf", "x-upsert": "false" },
"expires_at": "2026-09-01T04:15:00Z"
}5.2 Upload
PUT the bytes to upload.url with exactly the headers given. The URL is single-purpose and expires in two hours; if it lapses, replay the create (same key, same body) for a fresh one.
curl -s -X PUT "$UPLOAD_URL" \
-H "content-type: application/pdf" -H "x-upsert: false" \
--data-binary @foreshore-snapshot.pdf5.3 POST /resources/{resource_id}/upload-complete — verify and start
No body. CE Canvas looks at the stored object and checks that the byte count equals content_length, the stored content type equals content_type, and the sha256 of the bytes equals sha256.
Any mismatch deletes the object, marks the resource failed with an error class (size_mismatch, content_type_mismatch, checksum_mismatch) and returns 422 upload_verification_failed. Create a new resource to try again — a failed resource releases its external_id, so the same id can be declared afresh. A declaration that is never completed expires after 24 hours (upload_expired) and releases its id the same way.
If nothing has been uploaded yet: 409 invalid_state. Calling it again after success is a no-op that returns the current status.
5.4 GET /resources/{resource_id} — status
{
"id": "res_…", "project_id": "prj_…",
"status": "processing",
"stages": { "upload": "completed", "text_extraction": "completed", "indexing": "processing" },
"available_in_project": true,
"available_in_search": false,
"available_to_eva": false,
"error": null
}status is one of awaiting_upload, verifying, processing, completed, failed. Stages are pending, processing, completed, failed or skipped (when include_in_ai was false).
available_to_eva is true only when indexing has actually finished. Poll it; there is no webhook in v1.
6. Boundaries and measures (spatial features)
6.1 What happens to a boundary you send
It lands as a reference layer: drawn on the project’s map, dashed, attributed to you, listed under Received boundaries. It contributes to nothing — not the project extent, not the demographics, not EVA’s context — until a practitioner chooses Adopt and picks a role. Your proposed_role is offered as the default. If the project already has an area, adopting adds the layer under the existing combining rules; nothing is overwritten.
Sending the same source.external_id again replaces the reference layer (a re-export of the same map). If the earlier one was already adopted, it is left alone and a new reference layer appears beside it. Sending an external id you already used in a different project is a 409 duplicate_external_id.
6.2 What happens to measures
Measures are stored with the boundary, typed, and shown in the project’s community-context panel in their own section headed with your name and the reference year, marked as not yet adopted until the boundary is. They are never combined with, counted in, or compared against ABS or Statistics Canada figures. Once — and only once — a practitioner adopts the boundary, EVA receives them as a separate, labelled block and is told to attribute them to you.
Prose is not a measure. A figure needs a numeric value, a unit, a reference_year and a source; a denominator where it is a share. Text explaining a figure goes in caveats, or in a document.
6.3 POST /projects/{project_id}/spatial-features
{
"geometry": { "type": "MultiPolygon", "coordinates": [ … ] },
"proposed_role": "engagement_area",
"name": "Foreshore study area",
"source": {
"external_id": "map_67890",
"url": "https://scopomap.example/maps/67890",
"generated_at": "2026-09-01T02:09:00Z"
},
"measures": [
{
"code": "age_65_plus", "label": "Residents aged 65 and over",
"value": 412, "denominator": 2000, "unit": "persons",
"reference_year": 2021, "source": "ABS Census 2021",
"provider_version": "2026.3", "attribution_method": "area_weighted",
"caveats": ["Apportioned from SA1s; treat as an estimate."]
}
]
}Geometry is GeoJSON Polygon or MultiPolygon, WGS84, at most 20 000 vertices. Projected coordinates (MGA, UTM) are rejected. proposed_role is project_area (default), engagement_area or other. Up to 200 measures per request; the set replaces your previous measures for that boundary.
Response 201:
{
"id": "sf_…", "project_id": "prj_…", "name": "Foreshore study area",
"proposed_role": "engagement_area",
"state": "reference_layer", "adopted_role": null,
"area_m2": 184230.5, "bbox": [151.27, -33.80, 151.29, -33.79],
"source": { "external_id": "map_67890", "url": "…", "generated_at": "…" },
"measure_count": 1
}Requires spatial:write and the organisation to have Project Area enabled (403 feature_disabled otherwise; check capabilities.spatial_write on the project first).
6.4 GET /spatial-features/{feature_id}
state becomes adopted and adopted_role is set once a practitioner has adopted it.
7. Ceilings
Hard limits per request or per credential. These are validation, not rate limiting; they bound what a leaked credential can cost before it is revoked.
| Ceiling | Value |
|---|---|
| Request body | 2 MB |
| File size | 50 MB |
| Geometry | 20 000 vertices |
| Measures per request | 200 |
| Resources awaiting upload, per credential | 20 (declarations expire after 24 h) |
| Resources still processing, per credential | 25 |
Exceeding a per-credential ceiling is 429 limit_exceeded with the limit in the body. Request-rate limiting is not applied in v1.
8. Security notes
- The credential is a random 32-byte secret; we store only its SHA-256. It is shown once.
- Requests run as a dedicated machine identity per credential inside the database. Row-level security is the boundary.
- Revocation is immediate. Disabling the integration for the organisation also disables every credential under it.
- Every request is logged with the credential, route, project and outcome. Logs never contain secrets, URLs, checksums or payloads.
- Uploads go directly to private storage under a prefix only that credential can write to. We never fetch URLs you supply.
- Send aggregates and boundaries only — never household or personal data.
9. What is not in v1
Outbound webhooks (poll status instead), request-rate limiting, credential rotation in the UI (revoke and create), a separate sandbox tenant (staging is the test tenant), and results/BI endpoints. The reverse flow — CE Canvas sending a project area to a partner and receiving context back — is planned and will sit on the same conventions.
10. Getting set up
Staging is the test tenant. Contact support@cecanvas.com and we will set up an organisation for you; its admin issues your credential from Settings → Integrations. A complete happy path is: list projects → declare a PDF → PUT it → upload-complete → poll status → send a boundary with measures → read it back → replay a key and confirm a changed body is refused.