sucuri
Docs Sign in

POST /api/executions

API

You send Python, it runs sealed, you get stdout and what it cost. A run reaches nothing this deployment has not allowed: no filesystem beyond an in-memory /tmp, no process, no environment, and only the network endpoints the deployment permits. Every execution gets a process of its own and that process is thrown away afterwards, so nothing a run leaves behind can reach the next one — not a file, not a variable, not a module it imported. Two runs of the same program start from the same blank slate, and a run that fails in any way fails alone. `POST /api/executions` runs code. `GET /api/executions` lists the runs your key still has going, and `DELETE /api/executions/{id}` stops one — you need the first to use the second, because a run's own id only comes back once it has finished. Both are scoped to the key that started the run: another key never sees it, not even another key on your account.

Machine-readable schema: /api/openapi.json

quickstart
curl https://abstra-sucuri.eastus.cloudapp.azure.com/api/executions \
  -H "Authorization: Bearer $SUCURI_API_KEY" \
  -d '{"code": "print(sum(range(1000)))"}'

Request

code string required
The Python to run.
stdin string
Text fed to the program's stdin, split on newline into a queue of lines. input() pops one line per call and raises EOFError when the queue is empty, as CPython does at end of file. The optional prompt argument is written to stdout, like CPython.
files {path: contents}
Files written into /tmp before the run, for inline inputs. They count against the same maxTmpBytes budget the program writes against, so you cannot pre-fill past the limit.
mounts array
Filesystems to attach. See below. Omit it and the run sees only /tmp.
maxTmpBytes integer
Ceiling on /tmp, in bytes. It is clamped to the server maximum, so it can only narrow. The limit is on the total held, not per file.
modules {dotted.name: source}
Extra importable modules. This is how you ship code you do not want to inline into code, without deploying anything anywhere.
net ["host:port"]
Outbound endpoints this run may reach. Matching is exact on both host and port: api.example.com:443 permits neither a subdomain nor port 80. The ceiling is the deployment's and this field can only narrow it; an endpoint outside the ceiling is dropped and refused at connect time. Omit the field and the run gets the whole ceiling. When the deployment's ceiling is empty, no run has any network.
env {name: value}
Environment variables the run sees. Feeds os.getenv and os.environ inside the guest. Omit it and the run sees an empty environment — nothing is inherited from the host process.
hostCall.url string
Where sucuri.call(name, payload) inside the guest is sent. The server POSTs {name, payload} as JSON here and expects {ok: true, result} or {ok: false, error} back. The guest never sees this URL. Omit hostCall and sucuri.call raises PermissionError. Time spent waiting on the host does not count toward limits.timeoutMs.
hostCall.headers {name: value}
Headers sent with every host call, typically a per-execution bearer token. Held by the server and never shown to the guest.
limits.timeoutMs integer
Wall-clock ceiling for this run. Clamped to the deployment's own timeout, so it can only narrow it.
limits.maxInstructions integer
Instruction ceiling for the run. It can only narrow: the run stops at the smallest of this, the server ceiling, the per-run cap and your remaining balance, and status comes back halted.

Mounting a filesystem

A run has no filesystem of its own. It gets an in-memory /tmp, dropped when the run ends, plus whatever you mount. A mount points at your bucket or container and uses your credentials, so the data stays where it already is and nothing is stored here. A path under no mount is refused, like every other capability you did not grant.

provider Addressing Credentials Works with
s3 bucket, region, endpoint accessKeyId, secretAccessKey, sessionToken AWS S3, Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces, Wasabi
gcs bucket accessKeyId, secretAccessKey Google Cloud Storage, through its S3-compatible API with an HMAC key
azure-blob account, container sasToken or accessKey Azure Blob Storage

Fields of a mount

