Metadata-Version: 2.5
Name: robovue-sdk
Version: 0.2.2
Summary: Report runs from RPA bots and your own code to Robovue. It signs and retries each request, and checks each report before sending it.
Project-URL: Homepage, https://robovue.co.uk
Project-URL: Documentation, https://robovue.co.uk/api
Author: Robovue
License-Expression: MIT
License-File: LICENSE
Keywords: automation,bots,monitoring,robovue,rpa,webhook
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Description-Content-Type: text/markdown

# robovue-sdk

Report your bots' runs to [Robovue](https://robovue.co.uk), which shows every bot in one place and
alerts you when one fails or misses a run. This package signs each report and retries when the
network or Robovue has a problem. It checks each report against Robovue's rules before sending it,
and it can send dry runs while you set it up. It uses only Python's standard library.

## Install

```shell
pip install robovue-sdk
```

## Requirements

- Python 3.9 or later. No other packages are needed.
- A Robovue workspace. Signing up is free.

If you cannot install a package where your bot runs, such as Power Automate Desktop's built-in
Python, use Connections → Your code in Robovue. It gives you the same request as plain Python, C#
and shell code to paste in. The [wire contract](#the-wire-contract-any-language) below describes it
for any language.

## Quickstart

1. **Sign up** at [robovue.co.uk](https://robovue.co.uk).
2. **Copy your credentials** from **Settings → Connections → Your code**: the webhook secret and
   your Workspace ID (`tenantId` in the request). The Workspace ID is the long ID in the address
   shown there: `https://app.robovue.co.uk/ingest/{tenantId}/runs`.
3. **Keep them out of your code.** Put them in your platform's credential store, or set them as the
   environment variables `ROBOVUE_TENANT_ID` and `ROBOVUE_WEBHOOK_SECRET` on the machine that runs
   the bot. Then create one client:

<!-- sample:begin imports create-client -->
```python
import os
import uuid
from datetime import datetime, timezone

from robovue_sdk import FailureCategory, RobovueClient, RunReport, RunStatus

# One client for the whole process: it's safe to share between threads.
robovue = RobovueClient(
    base_url="https://app.robovue.co.uk",
    tenant_id=os.environ["ROBOVUE_TENANT_ID"],
    webhook_secret=os.environ["ROBOVUE_WEBHOOK_SECRET"],
)
```
<!-- sample:end -->

4. **Wrap your bot's job.** Report the run as `RUNNING` when it starts, then as `SUCCESS` or
   `FAILURE` when it ends. Use the same delivery ID for both, so Robovue knows
   it's one run:

<!-- sample:begin quickstart -->
```python
delivery_id = str(uuid.uuid4())  # one per run: it ties the run's reports together
started_at = datetime.now(timezone.utc)

robovue.try_report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.RUNNING,
        started_at=started_at,
    ),
    delivery_id=delivery_id,
)

try:
    process_invoices()  # your bot's work
except Exception as exc:
    robovue.try_report_run(
        RunReport(
            bot_id="invoice-bot-01",
            platform="python",
            status=RunStatus.FAILURE,
            started_at=started_at,
            completed_at=datetime.now(timezone.utc),
            failure_category=FailureCategory.APPLICATION_EXCEPTION,
            payload={"error": str(exc)},
        ),
        delivery_id=delivery_id,
    )
    raise

robovue.try_report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.SUCCESS,
        started_at=started_at,
        completed_at=datetime.now(timezone.utc),
    ),
    delivery_id=delivery_id,
)
```
<!-- sample:end -->

   `try_report_run` never raises, so a problem reaching Robovue cannot break your bot. The job's own
   exception is reported, then raised again, so the bot still fails the way it always did.

5. **Check it arrives.** Run the bot once with [`dry_run`](#checking-a-new-integration-with-a-dry-run)
   on, then for real. Each report shows in the Verify step on Connections → Your code and in the
   request log. The bot shows on Home once you register it, as the next section explains.

## Make missed runs alert

A failure alerts as soon as it's reported. Robovue can only notice a run that never happens if it
knows when to expect one, so register the bot and give it a schedule. Its Bot ID is the `bot_id`
you report with. Until a bot is registered, Home says it's reporting but not registered and links
to **Bots not yet registered**, where you register it. Each new Bot ID counts toward your plan's
bot limit.

## Reporting in more detail

### Failure categories

On a `FAILURE`, set `failure_category` to say why it failed. It's shown on the run, filterable on
Runs, and named in the alert. Leave it out if you do not know: Robovue records `Uncategorized`, and a
run can be reclassified later from its page.

<!-- sample:begin failure-category -->
```python
result = robovue.report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.FAILURE,
        started_at=started_at,
        completed_at=datetime.now(timezone.utc),
        failure_category=FailureCategory.BUSINESS_EXCEPTION,
    )
)
```
<!-- sample:end -->

| Category | Use it for |
|---|---|
| `BUSINESS_EXCEPTION` | A business rule stopped the run: bad input data, a missing record, a rejected transaction. |
| `APPLICATION_EXCEPTION` | Something broke in the bot or the application it drives. |
| `TIMEOUT` | The run, or a step in it, took too long. |
| `AUTHENTICATION_ERROR` | The bot could not sign in to something it needs. |
| `CANCELLED` | A person or a scheduler stopped the run. |
| `UNCATEGORIZED` | You do not know why. This is the default. |

### Attaching context with `payload`

`payload` must be a dict (a JSON object), and anything else raises a `ValueError`. Robovue stores it
as sent and shows it in full on the run's page. Put in whatever would help you debug the run later.
If it has an `error`, `errorMessage` or `message` key, the alert uses that text to say what went
wrong.

<!-- sample:begin payload -->
```python
result = robovue.report_run(
    RunReport(
        bot_id="invoice-bot-01",
        platform="python",
        status=RunStatus.FAILURE,
        started_at=started_at,
        completed_at=datetime.now(timezone.utc),
        failure_category=FailureCategory.TIMEOUT,
        payload={
            "invoiceId": "INV-2291",
            "error": "Timed out waiting for the vendor portal",
        },
    )
)
```
<!-- sample:end -->

The whole report, payload included, must be under 64 KB.

### Attaching a screenshot or log to a failed run

On the Team plan and above, you can attach one screenshot or log excerpt to a `FAILURE` run once
it's reported. Screenshots are off for each bot until someone turns them on (**Manage bots →
Screenshots**), because a screenshot can capture credentials or other sensitive data on screen.

<!-- sample:begin attachment -->
```python
robovue.upload_attachment(result.run_id, screenshot_bytes, "image/png")
```
<!-- sample:end -->

`content_type` is `"image/png"`, `"image/jpeg"` or `"text/plain"`, up to 5 MB, one per run. Robovue
deletes attachments after 30 days, or 7 or 14 if an Admin chose that. An upload for a bot with
screenshots off, or on a plan without them, gets the same 404 as an unknown run. Catch it if a
missing screenshot should not fail your bot.

### Reporting without ever raising

`try_report_run` never raises for a failed report, so reporting cannot make your bot fail. An
invalid report, a rejection or a network problem comes back in the outcome for you to log.

<!-- sample:begin try-report -->
```python
outcome = robovue.try_report_run(report)
if not outcome.succeeded:
    print(f"Could not report the run to Robovue: {outcome.error}", file=sys.stderr)
```
<!-- sample:end -->

### When a report is rejected

`report_run` raises `RobovueApiException`. Branch on `code`, which is stable; the message is for
people. `field` names the field at fault when there is one, and `request_id` is worth quoting if you
contact support.

<!-- sample:begin rejected -->
```python
from robovue_sdk import RobovueApiException

try:
    robovue.report_run(report)
except RobovueApiException as exc:
    if exc.code != "timestamp_out_of_range":
        raise
    # This machine's clock is more than 5 minutes out. The message gives Robovue's time.
    print(exc, file=sys.stderr)
```
<!-- sample:end -->

Network errors and 5xx responses are retried automatically, with 3 attempts by default. A `429` is
retried after its `Retry-After` wait. If that wait is over a minute, the 429 is raised instead, so
your bot is not blocked. A report is checked against the server's own rules before anything is sent, so
most mistakes fail at once with a `ValueError`. A `datetime` with no time zone is treated as UTC.
Every code is in the [responses table](#responses) below, with whether a retry can help.

### Checking a new integration with a dry run

With `dry_run=True`, every request is checked exactly as a real one would be (signature, fields,
plan limits, delivery ID) but nothing is stored. The result says what would have happened, and a
request that would fail raises the real error.

<!-- sample:begin dry-run -->
```python
robovue = RobovueClient(
    base_url="https://app.robovue.co.uk",
    tenant_id=tenant_id,
    webhook_secret=webhook_secret,
    dry_run=True,  # every check runs, nothing is stored
)

result = robovue.report_run(report)
print(result.dry_run, result.would_be)  # True created
```
<!-- sample:end -->

Each dry run shows in the request log, labelled Dry run. Until a real report has arrived, the Verify
step on Connections → Your code also says when a dry run passed in the last 24 hours. Turn dry runs
off before going live, or no runs are recorded.

### Runs that take a while

The quickstart reports `RUNNING` first. That's optional. With it, a bot that starts a job and then
hangs shows as Running straight away. Without it, the problem shows only when the bot misses its
next run.

The report that ends the run must reuse the delivery ID. With a fresh one, Robovue records two runs:
one stuck at Running and a separate finished run. A run left at Running does not stop a missed-run
alert. If it never completes, the bot still goes Late and then Missed on its schedule.

## Using it in your bot

**One client per process.** `RobovueClient` holds no state between calls, so share one instance
across threads.

**Timeouts, proxies and TLS.** Each attempt times out after 30 seconds. Requests go through
Python's `urllib`, so they use the operating system's trusted certificates and the proxy in
`HTTPS_PROXY` (or, on Windows, the system's proxy settings). If your network inspects TLS with its
own certificate, set `SSL_CERT_FILE` to a certificate bundle that includes it. For anything else,
pass `transport=`. It's a function that takes the URL, a dict of headers and the body bytes. It
sends the POST and returns `(status, response_body, response_headers)`.

## Options

| `RobovueClient(...)` | Default | What it does |
|---|---|---|
| `base_url` | required | `https://app.robovue.co.uk` |
| `tenant_id` | required | Your Workspace ID (`tenantId` in the request). |
| `webhook_secret` | required | Signs every request. Never put it in source code. |
| `max_retry_attempts` | `3` | Attempts per call, the first included. |
| `retry_base_delay` | `0.2` | Seconds before the first retry; each later wait doubles. |
| `dry_run` | `False` | Check every request in full and store nothing. |
| `transport` | `urllib` | How requests are sent (see above). |

## Troubleshooting

| What you see | What to do |
|---|---|
| `invalid_signature` | The webhook secret is wrong, or it was rotated. Copy the current one from Connections → Your code. |
| `timestamp_out_of_range` | The machine's clock is more than 5 minutes out. Sync it (on Windows, `w32tm /resync`). |
| `unknown_tenant` | The Workspace ID is wrong. Copy it from the address on Connections → Your code. |
| `bot_cap_reached` | A new Bot ID would take you over your plan's bot limit. Archive a bot you no longer use, then delete it. Or an Admin can choose a bigger plan on the Billing page. |
| `payload_too_large` | The report is over 64 KB. Send less in `payload`. On the Team plan and above, a failed run can carry a log file as an attachment instead. |
| A 403 with no code | Something between your bot and Robovue blocked the request, such as a proxy rewriting it. Ask whoever runs your network, or email contact@robovue.co.uk with the time it happened. |
| An `OSError` or `URLError` | The machine cannot reach `app.robovue.co.uk`. Check its proxy and firewall. |
| `CERTIFICATE_VERIFY_FAILED` | Your network inspects TLS. Point `SSL_CERT_FILE` at a bundle that includes its certificate. |
| The report succeeds but the bot is not on Home | The bot is not registered yet: see [Make missed runs alert](#make-missed-runs-alert). Or `dry_run` is still on, the bot is archived (Home links to **Archived**), or the credentials belong to another workspace. |
| A bot stopped running and nobody was alerted | It has no schedule. See [Make missed runs alert](#make-missed-runs-alert). |

The request log, in Connections, shows every request from the last 7 days, with the exact reason for any
that were rejected.

## The wire contract (any language)

This is exactly what `RobovueClient` sends. From a language with no Robovue SDK, send the same
request yourself. The tables below are generated from the server's own contract, so they list every
field, header, response and limit, including the optional ones.

<!-- contract:begin — generated from IngestionContract by ContractDocsTests; don't edit by hand -->
### `POST /ingest/{tenantId}/runs`

The endpoint both SDKs call, and the one to use from any other language. A JSON object with the fields below.

#### Headers

| Header | Value | Description |
|---|---|---|
| `Content-Type` | application/json | The body is JSON. A charset parameter is fine. |
| `X-Robovue-Delivery-Id` | 1–200 printable ASCII characters, no spaces | Unique to this run. Sending it again updates or confirms the same run instead of creating a second one, as a retry does, or the Success or Failure that ends a Running run. A GUID or your bot's own run ID both work. It cannot start with "uipath-job-" or "automation-anywhere-", because Robovue uses those for runs it receives from UiPath and Automation Anywhere. |
| `X-Robovue-Timestamp` | Unix time in whole seconds | When you signed the request. It must be within 5 minutes of Robovue's clock either way, so a captured request stops working within minutes. Sign each attempt just before sending it, retries included. |
| `X-Robovue-Signature` | v1=&lt;lowercase hex&gt; | An HMAC-SHA256 keyed with your webhook secret. Sign the timestamp, the word run and the delivery ID, each followed by a line break, then the exact request body. Encode it as lowercase hex and put v1= in front. Sign the exact bytes you send, because re-serialising the JSON after signing changes them. Several comma-separated v1= values are accepted, and any one matching is enough. |
| `X-Robovue-Dry-Run` | true | Checks the request exactly as a real one would be checked, including authentication, every field, the delivery ID and your plan's bot limit, and stores nothing. The response says what would have happened. Use it to test an integration without creating runs. Any other value, or no header, is an ordinary request. |

#### Body fields

| Field | Type | Required | Description |
|---|---|---|---|
| `botId` | string, ≤ 200 chars | Yes | Your name for the bot. Every run with the same botId belongs to the same bot in Robovue. Spaces and punctuation are fine, but not / or \. It cannot start or end with a space, or contain tabs or line breaks. The same ID registers and manages the bot through the Management API. |
| `platform` | string, ≤ 50 chars | Yes | What runs the bot, shown beside it in Robovue. Any short label works, such as dotnet, python, go or blue-prism. The same space and line break rules as botId apply. |
| `status` | one of `Success`, `Failure`, `Running` | Yes | How the run went. Running reports a run that has started and not finished yet. When it ends, send the same delivery ID again with Success or Failure. Values are case-sensitive. |
| `startedAt` | timestamp | Yes | When the run started, as an RFC 3339 timestamp with a time zone offset: Z for UTC, or ±hh:mm. For example, 2026-07-17T09:00:00Z. It cannot be before 2000, or more than an hour ahead of Robovue's clock. Unix times are not accepted. |
| `completedAt` | timestamp | Required when status is Success or Failure; leave it out when status is Running | When the run finished. It has the same format and limits as startedAt, and cannot be before it. |
| `failureCategory` | one of `Uncategorized`, `BusinessException`, `ApplicationException`, `Timeout`, `AuthenticationError`, `Cancelled` | No, only with status Failure | Why the run failed, if your bot can tell. If it cannot, leave it out: the run is recorded as Uncategorized, and you can change that later on the run's page. Values are case-sensitive. |
| `payload` | object | No | Anything that will help you debug the run later, such as an invoice number, the step that failed or a stack trace. It must be a JSON object, and it's stored as sent and shown on the run's page. A top-level error, errorMessage or message string becomes the summary in the failure alert. It counts toward the 64 KB request limit. |

#### Responses

Every error body is `{"error": "…", "code": "…", "field": "…", "requestId": "…"}`. `field` is there only when the error is about one field. Every response carries an `X-Request-Id` header.

| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 201 | None | A new run was recorded. Body: {"runId": "…", "duplicate": false} | No need. There is nothing more to send |
| 200 | None | The run was already known. duplicate is false when this completed a run reported as Running. It's true when this repeated a report already recorded, such as a retry, which is safe. | No need. There is nothing more to send |
| 200 | None | A dry run passed every check. Body: {"dryRun": true, "wouldBe": "created"}, where wouldBe can also be "completed" or "duplicate". Nothing was stored. | No need. There is nothing more to send |
| 400 | `invalid_body` | The body is not a single valid JSON object. The message gives the line and position. | No. Fix the request first |
| 400 | `duplicate_field` | A field appears twice. | No. Fix the request first |
| 400 | `unknown_field` | A field is not one of the documented names. Names are case-sensitive, so put your own data inside payload. | No. Fix the request first |
| 400 | `missing_field` | A required field is missing, blank or null. field names it. | No. Fix the request first |
| 400 | `invalid_field` | A field's value breaks its rule. field names it and the message says how. | No. Fix the request first |
| 400 | `missing_delivery_id` | X-Robovue-Delivery-Id is missing. | No. Fix the request first |
| 400 | `invalid_delivery_id` | X-Robovue-Delivery-Id is longer than 200 characters, has spaces or non-ASCII characters, or starts with a prefix Robovue reserves. | No. Fix the request first |
| 400 | `bot_cap_reached` | This would be a new bot, and your plan's bot limit is reached. Only deleting a bot frees its place: archive a bot you no longer use, then delete it. Or an Admin can choose a bigger plan on the Billing page. | No. Fix the request first |
| 401 | `missing_signature` | X-Robovue-Signature is missing. | No. Fix the request first |
| 401 | `missing_timestamp` | X-Robovue-Timestamp is missing. | No. Fix the request first |
| 401 | `invalid_timestamp` | X-Robovue-Timestamp is not in whole Unix seconds. Milliseconds are the usual cause. | No. Fix the request first |
| 401 | `timestamp_out_of_range` | X-Robovue-Timestamp is more than 5 minutes from Robovue's clock. The message gives the server's time; fix the sending machine's clock. | Yes, once re-signed with the current time |
| 401 | `invalid_signature` | The signature does not match. Check the webhook secret, and that you sign the timestamp, the delivery ID or run ID, and the exact bytes you send. | No. Fix the request first |
| 404 | `unknown_tenant` | No workspace has this Workspace ID. Copy the URL from Connections in Robovue. | No. Fix the request first |
| 409 | `delivery_id_conflict` | This delivery ID already belongs to another bot's run. Reuse a delivery ID only to complete the same bot's run. | No. Fix the request first |
| 413 | `payload_too_large` | The body is over 64 KB. | No. Fix the request first |
| 415 | `unsupported_media_type` | Content-Type is not application/json. | No. Fix the request first |
| 429 | `rate_limited` | Too many requests. Retry-After says how many seconds to wait. | Yes, after the Retry-After seconds |
| 500 | `internal_error` | Something went wrong on Robovue's side. If it keeps happening, quote the requestId. | Yes, with backoff. The same delivery ID makes it safe |

#### Limits

| Limit | Value | Notes |
|---|---|---|
| Run report size | 64 KB | Per request, payload included. Anything larger is rejected with 413, not cut short. |
| Run reports | 600 per minute per workspace | Shared by the run report, Power Automate and UiPath endpoints. A start and its completion count as two. |
| Attachment size | 5 MB | One file per failed run: PNG, JPEG or UTF-8 text. |
| Attachment uploads | 50 per hour per workspace | Across all bots. |
| Attachment storage | 500 MB per workspace | Attachments are deleted after the workspace's retention, 30 days at most, which frees their space. |
| Management API | 300 requests per minute per workspace | Every /tenants/… request. |
| Per address | 3,000 requests per minute per client IP | Across ingestion and the Management API together, for every workspace sending from that address. |
| Rejected credentials | 120 per minute per client IP | Requests with a wrong or missing signature or key, or an unknown Workspace ID. Past the limit, that address gets 429 until the minute ends. A workspace's own limits count only requests that authenticate, so other people's bad requests cannot use them up. |
<!-- contract:end -->

**Signing, in one line:** `"v1=" + hex(HMAC-SHA256(key: webhook secret, message: timestamp + "\nrun\n" + delivery ID + "\n" + the exact body bytes))`,
lowercase, sent with the same timestamp in `X-Robovue-Timestamp` (Unix seconds). Connections → Your code has a
signature checker to compare yours against.

The attachment upload (`POST /ingest/{tenantId}/runs/{runId}/attachment`) uses the same scheme, with three changes.
The word `attachment` replaces `run`, and the run ID (lowercase, hyphenated) replaces the delivery ID. The
signature covers the raw file bytes instead of a JSON body. The upload has no `X-Robovue-Delivery-Id` header,
and a run takes only one attachment, so a retry is still safe.

This scheme is only for the `/ingest/{tenantId}/runs` endpoint. UiPath Orchestrator, Power Automate and
Automation Anywhere connect to Robovue without code, and Connections in Robovue sets each one up.

## Versions and support

- The package follows [semantic versioning](https://semver.org). `CHANGELOG.md`, in the source
  distribution, lists what changed in each version.
- For bots written in C# or another .NET language, [`Robovue.Sdk`](https://www.nuget.org/packages/Robovue.Sdk)
  is the same client.
- Questions and problems: [contact@robovue.co.uk](mailto:contact@robovue.co.uk). Quote the `request_id` from
  the error if there is one.
- Found a security problem? See [robovue.co.uk/security](https://robovue.co.uk/security).

## Licence

MIT, so you can use, change and ship this package freely (see `LICENSE` in the package). Using the Robovue
service is covered by the [terms](https://robovue.co.uk/terms).
