Skip to content

Uploading guides

A guide is a captured walkthrough: one screenshot and some metadata per step, optionally narrated. Like video uploads, the file never passes through us. You ask for presigned storage URLs, PUT each screenshot directly, then send the step metadata.

Uploading needs a token granted the content:write scope, which is an OAuth path today.

A guide either publishes straight away or goes to a reviewer, depending on whether the person behind the token may publish guides to everyone in the organization. Someone who can publishes. Anyone else who may contribute, such as a team admin, goes to a reviewer.

You do not choose. The server decides from the caller’s permission and tells you which happened, in the mode field on both responses. This is deliberate: a client cannot reliably know what its user is allowed to do, and any answer it hardcodes goes stale the first time an organization adds a role.

mode What it means
published The guide was created directly, without going to review. On its own that leaves a draft, for someone to review and publish in the editor. Send publish: true when you finalize and it is in the library once processing finishes.
review The guide is waiting on a reviewer, and is not visible to learners yet.

Show the difference to whoever submitted it. “Your guide is live” and “your guide is with a reviewer” are not the same message.

POST /v1/guides/upload-urls

Creates the guide and returns one presigned URL per screenshot.

{
"title": "Filing an expense",
"screenshotCount": 3,
"includeAudio": false
}
{
"id": "…",
"mode": "review",
"screenshotUrls": [
{ "sequence": 0, "uploadUrl": "https://…", "key": "…" }
],
"audio": { "uploadUrl": "https://…", "key": "…" }
}

Treat id as opaque and hand it straight back when finalizing. It is a guide id when publishing and a submission id when going to review, and that difference is not yours to interpret.

Add a sourceKey to upload-urls: any string you choose that names what the guide was generated from, up to 200 characters. Send the same one again and the upload replaces that guide instead of creating a second. Send a different one and you get a new guide. We do not interpret it, so my-repo#invoice-tour works as well as anything.

{
"title": "Filing an invoice",
"screenshotCount": 3,
"includeAudio": true,
"sourceKey": "my-repo#invoice-tour"
}

Send the same sourceKey to finalize as well. The response says what happened:

  • updatesExisting: true means an existing guide is being replaced, so you can say “updated” and not “created”.
  • id is that guide’s id, and the screenshot and audio URLs point at fresh storage names, so the version people are watching is never overwritten while you upload.
  • The guide keeps its title. Someone may have renamed it, and the title you send is used only when the guide is created.

Replacing a guide replaces all of it, including step text that someone edited in the app since.

It is unique per organization, so two uploads racing to create the same sourceKey produce one guide. The second finds the first and carries on with it.

When you cannot publish, the upload is a proposal for an administrator, and the rules differ:

  • Your first upload under a key files a proposal, and the key belongs to it.
  • Uploading again while it is still pending replaces that proposal with a fresh one under the same key. The response says replacedPending: true. The earlier proposal is only withdrawn once the new one exists, and if an administrator decides it in the meantime, their decision stands and the upload is answered as it would be after that decision. Send "stack": true instead to file another beside it, which carries no key.
  • If an administrator rejects a first proposal, the key is released, so the source can be uploaded again as something new.
  • If a guide already exists under the key, uploading over it proposes an update to it. The response says updatesExisting: true with mode: "review", and the published guide stays exactly as it is until an administrator approves the update, which then replaces it. Uploading again while your update is pending replaces your proposal, as for a first upload.

A 409 on a sourceKey means the state of that guide stands in the way, and the code tells you which kind of wait it is:

Code Meaning
SOURCE_KEY_BUSY The guide is still being processed, or another upload for it is in progress. Try again in a few minutes. An upload that was started and abandoned holds the key for up to four hours.
SOURCE_KEY_AWAITING_REVIEW A proposal for this source, or a proposed update to it, is waiting in the review queue. It has to be approved or rejected first.
SOURCE_KEY_UPDATE_NEEDS_ADMIN The guide exists and you cannot see it, so you cannot propose an update to it. Someone who can see it can upload it.

POST /v1/guides/finalize

Call this once every screenshot has been PUT successfully.

{
"id": "…",
"steps": [
{
"sequence": 0,
"actionType": "click",
"targetInfo": { "selector": "#submit" },
"pageUrl": "https://example.com/expenses",
"screenshotKey": "…",
"defaultText": "Click Submit"
}
],
"audioKey": "…"
}

Each step’s screenshotKey is the key returned for the matching sequence, and must be one this guide’s upload-urls issued: a key belonging to another guide is refused with 400. targetInfo takes flat string values only.

submitterNote is also accepted here. It is attached to the review request, so it reaches a reviewer only when mode comes back as review, and is ignored otherwise because a published guide has nobody to read it. It belongs on this call rather than on upload-urls because the review request does not exist until now.

{ "id": "…", "title": "Filing an expense", "mode": "review" }

Add "publish": true to the finalize body and a guide that was created directly publishes as soon as it is finalized, instead of waiting as a draft for someone to publish it in the editor. It is for a client that already has the exact text of every step, such as a tool that generated the narration itself, so:

  • Audio is not transcribed. The transcript would replace the defaultText you sent, and it would arrive after the guide had already been published.
  • Publishing an update asks nobody who already learned the guide to retrain, unless you say otherwise with retrainingIntent (below), so it is safe to send again when the walkthrough changes.

publish is ignored when the guide goes to review, because a guide waiting on a reviewer is not yours to publish. Leave it out and you get the draft, which is what a tool that opens the editor afterwards wants.

If your audio was synthesized from the steps’ own text, add "scriptedNarration": true to the finalize body. The audio is then not transcribed, and the defaultText you sent stands. Without it, audio is transcribed and the transcript replaces each step’s text, which is right for a recording of someone speaking and wrong for narration made from the text.

Unlike publish, it is honored when the guide goes to review, since it describes the upload rather than asking to publish it. publish: true implies it.

When the upload updates a guide that already exists, add retrainingIntent to the finalize body to say whether people who already learned it should be retrained once the update is live:

Value What happens
none Nobody is asked to retrain. The default.
changed_material They get a short lesson on what changed.
full_retake They take the original lesson again.

An update that publishes now applies it at once. One that goes to review is offered to the reviewer as the starting choice, and the reviewer decides. It is ignored for a guide uploaded for the first time, since nobody has learned it yet.

When the guide was created directly, the response carries a url to it in the web app:

{ "id": "…", "title": "Filing an expense", "mode": "published", "url": "https://mereon.ai/guides/…/view" }

With publish the page is useful once processing finishes, which takes a few minutes, so say so if you show the link to someone straight away. Without it the guide is a draft and the link is for people who may edit it. A guide going to review has no url: it is not visible to learners yet.

There is no mode in the request. It is worked out again from your permission, so the answer cannot be influenced by the client.

That means the answer can differ from the one upload-urls gave you, if the caller’s permission changed in between. The guide is still finalized either way; what changes is whether it also becomes a review submission. Read mode on this response rather than the earlier one when telling someone where their guide went.

Status Meaning
400 The body is invalid, or a step references a screenshotKey that belongs to another guide.
403 The token lacks content:write. Note that lacking permission to publish is not an error: it routes to review instead.
404 The id does not exist, or belongs to somewhere the caller cannot see. The two are deliberately indistinguishable.