at string required
Absolute path the mount appears at, e.g. /data. It may not be /tmp or inside it, may not contain .., and may not nest inside another mount. Overlapping mounts are refused rather than resolved by a rule you would have to guess.
provider s3 | gcs | azure-blob required
Which service to talk to.
bucket string
Bucket name, for s3 and gcs.
account, container string
Storage account and container, for azure-blob.
prefix string
Prepended to every key, so a mount can expose one subtree of a bucket. With at: "/data" and prefix: "runs/42/", reading /data/in.csv fetches the key runs/42/in.csv.
region string
SigV4 region. Defaults to us-east-1, which is what S3-compatible services that do not use regions expect to see.
endpoint string
Override the provider's endpoint. Required for R2, MinIO and B2. It must be https and must resolve to a public address; redirects are not followed.
readOnly boolean
Refuse every write, append, delete and rename under this mount. Enforced before any request leaves this service, so a read-only mount never even asks.
credentials object
Sent over TLS, used for the run, and never stored, logged or echoed back. Your code cannot read them: no syscall exposes them and they are not placed in the environment. Omit them entirely for a public bucket.

Worked examples

Your first run — no storage, no setup

Nothing to configure: /tmp is memory, a relative path lands there because /tmp is the working directory, and the whole filesystem disappears when the run ends. Everything below adds storage to this.

/tmp only
{
  "code": "open('out.txt','w').write('hi')\nprint(open('out.txt').read())",
  "files": { "/tmp/in.json": "{\"n\": 1}" },
  "maxTmpBytes": 1048576
}

Feed it stdin

input() pops one line of stdin per call and raises EOFError when there is nothing left, so a program can read until end of input the way it would in a terminal.

stdin
{
  "code": "total = 0\nwhile True:\n    try:\n        total += int(input())\n    except EOFError:\n        break\nprint(total)",
  "stdin": "1\n2\n3"
}

Read from S3, write the result back

Two mounts: inputs read-only, outputs writable. The run never sees your whole bucket, only the prefixes you mounted.

s3
{
  "code": "import csv\nrows = list(csv.reader(open('/in/sales.csv')))\nopen('/out/count.txt','w').write(str(len(rows)))",
  "mounts": [
    { "at": "/in",  "provider": "s3", "bucket": "acme-data", "prefix": "2026-08/",
      "region": "us-east-1", "readOnly": true,
      "credentials": { "accessKeyId": "AKIA...", "secretAccessKey": "..." } },
    { "at": "/out", "provider": "s3", "bucket": "acme-results", "region": "us-east-1",
      "credentials": { "accessKeyId": "AKIA...", "secretAccessKey": "..." } }
  ]
}

Cloudflare R2, MinIO, Backblaze B2

Anything that speaks S3 works by setting endpoint. Use region "auto" for R2.

s3 + endpoint
{
  "code": "print(open('/data/model.json').read()[:80])",
  "mounts": [
    { "at": "/data", "provider": "s3", "bucket": "models", "region": "auto",
      "endpoint": "https://abc123.r2.cloudflarestorage.com", "readOnly": true,
      "credentials": { "accessKeyId": "...", "secretAccessKey": "..." } }
  ]
}

Google Cloud Storage

Through GCS's S3-compatible API, with an HMAC key from your service account. No OAuth flow to set up.

gcs
{
  "code": "open('/bucket/out.txt','w').write('done')",
  "mounts": [
    { "at": "/bucket", "provider": "gcs", "bucket": "acme-exports",
      "credentials": { "accessKeyId": "GOOG1...", "secretAccessKey": "..." } }
  ]
}

Azure Blob Storage

A container SAS token is the narrowest credential to hand over: scope it to the container and let it expire.

azure-blob
{
  "code": "import os\nprint(sorted(os.listdir('/models')))",
  "mounts": [
    { "at": "/models", "provider": "azure-blob", "account": "acmestore",
      "container": "models", "readOnly": true,
      "credentials": { "sasToken": "?sv=2024-11-04&se=..." } }
  ]
}

Response

executionId uuid required
This run.
status completed | failed | halted required
halted means a limit stopped the run: the instruction ceiling, the timeout, the heap or recursion depth. Your code cannot catch it.
stdout string required
Everything the program printed.
result string
The value of the last expression statement in code, as repr() prints it — the rule a notebook cell follows. 2 + 2 yields "4"; x = 2 + 2 yields nothing, because an assignment is not an expression; print(x) yields nothing, because it evaluates to None. Absent whenever there is no such value. An object of your own class renders through its __repr__, falling back to <ClassName object> when it defines none. Capped at 64 KiB — see resultTruncated.
resultTruncated bool
Present and true when result hit the 64 KiB ceiling and was cut. The cut is the transport's alone: inside the run the value was whole, so len(repr(x)) still answers the real length. Absent means nothing was cut.
resultError string
Present when rendering result raised — a __repr__ of your own that failed. It does NOT mean the run failed: the program finished, status is completed, and only this one rendering did not. Without it a __repr__ that raised would look exactly like a class that defines none.
error string
The Python error, when the run raised.
instructions integer required
What you are billed. The same program on the same input always bills the same, so you can predict it and check it.
cpuMicros integer required
CPU microseconds consumed. A diagnostic, not the bill: it varies with the machine and with whoever else is on it.

