Partner API

Integrate Maintainr into your workflow

Launch passwordless form sessions from your repair order system, sync status with webhooks, and pull PDFs from your backend.

Base URL: https://maintainr-backend-production.up.railway.app/api/v1/integration

Authenticate with Authorization: Bearer mk_test_… or mk_live_…. Create keys and webhooks in the app under Settings → Integrations (org admins).

Team provisioning

Anyone who taps Start form or Review in your app must exist in the same Maintainr organization. Use the same work email you send as actorEmail or viewerEmail.

  1. Invite users in Maintainr (Users → Invite), or
  2. Enable auto-provision users in Settings → Integrations (optional JIT; restrict allowed email domains).

Primary flow: Start form

POST /tasks/start is the recommended entry point. It creates or reuses a task by externalTaskId, opens a fill session for the technician, and returns links.open.

curl -X POST "$BASE/tasks/start" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: start-RO-1042-form-1" \ -d '{ "externalRepairOrderId": "RO-1042", "externalTaskId": "RO-1042-form-1", "formId": 1, "actorEmail": "tech@dealership.com", "externalCustomerId": "CRM-88921", "prefill": { "_jobName": "RO-1042", "_repairOrder": "RO-1042", "_customerName": "Jane Doe" }, "metadata": { "source": "partner-app" } }'

Open links.open with the system (not an embedded webview). If Maintainr is installed, the app opens; otherwise the browser. Response includes ownerEmail and canResume — gate Continue with canResume && ownerEmail === currentTechEmail. A different actorEmail on start reassigns the task (handoff).

Prefill system keys

Use these canonical keys (and field ids from GET /forms/{id} for custom fields):

KeyPurpose
_jobNameJob / work order display name
_repairOrderRepair order number
_customerNameCustomer name

On create only, aliases such as RO, _roNumber, and Customer Name are also copied into these keys when unset.

Poll by externalTaskId

GET /tasks?externalTaskId=RO-1042-form-1 returns the current task (status, ownerEmail, canResume). Use when a webhook is late. Returns TASK_NOT_FOUND if no task exists.

Handoff: PATCH /tasks/{submissionId}/assign with the coworker’s email, or start again with their actorEmail.

Review & PDF

Review in Maintainr

POST /tasks/{submissionId}/view-session with { "viewerEmail": "manager@dealership.com" }. Open links.view for read-focused access.

Download PDF

GET /submissions/{id}/pdf — call from your server with the API key and stream the PDF to your app.

Share customer report

POST /submissions/{id}/share-link — store links.publicReport on the job in your system.

Partner UI states

Your UIMaintainr signal
Not startedNo task for externalTaskId
In progresscanResume: true (IN_PROGRESS, RETURNED, IN_RELAY)
Under reviewStatus UNDER_REVIEW or webhook submission.under_review
CompletedStatus COMPLETED or webhook submission.completed
ReturnedWebhook submission.returned
Customer quoteWebhook quote.customer_responded

Buttons by state

StateStart formReviewPDFShare
Not startedPOST /tasks/start———
In progressPOST /tasks/startview-session——
Under review—view-session——
Completed—view-sessionGET pdfshare-link

Bulletin check (Arrival)

POST /bulletins/check (scope forms:read) matches org-subscribed active bulletins for a unit — same logic as Maintainr’s in-app TSB step. Soft-fail on your side if Maintainr is down; empty matches is not an error.

curl -sS -X POST "$BASE/bulletins/check" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "make": "MasterCraft", "model": "X24", "year": 2022, "hullId": "MBC2XXXXX222", "engine": "Ilmor", "engineSerial": "ABC123456" }'

Response is the standard envelope: use data.unit and data.matches. Enums: target = unit|engine; severity = advisory|bulletin|recall; certainty = potential|confirmed. Sorted recalls first. Also: GET /bulletins lists active subscribed bulletins.

Webhooks

Configure endpoints in Settings → Integrations. Verify X-Maintainr-Signature (HMAC-SHA256 of the raw body with your webhook secret). Deliveries are retried automatically.

Events: task.created, task.reassigned, submission.under_review, submission.completed, submission.returned, quote.customer_responded.

Payload data includes submissionId, status, externalRepairOrderId, externalTaskId, assignedTo, ownerEmail, canResume, links, and completedBy on lifecycle events.

Error codes

CodeMeaning
ASSIGNEE_NOT_FOUNDEmail not in org; invite user or enable JIT
ACTOR_REQUIREDMissing actorEmail and actorId
EMAIL_DOMAIN_NOT_ALLOWEDJIT on but domain not allowed
FORM_NOT_FOUNDInvalid formId for org
TASK_NOT_FOUNDInvalid submission id
IDEMPOTENCY_CONFLICTSame idempotency key, different body
INSUFFICIENT_SCOPEAPI key missing required scope

API key scopes

ScopeAccess
forms:readList forms and users
tasks:readList/query tasks, view-session
tasks:writestart, assign, fill-session, share-link
submissions:readGet submission, events, PDF

New keys include all default scopes. OpenAPI: GET /v1/integration/openapi.json (requires API key).

Ready to connect?

Generate a test key in the app, point webhooks at your staging URL, and use the curl example above with your form IDs.