sucuri
Docs Sign in

POST /api/executions

API

One endpoint. 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.

Machine-readable schema: /api/openapi.json

quickstart
curl https://sucuri.abstra.io/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.
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 before it depends. `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` draws from a real generator, but from a FIXED default seed, so the same program draws the same numbers on every run. That is deliberate — a run whose draws varied would bill differently each time, and the instruction count is meant to be predictable and auditable. `random.seed(n)` chooses the sequence. Use it for simulation and test data; never for a secret, a token 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
io
StringIO
itertools
accumulate, chain, combinations, compress, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, starmap, takewhile, zip_longest
json
dumps, loads
logging
CRITICAL, DEBUG, ERROR, Formatter, INFO, StreamHandler, WARNING, basicConfig, getLogger
math
ceil, e, factorial, floor, gcd, pi, pow, sqrt
operator
add, attrgetter, itemgetter, methodcaller, mul, sub, truediv
os
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
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 they refuse a generator or an iterator with a TypeError even though a list of the same items is accepted. in is the exception that does NOT drain: it pulls only as far as the answer, so 3 in endless_generator() returns and what it did not pull is still there. 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. The errors= argument is not read: a failure is raised, never substituted.
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. The message is CPython's [Errno N] form and the class is the matching subclass — PermissionError for 13, FileNotFoundError for 2, plain OSError otherwise.
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.

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.