The Python you get

Every module this sandbox can import, with what each one exports. The list is generated from the interpreter's own registry, so it is what the running engine resolves, not a promise about it. Importing anything else raises ModuleNotFoundError, which your code can catch. The `modules` request field adds your own Python on top. The list names what a module exports, not what each name can do: a method the engine does not implement raises AttributeError, which your code can catch too, so a program can probe a NAME before it depends on it. Syntax is the exception: a construct the compiler does not accept fails the whole submission before anything runs, so it cannot be probed for at runtime and nothing is printed or billed — check `status` for `failed` and read `error`. `collections`, the one most reached for, is complete on its mapping and deque surface — every public method CPython 3.14 gives dict, defaultdict, Counter, OrderedDict and deque, plus their operators. One module needs a warning rather than a list: `random` is seeded per execution, so two runs of the same program draw different sequences, as they would under CPython. `random.seed(n)` in your code chooses the sequence and replays it exactly — that is the only way to ask for a reproducible run, and taking `n` from `stdin` or a file is how you vary it per call without changing the program. What the generator is NOT is a source of secrets: it is an ordinary pseudo-random generator with a 64-bit state, so use it for simulation and test data, never for a token, a key or anything that has to be unguessable.

__future__
absolute_import, annotations, division, generator_stop, nested_scopes, print_function, unicode_literals, with_statement
abc
ABC, ABCMeta, abstractmethod
asyncio
Event, Lock, Queue, Semaphore, gather, run, sleep, wait_for
base64
b64decode, b64encode
bisect
bisect, bisect_left, bisect_right, insort, insort_left, insort_right
collections
Counter, OrderedDict, defaultdict, deque, namedtuple
contextlib
Provided as Python source.
contextvars
ContextVar
copy
copy, deepcopy
csv
DictReader, DictWriter, reader
dataclasses
MISSING, asdict, astuple, dataclass, field, fields, is_dataclass, replace
datetime
date, datetime, time, timedelta, timezone
decimal
Decimal
enum
Enum, IntEnum, auto
fractions
Fraction
functools
partial, reduce
hashlib
md5, sha256
heapq
heapify, heappop, heappush, nlargest, nsmallest
http
client
http.client
HTTPConnection, HTTPSConnection
inspect
Provided as Python source.
io
StringIO
itertools
accumulate, chain, combinations, compress, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, starmap, takewhile, zip_longest
json
dump, dumps, load, loads
logging
CRITICAL, DEBUG, ERROR, Formatter, INFO, StreamHandler, WARNING, basicConfig, getLogger
math
ceil, e, factorial, floor, gcd, inf, isfinite, isinf, isnan, nan, pi, pow, sqrt
operator
add, attrgetter, itemgetter, methodcaller, mul, sub, truediv
os
environ, getcwd, getenv, listdir, mkdir, path, sep, stat, walk, write_text
pathlib
Path
pickle
dumps, loads
random
choice, choices, getrandbits, randbytes, randint, random, randrange, sample, seed, shuffle, uniform
re
findall, search, split, sub
shlex
join, quote, split
socket
AF_INET, AF_INET6, SOCK_DGRAM, SOCK_STREAM, socket
statistics
mean, median, mode, stdev, variance
string
Template, ascii_letters, ascii_lowercase, ascii_uppercase, digits, punctuation
struct
calcsize, pack, unpack
subprocess
check_output, run
sucuri
call
sys
byteorder, getsizeof, maxsize
tempfile
mkdtemp
textwrap
dedent, fill, shorten, wrap
traceback
extract_tb
typing
Any, Dict, FrozenSet, Generic, List, Literal, Optional, Set, Tuple, Type, TypeVar, Union, cast, dataclass_transform, get_args, get_origin, get_type_hints, runtime_checks
typing_extensions
Any, Dict, FrozenSet, Generic, List, Literal, Optional, Set, Tuple, Type, TypeVar, Union, cast, dataclass_transform, get_args, get_origin, get_type_hints, runtime_checks
urllib
parse, request
urllib.parse
quote, unquote, urlencode, urlparse
urllib.request
Request, urlopen
weakref
WeakValueDictionary, ref

