Browse the docs

Reference

API reference

Every endpoint on this page is live, and each one states the token scope it requires, what it returns, what can go wrong, and whether it can be undone. Base URL /api/v1. Updrone is the software underneath — your business stays the professional of record for its own flights, pilots, airspace, and accuracy sign-off.

Authentication & conventions#

Read this once and every endpoint below behaves the way you expect.

  • Authenticate every request with an API token: Authorization: Bearer sk_…. Issue tokens under Settings → API tokens (the secret is shown once, at creation).
  • A token acts as its role through the same permission gate as the portal UI. It can be issued against one of your own custom roles, narrowed per kind and per verb ({job:["read"], deliverable:["create"]}), restricted to specific projects, bound to a member, and time-boxed — all under Settings → API tokens. It can never exceed the role it was issued against.
  • A project-scoped token answers 404 for a project outside its scope, exactly as it would for one in another tenant — existence is not disclosed to a caller that cannot reach it.
  • /api/portal/* is SESSION-ONLY and will refuse a Bearer token with 401 no_session. Every machine-callable endpoint is listed on this page.
  • Requests are rate-limited per token on separate buckets: 120 reads/min and 20 writes/min. A 429 carries code: "rate_limited" and a Retry-After header — back off and retry. X-RateLimit-Bucket on every response names which budget you are spending.
  • SEPARATE from the request limit, a CONSEQUENCE budget caps what can leave the system per hour, per token, per kind: 30 emails, 20 SMS, 10 charges, 10 publishes, 20 compute runs. One request can carry many consequences, and the ones that reach your customers are the ones that matter — a looping caller inside the write limit could still email forty people a minute. Exhausting one kind is a 429 naming that kind, so you can keep doing the parts that are still allowed.
  • An operation that is irreversible or binding requires a TWO-PHASE CONFIRM. The first call answers 409 confirmation_required with a confirmToken and a willDo description of what it is about to do; re-send the identical request with x-updrone-confirm: <token> to perform it. The token binds the tenant, the token, the operation AND the exact payload, so it cannot be replayed against a different body, and it expires in 10 minutes. Set X-Agent-Confirmed-By to record who authorised it — self-reported, and stored as a claim, not as proof.
  • Send x-updrone-dry-run: 1 to an endpoint that supports it to compute the effects and apply nothing; the response is {ok, dryRun: true, applied: false, willDo, effects}. An endpoint that cannot rehearse REFUSES the header rather than ignoring it — a rehearsal that silently applies is the one outcome the header must never produce.
  • A state change goes through the entity's own machine and a v1 route never invents a transition. An illegal trigger is 409 conflict carrying legalTriggers (or legalTransitions) — what IS legal from here — so you re-plan from one response instead of probing. A trigger whose destination the entity is ALREADY in answers 200 with replayed: true, because a retry after a lost response is not a failure.
  • List endpoints paginate by cursor: pass ?cursor= from the previous page's nextCursor (limit ≤ 100, default 50).
  • A write that AMENDS an existing record requires If-Match carrying the etag from your read. A missing one is 428 (not a silent overwrite); a stale one is 409 with the current record attached, so you can re-plan without a second round trip. Send * to overwrite unconditionally.
  • Every response carries ok: true | false. Errors are { ok: false, code, error, retryable } — branch on code, never on the English error sentence. A denial also carries required: { action, kind }, naming the exact scope you are missing.
  • A DEAD credential is 401 with WWW-Authenticate (invalid_token, revoked, expired) — stop and get a new token. An in-scope-but-not-permitted call is 403 (read_only, out_of_scope, role_denied) — ask for a broader token, or skip that call. Other statuses: 404 not found · 409 conflict · 429 rate limit.
  • Writes are exactly-once when you send an idempotency key — opId in the body or an Idempotency-Key header. A replay returns the ORIGINAL record with 200 and replayed: true; without a key a retry after a lost response creates a SECOND record, so always send one.
  • Every write is recorded in the tenant's tamper-evident audit trail against your token. Set X-Agent-Run-Id, X-Agent-Task and X-Agent-Model and they are recorded with it, so the tenant can reconstruct what an automated caller did and why.
  • Measured quantities always include their ± tolerance band — the band is part of the number; integrations must carry it through, never strip it. Every CONTINUOUS figure carries one: a measurement under band, each volume and the site rollup under bands keyed by field. A band states its own unit and gives relativeFraction (0.1 = ±10 %, never a percentage). Tallies (regionCount) carry none — a count is not a measured quantity, and we never fabricate a ± for one.
  • The machine-readable contract is GET /api/v1/openapi.json (OpenAPI 3.1, generated from this page's source). GET /api/v1/capabilities returns what YOUR token may call, which is narrower.

Errors#

Every failure carries a stable code and a retryable flag. Branch on the code — the English error sentence is for humans and may be reworded.

CodeHTTPRetryable
no_credential401no
invalid_token401no
revoked401no
expired401no
read_only403no
out_of_scope403no
role_denied403no
invalid_json400no
invalid_body400no
field_required400no
field_invalid400no
field_invalid_enum400no
field_too_long400no
unknown_field400no
not_found404no
principal_unbound409no
conflict409yes — back off and retry
confirmation_required409no
rate_limited429yes — back off and retry
unavailable503yes — back off and retry

Endpoints#

88 calls, grouped by what they act on. /api/portal/* is session-only and refuses a bearer token — everything a machine can call is on this page.

Paste an API token and every sample below becomes copyable as a call you can run. It is never rendered, never sent to us, and never stored — the samples show a mask, and only your clipboard gets the real value.

Project records#

Punch items, RFIs, incidents and daily logs — the field-integration write path, with retraction that can itself be undone.

GET /api/v1/projects/:id/recordsRequires read · deliverable

Project records of one kind (punch, RFI, incident, daily log), cursor-paginated.

read-onlylistProjectsRecords

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

kindquerypunch | rfi | incident | daily-logrequired

Which record kind to list.

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

includeVoidedqueryboolean

Include RETRACTED records, each carrying `voidedAt`. Without this a voided record is indistinguishable from one that never existed.

Returns

okbooleanrecordsarray of object{id, projectId, voidedAt, jobId, number, title, subject, question, status, priority, assignees, dueDate, occurredAt, occurredOn, ballInCourt, involvedParty, description, kind, severity, createdBy, createdAt, updatedAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_requiredfield_invalid_enumunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/records?kind=punch" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/projects/:id/recordsRequires create · deliverable

Create a punch, RFI, or incident record — the field-integration write. Send an idempotency key: a replay returns the ORIGINAL record with 200 and `replayed: true`.

reversibleretry-safe: send an idempotency keymay trigger: automationcreateProjectsRecords

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyone of 3 shapes, by `kind`required

Discriminated on `kind`: punch needs `title`; rfi needs `subject` + `question`; incident needs `title`.

Returns

okbooleankindpunch | rfi | incidentrecordobject{id, projectId, voidedAt, jobId, number, title, subject, question, status, priority, assignees, dueDate, occurredAt, occurredOn, ballInCourt, involvedParty, description, kind, severity, createdBy, createdAt, updatedAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/records" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"punch","title":"Handrail loose at stair 2","opId":"my-run-001"}'
GET /api/v1/projects/:id/records/:recordIdRequires read · deliverable

One project record of ANY kind, including a retracted one (its `voidedAt` says so). The generic read over every `RecordStore` kind.

read-onlygetProjectsRecords

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

recordIdpathidrequired

The record id.

from GET /projects/:id/records → records[].id

kindquerypunch | rfi | incident | daily-log | submittal | drawing | minutesrequired

Which record kind `recordId` is.

Returns

okbooleankindpunch | rfi | incident | daily-log | submittal | drawing | minutesrecordobject{id, projectId, voidedAt, jobId, number, title, subject, question, status, priority, assignees, dueDate, occurredAt, occurredOn, ballInCourt, involvedParty, description, kind, severity, createdBy, createdAt, updatedAt}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_requiredunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/records/RECORD_ID?kind=punch" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
PATCH /api/v1/projects/:id/records/:recordIdRequires update · deliverable

Amend a record's descriptive fields, for ANY kind. `status` is deliberately NOT patchable — each kind has its own state machine (submittal transitions are server-enforced), and those stay named verbs.

reversibleretry-safe: naturally idempotentupdateProjectsRecords

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

recordIdpathidrequired

The record id.

from GET /projects/:id/records → records[].id

If-Matchheaderstringrequired

The `etag` from your GET of this record. REQUIRED — a missing precondition is 428, not a blind overwrite, because 'I didn't check' and 'I checked' must not be the same request. Send `*` to overwrite unconditionally.

from GET /projects/:id/records/:recordId → etag

bodybodyobject{kind, title, subject, question, description, priority, assignees, dueDate, ballInCourt, involvedParty}required

`kind` plus at least one field to amend. Unknown fields are REFUSED, not dropped.

Returns

okbooleankindpunch | rfi | incident | daily-log | submittal | drawing | minutesrecordobject{id, projectId, voidedAt, jobId, number, title, subject, question, status, priority, assignees, dueDate, occurredAt, occurredOn, ballInCourt, involvedParty, description, kind, severity, createdBy, createdAt, updatedAt}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumfield_too_longunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X PATCH "https://app.updrone.com/api/v1/projects/PROJECT_ID/records/RECORD_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"punch","priority":"high"}'
POST /api/v1/projects/:id/records/:recordId/voidRequires update · deliverable

Retract a record. REVERSIBLE — `…/unvoid` takes the retraction back. Idempotent: voiding a voided record does not re-stamp it.

reversibleretry-safe: naturally idempotentcreateProjectsRecordsVoid

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

recordIdpathidrequired

The record id.

from GET /projects/:id/records → records[].id

bodybodyobject{kind}required

Which record kind `recordId` is.

Returns

okbooleankindstringrecordobject{id, projectId, voidedAt, jobId, number, title, subject, question, status, priority, assignees, dueDate, occurredAt, occurredOn, ballInCourt, involvedParty, description, kind, severity, createdBy, createdAt, updatedAt}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsonfield_invalid_enumunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/records/RECORD_ID/void" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"punch"}'
POST /api/v1/projects/:id/records/:recordId/unvoidRequires update · deliverable

Un-retract a record — the verb that makes voiding reversible rather than terminal.

reversibleretry-safe: naturally idempotentcreateProjectsRecordsUnvoid

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

recordIdpathidrequired

The record id.

from GET /projects/:id/records?includeVoided=1 → records[].id

bodybodyobject{kind}required

Which record kind `recordId` is.

Returns

okbooleankindstringrecordobject{id, projectId, voidedAt, jobId, number, title, subject, question, status, priority, assignees, dueDate, occurredAt, occurredOn, ballInCourt, involvedParty, description, kind, severity, createdBy, createdAt, updatedAt}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsonfield_invalid_enumunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/records/RECORD_ID/unvoid" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"punch"}'

Deliverables & measurements#

Pull a processed artifact down, and read the measured quantities a scene produced — each figure with the ± band that belongs to it.

GET /api/v1/projects/:id/artifacts/:artifactIdRequires read · deliverable

A short-lived, TOKEN-FETCHABLE download link for one artifact. The `href` values in a project's deliverable manifests are portal-session URLs a token cannot use — fetching one returns the login page, not the file.

read-onlygetProjectsArtifacts

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

artifactIdpathidrequired

The artifact id.

from GET /projects/:id → project.scenes[].artifacts[].id

Returns

okbooleanartifactobject{id, format, href, expiresInSec}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/artifacts/ARTIFACT_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:id/quantitiesRequires read · deliverable

Measured quantities from the site snapshot — every measurement with its unit AND ± tolerance band; per-region volumes and the site rollup, each with a `bands` map carrying the ± for every continuous figure (cut/fill/net/footprint/tonnage).

read-onlylistProjectsQuantities

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Returns

okbooleanframeobject{epsg, originGeo, units}measurementsarray of object{id, kind, unit, value, band, points}volumesarray of object{regionId, cutM3, fillM3, netM3, footprintM2, tonnage, bands}rollupobject{totalCutM3, totalFillM3, netM3, regionCount, bands}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/quantities" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Leads & customers#

Demand before it is a customer, and the customer once it is. Capture a lead, move it through its status machine, convert it into a real record — and read the whole relationship back in one call.

GET /api/v1/leadsRequires read · lead

The tenant's lead inbox, cursor-paginated. `score` is DATA COMPLETENESS (0–85), never a rating of the person.

read-onlylistLeads

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

statusqueryNew | Contacted | Qualified | Converted | Lost

Only leads in this status.

qquerystring, ≤ 200 chars

Free-text over name, email, phone and message — across the whole inbox, not just this page.

ownerIdqueryid

Only leads owned by this member.

from GET /members → members[].id

Returns

okbooleanleadsarray of object{id, name, email, phone, message, status, score, source, ownerId, matchedCustomerId, convertedCustomerId, propertyRef, capturedAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enumunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/leads" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/leads/:idRequires read · lead

One lead, with the inbound messages logged against it and the statuses it may move to next — so a status change is planned rather than discovered by 409.

read-onlygetLeads

Parameters

idpathidrequired

The lead id.

from GET /leads → leads[].id

Returns

okbooleanleadobject{id, name, email, phone, message, status, score, source, ownerId, matchedCustomerId, convertedCustomerId, propertyRef, capturedAt}timelinearray of object{customerId, kind, direction, body, source}nextStatesarray of New | Contacted | Qualified | Converted | Lostetagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/leads/LEAD_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/leadsRequires create · lead

Capture a lead. DEDUPED against an open lead with the same email (a second post returns the FIRST lead with `deduped: true`), and matched against an existing customer — the match is a flag, never a merge.

reversibleretry-safe: send an idempotency keymay trigger: automationcreateLeads

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{name, email, phone, message, source, ownerId, opId}required

`name` is required; everything else is what the form actually collected. The server owns the id, the tenant and the timestamp.

Returns

okbooleanleadobject{id, name, email, phone, message, status, score, source, ownerId, matchedCustomerId, convertedCustomerId, propertyRef, capturedAt}dedupedbooleanreplayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_too_longunknown_field

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/leads" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"The Bensons","email":"kbenson@example.com","source":"website","opId":"lead-run-001"}'
PATCH /api/v1/leads/:idRequires update · lead

Amend a lead, including a GUARDED status move. Legal transitions only (`LEAD_TRANSITIONS`); an illegal one is 409 listing what IS legal from here. `Converted` is refused — conversion is its own verb, because it mints a customer.

reversibleretry-safe: naturally idempotentupdateLeads

Parameters

idpathidrequired

The lead id.

from GET /leads → leads[].id

If-Matchheaderstringrequired

The `etag` from your GET of this record. REQUIRED — a missing precondition is 428, not a blind overwrite. Send `*` to overwrite unconditionally.

from GET /leads/:id → etag

bodybodyobject{status, ownerId, name, email, phone, message}required

At least one field. `status` moves through the machine; the rest are plain amendments. Unknown fields are REFUSED, not dropped.

Returns

okbooleanleadobject{id, name, email, phone, message, status, score, source, ownerId, matchedCustomerId, convertedCustomerId, propertyRef, capturedAt}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_invalidfield_invalid_enumfield_too_longunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X PATCH "https://app.updrone.com/api/v1/leads/LEAD_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"Qualified"}'
POST /api/v1/leads/:id/qualifyRequires update · lead

Read the facts a lead STATED in their own message and return them as a typed persona (buyer / seller / investor). Meters AI credits, so it is a write for rate-limiting purposes. It stores nothing on the lead — what to do with the reading stays a decision you make through PATCH.

reversibleretry-safe: naturally idempotentcreateLeadsQualify

Parameters

idpathidrequired

The lead id.

from GET /leads → leads[].id

Returns

okbooleanleadIdidqualificationobject{personaKind, profile, rationale, source, sanitized}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/leads/LEAD_ID/qualify" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/leads/:id/convertRequires create · customer

Convert a lead into a real CRM customer plus an opening deal, and log the inbound message on the customer's timeline.

reversibleretry-safe: naturally idempotentmay trigger: automationmay trigger: emailcreateLeadsConvert

Parameters

idpathidrequired

The lead id.

from GET /leads → leads[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleancustomerIdidleadobject{id, name, email, phone, message, status, score, source, ownerId, matchedCustomerId, convertedCustomerId, propertyRef, capturedAt}createdboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/leads/LEAD_ID/convert" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/customersRequires read · customer

The tenant's customers, cursor-paginated. `?q=` searches the whole tenant in SQL, so 'no match' means no match rather than 'none on this page'.

read-onlylistCustomers

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

qquerystring, ≤ 200 chars

Free-text over name, email and company, across the tenant.

stagequeryLead | Qualified | Proposal | Active | Lost

Only customers in this stage.

Returns

okbooleancustomersarray of object{id, name, email, phone, company, stage, source, ownerId, ownerName, createdAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/customers" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/customers/:idRequires read · customer

One customer record with its deals, projects, captures, proposals, invoices and money rollup — the whole relationship in one read.

read-onlygetCustomers

Parameters

idpathidrequired

The customer id.

from GET /customers → customers[].id

Returns

okbooleancustomerobject{id, name, email, phone, company, stage, source, ownerId, ownerName, createdAt}dealsarray of object{id, customerId, customerName, stage, valueCents, currency, ownerId, proposalId, projectId, jobId, lostReason, expectedCloseAt, createdAt}timelinearray of object{id, type, at, actor, summary}projectsarray of object{id, name, status}proposalsarray of object{id, status}invoicesarray of object{id, status}moneyobject{lifetimePaidCents, outstandingCents}errorsarray of stringetagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/customers/CUSTOMER_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/customersRequires create · customer

Create a customer. Send an idempotency key: a replay returns the ORIGINAL record with `replayed: true`.

reversibleretry-safe: send an idempotency keymay trigger: automationcreateCustomers

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{name, email, phone, company, stage, source, opId}required

`name` is required. `stage` defaults to `Lead`.

Returns

okbooleancustomerobject{id, name, email, phone, company, stage, source, ownerId, ownerName, createdAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumfield_too_longunknown_field

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/customers" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Rodriguez Roofing","email":"ops@rodriguez.example","opId":"cust-run-001"}'
PATCH /api/v1/customers/:idRequires update · customer

Amend a customer's fields. Every change is written to the record's own activity trail, so the customer page shows what an integration did.

reversibleretry-safe: naturally idempotentupdateCustomers

Parameters

idpathidrequired

The customer id.

from GET /customers → customers[].id

If-Matchheaderstringrequired

The `etag` from your GET of this record. REQUIRED — a missing precondition is 428, not a blind overwrite. Send `*` to overwrite unconditionally.

from GET /customers/:id → etag

bodybodyobject{name, email, phone, company, stage, source, ownerId}required

At least one field to amend. Unknown fields are REFUSED, not dropped.

Returns

okbooleancustomerobject{id, name, email, phone, company, stage, source, ownerId, ownerName, createdAt}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_invalidfield_invalid_enumfield_too_longunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X PATCH "https://app.updrone.com/api/v1/customers/CUSTOMER_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stage":"Active"}'

Deals & pipeline#

The pipeline as data rather than as a board: cents and currency codes, the stage machine's legal triggers stated up front, and moves that refuse illegally rather than silently doing nothing.

GET /api/v1/dealsRequires read · deal

The deal pipeline as DATA — cents and currency, not the board's pre-formatted labels. Cursor-paginated, with the tenant's stage counts and open value alongside.

read-onlylistDeals

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

stagequerylead | proposal_sent | won | scheduled | captured | delivered | invoiced | lost

Only deals in this stage.

customerIdqueryid

Only this customer's deals.

from GET /customers → customers[].id

openqueryboolean

`true` keeps only OPEN deals (everything but `invoiced` and `lost`).

Returns

okbooleandealsarray of object{id, customerId, customerName, stage, valueCents, currency, ownerId, proposalId, projectId, jobId, lostReason, expectedCloseAt, createdAt}countsobject{lead, proposal_sent, won, scheduled, captured, delivered, invoiced, lost}openValueCentsintegercurrencystringnextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enumunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/deals" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/deals/:idRequires read · deal

One deal with its activity timeline and the triggers that are LEGAL from its current stage — so you can plan a move without discovering the machine by 409.

read-onlygetDeals

Parameters

idpathidrequired

The deal id.

from GET /deals → deals[].id

Returns

okbooleandealobject{id, customerId, customerName, stage, valueCents, currency, ownerId, proposalId, projectId, jobId, lostReason, expectedCloseAt, createdAt}timelinearray of object{id, type, at, actor, summary}legalTriggersarray of send_proposal | win | schedule | capture | deliver | invoice | loseetagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/deals/DEAL_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/dealsRequires create · deal

Open an UNPRICED deal against a customer. The server checks the customer in tenant scope and mints the initial zero value in the tenant's persisted currency; setting contract value is a separate manual-money command, not part of this API intent.

reversibleretry-safe: send an idempotency keymay trigger: automationcreateDeals

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{customerId, contactId, expectedCloseAt, opId}required

`customerId` is required and must exist in your tenant. Amount, currency, totals, tax, fees, balances, status and provider fields are not intent and are rejected as unknown fields.

Returns

okbooleandealobject{id, customerId, customerName, stage, valueCents, currency, ownerId, proposalId, projectId, jobId, lostReason, expectedCloseAt, createdAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/deals" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"CUSTOMER_ID","opId":"deal-run-001"}'
POST /api/v1/deals/:id/advanceRequires update · deal

Move a deal through the pipeline machine. An illegal trigger is 409 listing the legal ones — a v1 route never invents a transition. A trigger whose destination the deal is ALREADY in answers 200 with `replayed: true`, so a lost response is safe to retry.

reversibleretry-safe: naturally idempotentmay trigger: automationcreateDealsAdvance

Parameters

idpathidrequired

The deal id.

from GET /deals → deals[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{trigger}required

The machine trigger. `lose` is deliberately absent — it takes a reason, so it has its own verb.

Returns

okbooleandealobject{id, customerId, customerName, stage, valueCents, currency, ownerId, proposalId, projectId, jobId, lostReason, expectedCloseAt, createdAt}legalTriggersarray of stringreplayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalid_enumunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/deals/DEAL_ID/advance" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"trigger":"win"}'
POST /api/v1/deals/:id/loseRequires update · deal

Close a deal as lost, with a reason. REVERSIBLE — the pipeline can reopen it. Idempotent for an already-lost deal; a stage with no `lose` edge is refused out loud rather than reported as a close that never happened.

reversibleretry-safe: naturally idempotentmay trigger: automationcreateDealsLose

Parameters

idpathidrequired

The deal id.

from GET /deals → deals[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{reason}required

Why it was lost. Free text, stored on the deal and shown on the board.

Returns

okbooleandealobject{id, customerId, customerName, stage, valueCents, currency, ownerId, proposalId, projectId, jobId, lostReason, expectedCloseAt, createdAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_too_longunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/deals/DEAL_ID/lose" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Went with a cheaper bid"}'

Projects#

The unit of delivery work: the list, one project with its scenes and deliverable manifests, and its computed schedule.

GET /api/v1/projectsRequires read · job

List projects (lean summaries: customer, site, status).

read-onlylistProjects

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

qquerystring, ≤ 200 chars

Free-text search across the TENANT, not just the current page — so 'no match' means no match.

statusquerystring

Exact project status.

verticalquerystring

Exact project vertical.

customerIdqueryid

Only this customer's projects.

updatedSincequerydate-time

Only projects changed since this instant — the cheap change poll.

Returns

okbooleanprojectsarray of object{id, name, customer, site, status}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:idRequires read · job

One project with its scenes (artifacts + export formats) and deliverable manifests (download hrefs).

read-onlygetProjects

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Returns

okbooleanprojectobject{id}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:id/takeoffsRequires read · deliverable

Every takeoff on a project — the quantity documents, each with its rollup by cost code and the provenance mix on every total. A total spanning captured and drawn quantities reports `mixed: true`; a `null` band means no honest ± exists and must not be rendered as zero.

read-onlylistProjectsTakeoffs

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

Returns

okbooleantakeoffsarray of object{id, name, revision, status}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/takeoffs" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:id/takeoffs/:takeoffIdRequires read · deliverable

One takeoff in full: every line with its quantity in canonical SI, the ± it earned (or `null` where none honestly exists), its provenance, and the scope / assumptions / disclosures that make it defensible.

read-onlygetProjectsTakeoffs

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

takeoffIdpathidrequired

The takeoff id.

from GET /projects/{id}/takeoffs → takeoffs[].id

Returns

okbooleantakeoffobject{id}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/takeoffs/TAKEOFF_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:id/takeoffs/:takeoffId/deltaRequires read · deliverable

What changed between two revisions of a takeoff — added / removed / changed lines, and for each change WHICH SIDE OF THE ± BAND it sits on. A change inside the band is two readings of the same thing; outside it, something moved.

read-onlylistProjectsTakeoffsDelta

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

takeoffIdpathidrequired

The `to` revision — the newer document.

from GET /projects/{id}/takeoffs → takeoffs[].id

fromqueryidrequired

The `from` revision to compare against.

from GET /projects/{id}/takeoffs → takeoffs[].id

Returns

okbooleandeltaobject{outsideBandCount}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_requiredunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/takeoffs/TAKEOFF_ID/delta?from=VALUE" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:id/scheduleRequires read · job

The CPM schedule plan: tasks + computed dates, float, critical path.

read-onlylistProjectsSchedule

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

startquerydate

Override the project start. A date, NOT an instant — an ISO date-time is rejected. Echoed back in the SAME form it was sent.

Returns

okbooleanstartdatetaskCountintegerplanobject{projectStart, projectFinish, durationWorkdays, criticalPath, schedule}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_invalidconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/schedule" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/projectsRequires create · job

Open a project. Mints the Job, its Site and the linked CRM deal through the real lifecycle core, so a project created here is indistinguishable from one created in the portal.

reversibleretry-safe: send an idempotency keymay trigger: automationcreateProjects

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{customerId, customerName, name, siteAddress, ownerName, accessNotes, geo, vertical, opId}required

`customerId` + `customerName` are required; the server reloads the canonical customer name and persisted tenant currency. Contract value and other authoritative money fields are rejected. Omit `vertical` to take the tenant default.

Returns

okbooleanprojectobject{id, name, status}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidunknown_fieldunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"CUSTOMER_ID","customerName":"Rodriguez Roofing","name":"Warehouse roof scan","siteAddress":"1200 Industrial Way, Austin TX","opId":"proj-run-001"}'
PATCH /api/v1/projects/:idRequires update · job

Amend a project's non-financial mutable fields: name, owner, site address and geo, and the trade vertical. Contract value is deliberately not patchable here.

reversibleretry-safe: naturally idempotentupdateProjects

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

If-Matchheaderstringrequired

The `etag` from your GET of this record. REQUIRED — a missing precondition is 428, not a blind overwrite. Send `*` to overwrite unconditionally.

from GET /projects/:id → etag

bodybodyobject{name, ownerId, siteAddress, geo, accessNotes, vertical}required

At least one field. Authoritative money and status fields are rejected. Changing `vertical` on a project that already has design or delivery content is REFUSED with 409 rather than silently discarding it.

Returns

okbooleanprojectobject{id}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X PATCH "https://app.updrone.com/api/v1/projects/PROJECT_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Warehouse roof scan — phase 2"}'
POST /api/v1/projects/:id/lifecycleRequires update · job

Move a project through `JOB_MACHINE`. An illegal trigger is 409 listing the legal ones; a trigger whose destination the project is ALREADY in answers 200 with `replayed: true`.

reversibleretry-safe: naturally idempotentmay trigger: automationcreateProjectsLifecycle

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{trigger}required

`cancel` is deliberately absent — cancelling is irreversible through this API and is not an agent verb. `deliverable_shipped` is refused while the project has no packaged deliverable, so the stage can never contradict the count.

Returns

okbooleanprojectobject{id, status}legalTriggersarray of stringreplayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalid_enumunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/lifecycle" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"trigger":"schedule_assigned"}'
POST /api/v1/projects/:id/capture/presignRequires create · capture

Begin a server-owned capture session and receive short-lived, content-type-pinned PUT URLs. The SERVER mints every object key inside your tenant's namespace and records them as the session's expected set — you cannot choose, influence or add a key.

reversibleretry-safe: send an idempotency keycreateProjectsCapturePresign

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{source, device, capturedBy, frames, depth, confidence, control, hasLidar, mode, engine, quality, idempotencyKey, opId}required

`frames` is a MANIFEST — one entry per file you intend to upload, carrying its content type and declared size. The declared sizes are an advisory fast-fail; the real bytes are measured at finalize.

Returns

okbooleanenabledbooleansessionIdidexpiresSecondsintegeruploadsarray of object{key, uploadUrl, contentType}depthUploadsarray of object{key, uploadUrl, contentType}confidenceUploadsarray of object{key, uploadUrl, contentType}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/capture/presign" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source":"drone","device":"Mavic 3E","frames":[{"contentType":"image/jpeg","size":4200000}],"opId":"cap-run-001"}'
POST /api/v1/projects/:id/capture/:sessionId/finalizeRequires update · capture

Complete a presigned capture and start the reconstruction. SERVER-AUTHORITATIVE and BODY-FREE: the server HEAD-verifies its own expected keys, meters the real bytes, and kicks the pipeline. Idempotent — a duplicate call re-meters nothing.

reversibleretry-safe: naturally idempotentmay trigger: computemay trigger: automationcreateProjectsCaptureFinalize

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

sessionIdpathidrequired

The capture session id.

from POST /projects/:id/capture/presign → sessionId

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleancaptureIdidframesintegeralreadyFinalizedbooleanpollAtstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_requiredconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/capture/SESSION_ID/finalize" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/projects/:id/capture/:sessionId/abortRequires update · capture

Report that an upload failed or was cancelled, so the session stops claiming to be in flight. Only touches a session still `uploading`; a racing finalize always wins.

reversibleretry-safe: naturally idempotentmay trigger: chargecreateProjectsCaptureAbort

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

sessionIdpathidrequired

The capture session id.

from POST /projects/:id/capture/presign → sessionId

reasonqueryfailed | cancelled

`failed` (the network gave up) is never charged. `cancelled` (you chose to stop) bills the flat, non-refundable UPLOAD FEE — the same rule a person sees in the cancel dialog. Defaults to `failed`.

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleancaptureIdidstatusstringremovedbooleanalreadyAdvancedbooleanuploadFeeChargedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/capture/SESSION_ID/abort" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/projects/:id/processRequires update · capture

Re-run the reconstruction for a capture. METERED COMPUTE — budgeted, and gated: an infeasible engine/mode is refused before anything is queued, and a capture already in flight is never started twice.

reversibleretry-safe: naturally idempotentmay trigger: computecreateProjectsProcess

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{captureId, mode, engine, quality, structureSpanM, sceneTag}

Everything is optional: no body reprocesses the newest capture on the project's own persisted mode and engine. An explicit `mode`/`engine` also PERSISTS on the project, so the record, the charge and the backend agree.

Returns

okbooleancaptureIdidenginestringmodelocal | cloudmodeCoercedFromlocal | cloudalreadyProcessingbooleanpollAtstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_invalidfield_invalid_enumunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/process" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"engine":"native","mode":"cloud"}'
GET /api/v1/projects/:id/complianceRequires read · deliverable

What this job owes: the STATUTORY obligations the site's jurisdiction imposes (with citations), and — when a solar design exists — whether its plan set is ready to submit.

read-onlylistProjectsCompliance

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Returns

okbooleanjurisdictionobject{stateKnown, stateName, country, windowDays, windowUnit, windowCitation}obligationsarray of object{id, title, detail, citation}blockersarray of object{id, title, detail}planSetobject{ready, blocking}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/compliance" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/projects/:id/permitRequires read · deliverable

The permit metadata in effect for this project — the tenant defaults with any per-project override merged over them.

read-onlylistProjectsPermit

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Returns

okbooleanpermitobject{}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/permit" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/projects/:id/permitRequires update · deliverable

Set this project's permit override. Tenant-level defaults stay untouched — this is the per-job exception, not a settings edit.

reversibleretry-safe: naturally idempotentcreateProjectsPermit

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

bodybodyobject{}required

Sparse permit metadata — unknown keys are dropped by the store's validator.

Returns

okbooleanpermitobject{}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_body

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/projects/PROJECT_ID/permit" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ahjName":"City of Austin"}'
GET /api/v1/projects/:id/export-formatsRequires read · deliverable

Which formats this project's scenes can actually be exported to. A codec that is not installed is ABSENT rather than listed-and-then-refused, so a caller plans an export it can complete.

read-onlylistProjectsExport-formats

Parameters

idpathidrequired

The project id.

from GET /projects → projects[].id

Returns

okbooleanscenesarray of object{sceneId, formats}availablearray of string

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/projects/PROJECT_ID/export-formats" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Client pages & exports#

Getting work out of Updrone. Client pages are assembled rather than stored — every project and customer always has one — and a tenant export is queued, swept and polled, because archiving a whole tenant is not something to hold a socket open for.

GET /api/v1/client-pagesRequires read · deliverable

The tenant's client pages — one per project and customer, always. ASSEMBLED on read, not stored, so there is nothing to create; the capability token in each link is minted server-side and never accepted from a caller.

read-onlylistClient-pages

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

kindqueryproject | customer

Only project pages, or only customer pages.

Returns

okbooleanclientPagesarray of object{id, kind, title, href, customerId, projectId, hasReadyScene, updatedAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/client-pages" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/exportsRequires create · settings

Request a full tenant data export. Queued, not performed: `cron/exports` packages it and you poll `GET /exports/:id`. METERED COMPUTE — this reads and archives the whole tenant.

reversibleretry-safe: send an idempotency keymay trigger: computecreateExports

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{opId}

No parameters — an export is the whole tenant. Send a key: a retried request must not queue a second archive of everything.

Returns

okbooleanexportobject{id, status, byteSize, createdAt, readyAt, expiresAt, error}pollAtstringreplayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedinvalid_jsoninvalid_bodyunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/exports" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"opId":"exp-run-001"}'
GET /api/v1/exportsRequires read · settings

Your export requests, newest first, with their status and — once ready — the archive's size and expiry.

read-onlylistExports

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

Returns

okbooleanexportsarray of object{id, status, byteSize, createdAt, readyAt, expiresAt, error}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/exports" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/exports/:idRequires read · settings

One export — THE POLL TARGET. `queued → tables → assets → packaging → ready`, with `error` naming the phase that failed.

read-onlygetExports

Parameters

idpathidrequired

The export id.

from POST /exports → export.id

Returns

okbooleanexportobject{id, status, byteSize, createdAt, readyAt, expiresAt, error}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/exports/EXPORT_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Saved reports#

What a tenant has chosen to keep watching. A report is config-backed — it stores the figures and the queries, not their values, and the board is re-derived every time it is opened — so these calls describe what a report IS, and the numbers stay where they are computed. Reports are private to the member who saved them, which is why an unbound token is refused rather than shown an empty shelf.

GET /api/v1/reportsRequires read · report

Saved reports you can see, newest first. A report is PRIVATE TO THE MEMBER WHO SAVED IT — a Manager token sees the tenant's, a Staff token sees the bound member's own, and an unbound token is refused rather than shown an empty list.

read-onlylistReports

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

Returns

okbooleanreportsarray of object{id, name, origin, cards, queries, createdBy, createdAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedprincipal_unboundfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/reports" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/reports/:idRequires read · report

One saved report's DEFINITION — the figures it watches and the queries behind them, not their current values. The board is re-derived when it is opened; nothing is computed by this read.

read-onlygetReports

Parameters

idpathidrequired

The report id.

from GET /reports → reports[].id

Returns

okbooleanreportobject{id, name, origin, cards, queries, createdBy, createdAt}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedprincipal_unboundnot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/reports/REPORT_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Invoices#

Asking to be paid. A draft asks nobody for anything; issuing one puts a live payment page in front of a customer, so it is confirmed. Refunds and payment itself are not here — returning money and paying are human acts.

GET /api/v1/invoicesRequires read · payment

The tenant's invoices, cursor-paginated. Amounts in cents; status straight from `INVOICE_MACHINE`.

read-onlylistInvoices

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

statusquerydraft | sent | paid | overdue | void | refunded

Only invoices in this status.

customerIdqueryid

Only this customer's invoices.

from GET /customers → customers[].id

Returns

okbooleaninvoicesarray of object{id, number, status, customerId, customerName, type, terms, totalCents, amountDueCents, currency, issuedAt, dueAt, paidAt, projectId}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/invoices" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/invoices/:idRequires read · payment

One invoice with its line items and the legal triggers from here. Payment details are summarised, never the raw processor payload.

read-onlygetInvoices

Parameters

idpathidrequired

The invoice id.

from GET /invoices → invoices[].id

Returns

okbooleaninvoiceobject{id, number, status, customerId, customerName, type, terms, totalCents, amountDueCents, currency, issuedAt, dueAt, paidAt, projectId}lineItemsarray of object{id, description, quantity, unitPriceCents, totalCents}legalTriggersarray of issue | pay_full | pass_due | void | refund

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/invoices/INVOICE_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/invoicesRequires create · payment

Create a server-priced DRAFT invoice from one active tenant pricing package and an optimistic pricebook version. The API accepts selectors and quantity-free package intent, never line prices or totals.

reversibleretry-safe: send an idempotency keycreateInvoices

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{customerId, invoiceType, terms, packageId, sourceVersion, opId}required

`packageId` must name an active package in this tenant; `sourceVersion` is the opaque current pricebook version. Send `opId` or `Idempotency-Key`. Currency, lines, unit prices, totals, tax, fees, balances, status, provider ids and URLs are rejected as unknown fields.

Returns

okbooleaninvoiceobject{id, number, status, customerId, customerName, type, terms, totalCents, amountDueCents, currency, issuedAt, dueAt, paidAt, projectId}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/invoices" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"CUSTOMER_ID","packageId":"PACKAGE_ID","sourceVersion":"PRICEBOOK_VERSION","opId":"inv-run-001"}'
POST /api/v1/invoices/:id/issueRequires update · payment

Issue a draft invoice: `draft → sent` in `INVOICE_MACHINE`, and the hosted payment page goes LIVE. Two-phase confirm — this is the moment you ask a customer for money.

irreversibleretry-safe: naturally idempotentmay trigger: automationcreateInvoicesIssue

Parameters

idpathidrequired

The invoice id.

from GET /invoices → invoices[].id

X-Agent-Confirmed-Byheaderstring, ≤ 500 chars

Who authorised it; recorded as a claim, not proof.

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleaninvoiceobject{id, number, status, customerId, customerName, type, terms, totalCents, amountDueCents, currency, issuedAt, dueAt, paidAt, projectId}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflictconfirmation_required

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

# This call is irreversible. The FIRST request returns 409 with a confirmToken and a
# willDo description; re-send it with  x-updrone-confirm: <token>  to perform it.

curl -X POST "https://app.updrone.com/api/v1/invoices/INVOICE_ID/issue" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/invoices/:id/voidRequires update · payment

Void an invoice. TERMINAL in `INVOICE_MACHINE`. A `paid` invoice cannot be voided — returning money is a refund, which this API does not do.

irreversibleretry-safe: naturally idempotentmay trigger: automationcreateInvoicesVoid

Parameters

idpathidrequired

The invoice id.

from GET /invoices → invoices[].id

X-Agent-Confirmed-Byheaderstring, ≤ 500 chars

Who authorised it; recorded as a claim.

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleaninvoiceobject{id, number, status, customerId, customerName, type, terms, totalCents, amountDueCents, currency, issuedAt, dueAt, paidAt, projectId}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflictconfirmation_required

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

# This call is irreversible. The FIRST request returns 409 with a confirmToken and a
# willDo description; re-send it with  x-updrone-confirm: <token>  to perform it.

curl -X POST "https://app.updrone.com/api/v1/invoices/INVOICE_ID/void" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/invoices/:id/pdfRequires read · payment

A short-lived, TOKEN-FETCHABLE link to the invoice PDF. The hosted page's own URL needs a session a token does not have.

read-onlylistInvoicesPdf

Parameters

idpathidrequired

The invoice id.

from GET /invoices → invoices[].id

Returns

okbooleanhrefstringexpiresInSecinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/invoices/INVOICE_ID/pdf" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Proposals#

Winning the work. Drafts are inert and freely editable; sending one is irreversible, confirmed and budgeted, because it puts a document in front of a named human who can act on it.

GET /api/v1/proposalsRequires read · proposal

The tenant's proposals, cursor-paginated. Operator-shaped: status, recipient, totals and the machine's legal next moves.

read-onlylistProposals

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

statusquerydraft | sent | viewed | accepted | declined | expired | voided

Only proposals in this status.

customerIdqueryid

Only this customer's proposals.

from GET /customers → customers[].id

Returns

okbooleanproposalsarray of object{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/proposals" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/proposals/:idRequires read · proposal

One proposal with its line items and the legal triggers from here. NEVER carries the signing OTP or the signed PDF bytes.

read-onlygetProposals

Parameters

idpathidrequired

The proposal id.

from GET /proposals → proposals[].id

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}lineItemsarray of object{id, label, quantity, unitPriceCents, totalCents}legalTriggersarray of send | view | accept | decline | expire | voidetagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/proposals/PROPOSAL_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/proposalsRequires create · proposal

Create a DRAFT proposal priced by the server from the tenant's persisted catalog/rate settings and currency. A draft is inert; sending remains a separate confirmed act.

reversibleretry-safe: send an idempotency keycreateProposals

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{customerId, title, projectId, opId}required

The customer must exist in your tenant. When `projectId` is present it must belong to that customer and supplies the server-owned vertical/site/deal pricing context. Client line prices, currency, totals, tax, fees, balances, status and provider/URL fields are rejected.

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/proposals" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"CUSTOMER_ID","title":"Roof scan + solar","opId":"prop-run-001"}'
PATCH /api/v1/proposals/:idRequires update · proposal

Amend a DRAFT's non-financial presentation intent: title, recipient, or embedded 3D scene. Priced lines and authoritative economics are not patchable through this command.

reversibleretry-safe: naturally idempotentupdateProposals

Parameters

idpathidrequired

The proposal id.

from GET /proposals → proposals[].id

If-Matchheaderstringrequired

The `etag` from your GET of this record. REQUIRED — a missing precondition is 428, not a blind overwrite. Send `*` to overwrite unconditionally.

from GET /proposals/:id → etag

bodybodyobject{title, recipientEmail, recipientName, sceneId}required

At least one field. Money, tax, fee, balance, status, provider and URL fields are rejected. A proposal that is no longer a draft is 409 with its current status.

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}etagstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_too_longunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X PATCH "https://app.updrone.com/api/v1/proposals/PROPOSAL_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Revised scope"}'
POST /api/v1/proposals/:id/sendRequires update · proposal

Send the proposal to its recipient. IRREVERSIBLE — there is no un-send. Two-phase confirm; the `willDo` names the recipient (redacted). Freezes the figures AS SENT, so a later design change cannot retroactively alter what the customer saw.

irreversibleretry-safe: naturally idempotentmay trigger: emailmay trigger: automationcreateProposalsSend

Parameters

idpathidrequired

The proposal id.

from GET /proposals → proposals[].id

X-Agent-Confirmed-Byheaderstring, ≤ 500 chars

Who authorised the send. SELF-REPORTED and recorded as a claim, not proof — the server-issued confirm token is the control.

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}sentTostring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflictconfirmation_requiredunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

# This call is irreversible. The FIRST request returns 409 with a confirmToken and a
# willDo description; re-send it with  x-updrone-confirm: <token>  to perform it.

curl -X POST "https://app.updrone.com/api/v1/proposals/PROPOSAL_ID/send" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/proposals/:id/voidRequires update · proposal

Void a proposal. TERMINAL in `PROPOSAL_MACHINE` — there is no way back, and a voided proposal's hosted page stops working. Two-phase confirm.

irreversibleretry-safe: naturally idempotentmay trigger: automationcreateProposalsVoid

Parameters

idpathidrequired

The proposal id.

from GET /proposals → proposals[].id

X-Agent-Confirmed-Byheaderstring, ≤ 500 chars

Who authorised it; recorded as a claim.

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflictconfirmation_required

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

# This call is irreversible. The FIRST request returns 409 with a confirmToken and a
# willDo description; re-send it with  x-updrone-confirm: <token>  to perform it.

curl -X POST "https://app.updrone.com/api/v1/proposals/PROPOSAL_ID/void" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/proposals/:id/reviseRequires create · proposal

Fork a sent proposal into a NEW draft version. The original stays exactly as the customer saw it — revising never edits a document somebody has already been shown.

reversibleretry-safe: naturally idempotentcreateProposalsRevise

Parameters

idpathidrequired

The proposal id.

from GET /proposals → proposals[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}revisedFromid

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/proposals/PROPOSAL_ID/revise" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/proposals/:id/duplicateRequires create · proposal

Copy a proposal into a fresh draft — the template path, for a similar job on a different customer.

reversibleretry-safe: naturally idempotentcreateProposalsDuplicate

Parameters

idpathidrequired

The proposal id.

from GET /proposals → proposals[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleanproposalobject{id, title, status, customerId, customerName, projectId, totalCents, currency, recipientEmail, sceneId, createdAt, sentAt, expiresAt}duplicatedFromid

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/proposals/PROPOSAL_ID/duplicate" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Scenes & geometry#

The reconstructions themselves: what is ready, what each one produced, and the plane-detection kernel that turns a dense point cloud into a roof you can design on.

GET /api/v1/scenesRequires read · scene

The tenant's reconstructed scenes. Ready scenes carry their artifacts and export formats; pending ones carry only an id and a label, because there is nothing yet to describe.

read-onlylistScenes

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

projectIdqueryid

Only this project's scenes.

from GET /projects → projects[].id

statusqueryready | pending

`ready` (reconstructed) or `pending` (still processing). Default: both.

Returns

okbooleanscenesarray of object{id, projectId, status, label, createdAt, artifacts, exportFormats, splatUrl}pendingarray of object{id, label}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/scenes" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/scenes/:idRequires read · scene

One ready scene with its artifacts, export formats and the geometry URLs a viewer needs.

read-onlygetScenes

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

Returns

okbooleansceneobject{id, projectId, status, label, createdAt, artifacts, exportFormats, splatUrl}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/scenes/SCENE_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/scenes/:id/detect-roofRequires read · scene

Run the plane-detection kernel over a scene's dense reconstruction and report the roof it finds. PERSISTS NOTHING — it is a reading, and it gates on `read` because of that.

read-onlycreateScenesDetect-roof

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

Returns

okbooleansplatOnlybooleanroofobject{faces, source}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/scenes/SCENE_ID/detect-roof" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/scenes/:id/geometryRequires read · scene

A short-lived, TOKEN-FETCHABLE link to one of a scene's geometry artifacts: the mesh, the bare-earth DTM, the plan-set model, or the solar surface.

read-onlylistScenesGeometry

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

kindquerymesh | dtm | planset-model | solarrequired

Which artifact. A scene that never produced this one answers 404 — it is genuinely absent, not withheld.

Returns

okbooleangeometryobject{kind, format, href, expiresInSec}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_requiredfield_invalid_enumunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/scenes/SCENE_ID/geometry?kind=mesh" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/scenes/:id/measurementsRequires read · scene

The measurements taken on a scene. Every continuous figure carries its ± tolerance BAND — the band is part of the number, and an integration must carry it through rather than strip it.

read-onlylistScenesMeasurements

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

Returns

okbooleanmeasurementsarray of object{id, sceneId, kind, label, unit, value, band, points, createdAt}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/scenes/SCENE_ID/measurements" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/scenes/:id/measurementsRequires create · deliverable

Take a measurement on a scene. The kernel computes the value from your points and REJECTS a bad pick set (too few points, a non-finite coordinate) rather than storing it.

reversibleretry-safe: send an idempotency keycreateScenesMeasurements

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{kind, points, label, opId}required

`points` are in the scene's own frame — see `GET /projects/:id/quantities → frame` for its EPSG and origin. The accuracy tier is DERIVED from the scene and is never something you set.

Returns

okbooleanmeasurementobject{id, sceneId, kind, label, unit, value, band, points, createdAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/scenes/SCENE_ID/measurements" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"distance","points":[{"x":0,"y":0,"z":0},{"x":3,"y":0,"z":0}],"opId":"meas-run-001"}'
DELETE /api/v1/scenes/:id/measurementsRequires update · deliverable

Remove one measurement. Gates on `update`, not `delete`: it retracts an annotation on a scene, it does not destroy the scene.

reversibleretry-safe: naturally idempotentdeleteScenesMeasurements

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

measurementIdqueryidrequired

Which measurement to remove.

from GET /scenes/:id/measurements → measurements[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

Returns

okbooleanremovedbooleanmeasurementIdid

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundfield_required

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X DELETE "https://app.updrone.com/api/v1/scenes/SCENE_ID/measurements?measurementId=VALUE" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/scenes/:id/waypointsRequires read · scene

The scene's navigation graph — the pointers, go-tos and tour stops authored on it.

read-onlylistScenesWaypoints

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

Returns

okbooleanwaypointsarray of object{id, sceneId, kind, label, position, link, order}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/scenes/SCENE_ID/waypoints" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/scenes/:id/waypointsRequires create · deliverable

Save a waypoint — a scene-space position, a kind and a label. These are the stops a client tour plays.

reversibleretry-safe: send an idempotency keycreateScenesWaypoints

Parameters

idpathidrequired

The scene id.

from GET /scenes → scenes[].id

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{kind, label, position, link, order, opId}required

`position` is `[x, y, z]` in the scene's own frame — see `GET /projects/:id/quantities → frame` for its EPSG and origin. A waypoint is navigation metadata: it never asserts a flight is permitted and never raises the scene's accuracy tier.

Returns

okbooleanwaypointobject{id, sceneId, kind, label, position, link, order}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidunknown_field

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/scenes/SCENE_ID/waypoints" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"tour-stop","label":"North elevation","position":[0,12,30],"opId":"wp-run-001"}'

Capture & processing#

Getting pixels in and turning them into a scene: the presigned upload handshake where the server owns every key, the reconstruction kick, and the poll target that says which stage a run is on and why it stopped.

GET /api/v1/capturesRequires read · capture

Every capture session in the tenant, newest first, with its per-stage progress. This is what you poll while a reconstruction runs.

read-onlylistCaptures

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

projectIdqueryid

Only this project's captures.

from GET /projects → projects[].id

statusquerycapturing | uploading | queued | processing | ready | failed

Only captures in this status.

Returns

okbooleancapturesarray of object{id, projectId, projectName, source, device, status, frameCount, coverage, capturedBy, capturedAt, previewReady, stages, error}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalidfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/captures" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/captures/:idRequires read · capture

One capture session — THE PROCESSING-STATUS POLL TARGET. Carries the per-stage breakdown and, on a failure, the stage that failed and why.

read-onlygetCaptures

Parameters

idpathidrequired

The capture session id.

from GET /captures → captures[].id

Returns

okbooleancaptureobject{id, projectId, projectName, source, device, status, frameCount, coverage, capturedBy, capturedAt, previewReady, stages, error}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/captures/CAPTURE_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/uploads/initRequires create · capture

Issue an opaque, tenant/principal/project-bound upload intent. The server reserves quota and mints every key; callers declare only MIME and a byte ceiling, never a tenant, key, filename or URL.

reversibleretry-safe: naturally idempotentcreateUploadsInit

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{projectId, captureId, uploads}required

`captureId` is required for solar artifacts and forbidden for project attachments. `maxBytes` is an upload ceiling and quota reservation, not a claimed landed size. Completion HEAD-verifies the provider's actual key, MIME and bytes from the server-side intent row.

Returns

okbooleanintentstringresourceobject{kind, id}expiresAtstringuploadsarray of object{uploadId, index, purpose, contentType, maxBytes, uploadUrl}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/uploads/init" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","uploads":[{"purpose":"project_attachment","contentType":"image/jpeg","maxBytes":12000000}]}'
POST /api/v1/uploads/completeRequires update · capture

Atomically consume one opaque upload intent. The server loads its own exact keys, HEAD-verifies MIME and real bytes, meters quota, and returns opaque artifact references; callers cannot submit a key or URL.

reversibleretry-safe: naturally idempotentcreateUploadsComplete

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{intent}required

Only the opaque token from `/uploads/init`. Tenant, principal, project, keys, MIME and byte ceilings are reloaded from the durable server row.

Returns

okbooleanresourceobject{kind, id}completedAtstringuploadsarray of object{artifactId, index, purpose, contentType, sizeBytes}totalBytesinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidunknown_fieldconflictunavailable

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/uploads/complete" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"intent":"uit_OPAQUE_TOKEN"}'

Templates & geocoding#

The pieces a project is assembled from: the document templates every invoice, proposal and client page renders through, and the address lookup that turns a street line into the coordinate every downstream kernel needs.

GET /api/v1/geocodeRequires read · job

Address ⇄ coordinate, both directions. `?q=` for forward suggestions, `?lat=&lng=` for the address at a point. UNITED STATES ONLY today.

read-onlylistGeocode

Parameters

qquerystring, ≤ 300 chars

Forward: the partial address to resolve. Mutually exclusive with lat/lng.

latquerynumber

Reverse: latitude. Send with `lng`.

lngquerynumber

Reverse: longitude. Send with `lat`.

Returns

okbooleanresultsarray of object{label, primary, secondary, lat, lng}degradedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_requiredfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/geocode" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/templatesRequires read · templates

The tenant's DOCUMENT templates — invoice, proposal, client page and report — plus the assignment map that decides which one a given customer or project gets.

read-onlylistTemplates

Parameters

kindqueryinvoice | proposal | clientPage | report

Only templates of this kind.

Returns

okbooleantemplatesarray of object{id, kind, name, updatedAt}assignmentsarray of object{kind, scope, scopeId, templateId}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalid_enum

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/templates" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/templates/:idRequires read · templates

One template, with its document body — the blocks a rendered invoice, proposal, client page or report is built from.

read-onlygetTemplates

Parameters

idpathidrequired

The template id.

from GET /templates → templates[].id

Returns

okbooleantemplateobject{id, kind, name, document, updatedAt}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_found

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/templates/TEMPLATE_ID" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
PUT /api/v1/templates/assignmentsRequires update · templates

Point a template at a scope: the tenant default, one customer, or one project. PUT because it is a whole-value replace — sending the same assignment twice leaves exactly one pointer.

reversibleretry-safe: naturally idempotentsetTemplatesAssignments

Parameters

bodybodyobject{kind, scope, scopeId, templateId}required

Omit `templateId` to CLEAR an override, so the scope falls back to the next one up (project → customer → tenant → system).

Returns

okbooleanassignmentobject{kind, scope, scopeId, templateId}clearedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitednot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_invalid_enumunknown_field

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X PUT "https://app.updrone.com/api/v1/templates/assignments" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"proposal","scope":"tenant","scopeId":"","templateId":"TEMPLATE_ID"}'

Discovery & sync#

Find things, and learn what moved since you last looked — the calls that replace re-reading every collection and diffing it.

GET /api/v1/changesRequires read · job

What changed since an instant — the poll that replaces re-reading every collection and diffing it. Pass the returned `cursor` back as `since`. `hasDeletions: false` is stated explicitly: this feed is derived from timestamps and cannot see a removal, so do not infer completeness.

read-onlylistChanges

Parameters

sincequerydate-timerequired

Return changes strictly after this instant. Use the previous response's `cursor`.

from GET /changes → cursor

kindsquerystring, ≤ 200 chars

Comma-separated families to watch: project, lead, customer, deal, capture. Absent ⇒ everything your token may read, which is the safer default. `coverage[]` in the response says what was actually looked at.

Returns

okbooleansincedate-timecursordate-timechangesarray of object{kind, verb, id, at, resource}hasDeletionsbooleancoveragearray of string

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_requiredfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/changes?since=2026-07-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/searchRequires read · job

Cross-entity search over everything your token can read. Each hit is gated by the SAME per-surface grant check the portal uses, so a token scoped away from customers gets projects and no customers from one query.

read-onlylistSearch

Parameters

qquerystring, ≤ 200 charsrequired

The search text.

typesquerystring

Comma-separated hit types to keep.

limitqueryinteger, 1–50, default 20

Max hits (≤ 50). The applied value is echoed.

Returns

okbooleanquerystringlimitintegerresultsarray of object{type, id, title, subtitle, href}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_requiredfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/search?q=VALUE" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/membersRequires read · staff

The tenant's active roster — the ids `assignees` accepts. Writing an unknown assignee is a 400, so this is how you discover a valid one. Emails are not returned.

read-onlylistMembers

Returns

okbooleanmembersarray of object{id, name, role, status}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limited

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/members" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Token & capabilities#

Introspection. Ask what your own credential is and what it may do, before you act rather than after a 403.

GET /api/v1/meRequires read · settings

Introspect YOUR token: its scope, expiry, member binding, the grant grid it can actually perform, and how much rate-limit budget is left. Cheap and side-effect-free — call it at the start of a run instead of probing destructively.

read-onlylistMe

Returns

okbooleantenantobject{id}tokenobject{id, name, role, readOnly, boundToMember, expiresAt}grantsarray of object{kind, actions}rateLimitobject{limit, remaining, resetAt, bucket}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limited

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/me" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
GET /api/v1/capabilitiesRequires read · settings

The agent's bootstrap call: which endpoints YOUR token may call (and why not, when it may not), plus the tenant's configured verticals and modules — so you read the endpoint list through the right lens rather than as a flat menu.

read-onlylistCapabilities

Returns

okbooleantenantobject{id, verticals, defaultVertical, modules}tokenobject{id, role, boundToMember}endpointsarray of object{operationId, method, path, allowed, deniedBecause, reason}contractstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limited

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/capabilities" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Bookings#

Requests customers have made, and the two acts that answer them. A `requested` row HOLDS its slot against every other customer until you do — so this is work, not history. Confirming re-checks the window and refuses a clash with alternatives rather than double-booking; declining carries a reason the customer reads. There is deliberately no endpoint that confirms a booking on a rule or a schedule: confirmation is a human act or the tenant's own recorded instant-book policy, and nothing else.

GET /api/v1/booking_requestsRequires read · booking

Booking requests, newest first. A `requested` row is HOLDING its slot against every other customer until somebody answers it, so this list is work rather than history — `expiresAt` says how long the hold has left.

read-onlylistBooking_requests

Parameters

statusqueryrequested | confirmed | declined | withdrawn | expired

Narrow to one status. Omitted ⇒ all.

Returns

okbooleanrequestsarray of object{id, status, startAt, endAt, contactName, contactEmail, contactPhone, origin, expiresAt, note}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/booking_requests" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/booking_requests/confirmRequires update · booking

Confirm a booking request: materializes the calendar visit, attaches or creates the customer, advances the deal, queues the confirmation and reminders. Re-checks the window first — a request that has been sitting in an inbox may have been overtaken, and a clash answers 409 `slot_conflict` with what is in the way AND three open times. Nothing here resolves a clash for you: only an explicit `override.reason` proceeds, and it is recorded.

reversibleretry-safe: naturally idempotentcreateBooking_requestsConfirm

Parameters

bodybodyobject{id, title, override}required

`id` is the request to confirm. `override.reason` is the ONLY way past a clash, and it is recorded — an override with no stated cause is indistinguishable from the check never having run.

Returns

okbooleanrequestobject{id, status, calendarEventId}identitymatched | ambiguous | nonecustomerIdstringleadIdstring

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_requiredfield_invalidnot_foundconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/booking_requests/confirm" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"bkr_…"}'
POST /api/v1/booking_requests/declineRequires update · booking

Decline a booking request WITH a reason, and get back the nearest open times. The reason reaches the customer — 'we can't do that time' with nothing after it is how a business loses somebody who was ready to buy over a clash a different day would have solved. Declining frees the hold immediately.

reversibleretry-safe: naturally idempotentcreateBooking_requestsDecline

Parameters

bodybodyobject{id, reason}required

`reason` reaches the customer. Omitting it is allowed and produces a bare refusal — which is the thing this endpoint exists to make avoidable.

Returns

okbooleanrequestobject{id, status}alternativesarray of object{startIso, endIso}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_requirednot_foundconflict

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/booking_requests/decline" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"bkr_…","reason":"We are on another site that morning."}'

The day#

What a given day holds and how prepared it is: each visit's site, the age of its imagery, whether a roof is marked, whether a draft flight plan exists, and whether a human has reviewed the drafts. `readiness` describes the PREPARATION and never the flight — there is no go/no-go here, and nothing in this product authorizes one.

GET /api/v1/day_sheetRequires read · booking

A day's visits with their day-of preparation: site, imagery age, roof mark, draft flight plan, checklist. Scoped to the token's bound member by default; `all=1` widens to the whole team for an Owner/Manager token. Deliberately carries NO go/no-go, readiness score, or 'ready to fly' — `readiness` describes the PREPARATION (whether the steps ran and whether a human reviewed the drafts), never the flight. Nothing in this product authorizes one.

read-onlylistDay_sheet

Parameters

datequerystring

YYYY-MM-DD in the TENANT's timezone. Omitted ⇒ the tenant's today, which is not necessarily the caller's.

allquerystring

`1` widens from the bound member's day to the whole team. Ignored for a token that is not Owner/Manager.

Returns

okbooleandateKeystringtimeZonestringscopemine | teamvisitsarray of object{eventId, jobId, title, startAt, endAt, allDay, readiness, reason, site, imagery, roof, missionId, checklistInstanceId}

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/day_sheet" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"

Tasks#

Operational tasks for the member a token is bound to — read them, and file new ones. Both verbs need that binding: a task owned by a credential rather than by a person is one no human would ever be shown.

GET /api/v1/tasksRequires read · booking

Operational tasks for the token's tenant. This read is scoped to a MEMBER (owned + assigned), so the token must be bound to one — an unbound token gets 409 `principal_unbound` rather than a misleading empty list.

read-onlylistTasks

Parameters

cursorquerycursor

Resume after the previous page's nextCursor.

limitqueryinteger, 1–100, default 50

Page size. Values outside the range are CLAMPED, and the response tells you so.

Returns

okbooleantenantIdstringtasksarray of object{id, title, details, list, projectId, due, time, done, starred, ownerId, assigneeId, createdAt}nextCursorstringcursorResolvedbooleanlimitinteger

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedprincipal_unboundfield_invalid

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X GET "https://app.updrone.com/api/v1/tasks" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN"
POST /api/v1/tasksRequires create · booking

File an operational task. It is OWNED by the member the token is bound to — so an unbound token is refused with 409 `principal_unbound` rather than filing work against a synthetic principal, where the task would exist, cost nothing, and be visible to nobody.

reversibleretry-safe: send an idempotency keycreateTasks

Parameters

Idempotency-Keyheaderstring, ≤ 64 chars

Equivalent to `opId` in the body; send one on every retry-able write.

X-Agent-Run-Idheaderstring, ≤ 500 chars

Recorded in the tenant's audit trail alongside the write.

X-Agent-Taskheaderstring, ≤ 500 chars

What the run was asked to do; recorded with the write.

bodybodyobject{title, details, list, due, time, starred, projectId, captureId, opId}required

`title` is required. `due` is a civil date (`YYYY-MM-DD`) and `time` a 24-hour wall clock (`HH:MM`) — an unscheduled task carries neither, and a `time` without a `due` does not schedule anything. `list` files it under a personal bucket, created on first use. `projectId` must name a project this token can reach; an unreachable one is 404, exactly as another tenant's would be. The server owns the id, the owner and the timestamp.

Returns

okbooleantaskobject{id, title, details, list, projectId, due, time, done, starred, ownerId, assigneeId, createdAt}replayedboolean

Solid = always present. Dashed = present when applicable.

Errors

no_credentialinvalid_tokenrevokedexpiredread_onlyout_of_scoperole_deniedrate_limitedprincipal_unboundnot_foundinvalid_jsoninvalid_bodyfield_requiredfield_invalidfield_too_longunknown_field

Highlighted codes are retryable. Branch on code, never on the English message.

Example request

curl -X POST "https://app.updrone.com/api/v1/tasks" \
  -H "Authorization: Bearer sk_YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Re-fly the north roof plane","due":"2026-08-03","opId":"task-run-001"}'

/api/v1 is versioned and frozen additively: existing endpoints, parameters and response fields do not change or disappear, and a build-time check enforces it. New capability arrives as new endpoints and new optional fields. Deliverable and export download links returned by /projects/:id are portal-session URLs in v1.0 — use /projects/:id/artifacts/:artifactId for a token-fetchable one. Webhooks are configured under Settings → Webhooks.

Building something? Email support@updrone.com.