What gets raised

BaseException
Every builtin exception class of CPython 3.14 exists here under the same name and with the same bases, so naming one in except never costs you a NameError and a handler written against a base fires: except ArithmeticError catches a ZeroDivisionError, except LookupError catches a KeyError, and except Exception does NOT catch KeyboardInterrupt, SystemExit or GeneratorExit. Which of them the engine itself raises is a separate question — the rest of this section covers that. ExceptionGroup and BaseExceptionGroup are here too, with .exceptions, .subgroup, .split and .derive, and except* splits a group across its clauses and re-raises whatever no clause claimed.
type(e).__name__
Catchable. A failing builtin raises the class CPython raises, so the handler you would write for it fires: [].pop() is an IndexError, [1].index(9) and min([]) are ValueError, next() past the end is StopIteration, ord('ab') and sorted([1,'a']) are TypeError. A failure no rule recognises arrives as RuntimeError — the net, so an unforeseen failure is still caught rather than escaping.
TypeError: not iterable
Catchable. A generator is an iterable and is DRAINED wherever one is accepted, which covers map, filter, zip and enumerate, since each of those returns a generator: sorted(map(...)), dict(zip(...)) and "".join(map(str, xs)) all run the source. Draining consumes it, exactly as in CPython, so a second walk of the same object is empty. Being iterable is not being a sequence: reversed, random.choice, bisect and urlencode measure or index their argument, so an iterator may be refused with a TypeError where a list of the same items is accepted. Wrap it in list(...) and the question does not come up. A membership test CONSUMES the source too: after 3 in items, treat items as spent rather than as partly read. io.StringIO is iterable too, LINE by line, moving the buffer's own cursor — reading a line through the iterator and reading it with readline are the same read.
str.encode / bytes.decode
Catchable. Three text codecs are implemented — utf-8, ascii and latin-1 — under the aliases CPython accepts for them, and utf-8 is the default. Any other codec name raises LookupError, which is NOT a ValueError, so a typo'd name does not slip past as a bad value. Text the codec cannot represent raises UnicodeEncodeError and bytes it cannot read raise UnicodeDecodeError, both with CPython's message and both catchable as ValueError. Substitution policies are not offered: errors= is refused rather than silently ignored, so text that cannot be encoded raises instead of arriving mangled.
OSError
Catchable. Every capability the sandbox refuses and every one that fails: a path under no mount, a write past maxTmpBytes, an endpoint outside the ceiling, a subprocess. A refusal is a PermissionError, which is what to catch when the question is whether a capability was granted; anything else the filesystem reports arrives as OSError. Catch OSError when you want both.
ModuleNotFoundError
Catchable. An import of a module this sandbox does not have. It is a subclass of ImportError, so except ImportError catches it too, and e.name is the module that was missing.
EOFError
Catchable. input() with nothing left in stdin, exactly where CPython raises at end of file.
status: failed
In the response. The program raised and nothing caught it: error carries the exception and stdout carries what was printed before it. Code that does not compile reports here too, and bills nothing.
status: halted
Not catchable. A resource limit stopped the run: the instruction ceiling, the wall-clock timeout, the heap or recursion depth. It is the one thing your code can neither catch nor clean up after — there is no exception, the run stops. Everything executed up to that point is billed.

What a call costs

Billing is in instructions. Interpretation charges one per instruction, a builtin that iterates charges per element, and a call that blocks pays the fixed price below. Waiting itself is free by design, so a slow response costs no more than a fast one.

Call Instructions What it covers
socket connect50,000Opening a connection: a TCP connect, a TLS handshake, or a UDP bind.
socket send / recv10,000One send or one receive on an open socket, whatever its size.
mount read / write20,000One round trip to a mounted bucket or container: read, write, stat, list.
/tmp operation500One operation on the in-memory /tmp, which is RAM: no network and no disk.
/tmp byte1Per byte moved through /tmp, on top of the operation itself.

A call is charged when it is attempted, whether it succeeds, fails or is refused — otherwise a program could probe the sandbox for free. Code that does not compile runs nothing and bills nothing.

No run executes more than 100,000,000 instructions, whatever the balance is. Past that it is halted, and everything it did up to the halt is billed.

Editor

Signed in, the portal has a Python editor that runs programs on Sucuri (Run, or Ctrl/⌘+Enter), debugs them, and understands them as you type. Runs and debug sessions are real executions, billed against your balance; the balance updates as soon as one ends.

Debugger

Breakpoints click the gutter
Click the margin next to a line to stop there; click again to remove. A line with no code of its own (blank, a comment) binds to the next line that has some, and the dot moves there. Breakpoints are kept with your code in the browser.
Conditions, hit counts, log points right-click the gutter
Plain text is a condition (n > 10), evaluated in the frame each time the line is reached. hit >= 3 stops only from the third pass on (== 3, % 2 and the other VS Code forms work too). log total is {total} prints the message with the expressions in braces evaluated, and does not stop.
Continue, pause, stop F5 · F6 · Shift+F5
F5 starts a debug session, and continues a stopped one. Pause stops a running program at its next line. Stop ends it: the run ends as halted, like a cancelled execution.
Step over, into, out F10 · F11 · Shift+F11
The same semantics as debugpy in VS Code: over does not enter calls, into enters the next call, out returns to the line of the call in the caller.
Exceptions
An uncaught exception stops the program on the line that raised it, with the stack still there to inspect. Tick "Stop on raised exceptions" to also stop where exceptions are raised and caught (StopIteration and GeneratorExit, which are control flow, never stop).
Call stack and variables
Click a frame to see its locals and the globals, and its line highlighted. Lists, tuples, sets, dicts, objects and modules expand. Double-click a value to change it: what you type is evaluated in that frame.
Watch, console, hover
Watched expressions are evaluated again at every stop. The console evaluates any expression in the selected frame, and name = value assigns. Hovering a name (or obj.attr) while stopped shows its value.
Billing and limits
A debug session is an execution: instructions are metered and billed exactly like POST /api/executions, including those your watches and console run. Time spent stopped does not count against the timeout. One debug session per account at a time, 15 minutes at most.

Language server

Diagnostics
Type errors, undefined names and wrong calls are underlined as you type. So is what Sucuri cannot run: importing a standard-library module the engine does not have, and constructs its compiler refuses — marked as sucuri, before anything is executed.
Completion Ctrl+Space · .
Names in scope, attributes after ., keyword arguments and modules, with their types and documentation. Completing a name that is not imported yet adds the import.
Hover and signature help
Hovering shows the inferred type and the documentation. Inside a call's parentheses, the signature is shown with the current parameter highlighted.
Inlay hints
Inferred types of unannotated variables and parameter names at call sites are shown inline, in grey.
Navigation F12 · Shift+F12
Go to definition, declaration and type definition; find all references; occurrences of the name under the cursor are highlighted.
Rename F2
Renames a variable, function, class or parameter everywhere it is used, and only there.
Quick fixes Ctrl+.
Fixes the language server offers for a diagnostic, such as adding a missing import.
Formatting Shift+Alt+F
Formats the program with ruff's formatter (Black style).
Outline, folding, selection Ctrl+Shift+O · Shift+Alt+→
Jump to any function or class; fold blocks; grow the selection to the enclosing expression, statement and block.
Semantic highlighting
Colours from what the program means rather than how it looks: parameters, classes, functions, modules and properties are told apart.
How it runs
The language server is ty (from Astral, the makers of ruff), told about Sucuri's standard library rather than CPython's. Each editor tab has its own isolated server process; nothing is executed and nothing is billed. Up to 3 tabs per account.

HTTP responses

400
Malformed body, or a mount that cannot be accepted: a bad mount point, two mounts that overlap, or an endpoint this service will not connect to.
401
Missing or invalid API key.
402
Your balance is at or below zero. The body carries a topUp URL.
503
We could not meter the run, so it did not execute and nothing was charged. Retry.