# Calling operations (https://upapi.io/docs/api)



Every operation is called the same way. Learn the shape once and the whole
catalog is reachable — there is no per-operation base URL, no per-operation
auth scheme and no per-operation response envelope to special-case.

<Tabs groupId="upapi-docs-lang" items="['cURL', 'Node', 'Python']">
  <Tab value="cURL">
    ```bash
    curl -X POST https://api.upapi.io/{slug} \
      -H 'x-api-key: upapi_YOUR_KEY' \
      -H 'content-type: application/json' \
      -d '{ … }'
    ```
  </Tab>

  <Tab value="Node">
    ```ts
    const res = await fetch('https://api.upapi.io/{slug}', {
      method: 'POST',
      headers: {
        'x-api-key': process.env.UPAPI_API_KEY,
        'content-type': 'application/json',
      },
      body: JSON.stringify({}),
    });

    const data = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os, requests

    res = requests.post(
        'https://api.upapi.io/{slug}',
        headers={
            'x-api-key': os.environ['UPAPI_API_KEY'],
            'content-type': 'application/json',
        },
        json={},
    )

    data = res.json()
    ```
  </Tab>
</Tabs>

* **The slug is the path.** It is the callable form shown in the marketplace,
  which is not always identical to the operation's internal id — see
  [Quickstart](https://upapi.io/docs/quickstart#3-make-a-call).
* **The body is the input.** It is validated against the operation's schema; a
  mismatch is `400 INVALID_INPUT` and names the offending field.
* **A success returns the output object directly**, with no wrapper. Failures
  use the [standard error envelope](https://upapi.io/docs/errors).

That uniformity is the point of the marketplace. Swapping one operation for
another means changing the slug and the body — not rewriting your client.

## Try it [#try-it]

Pick an operation and send the request from here. The call goes straight from
your browser to `api.upapi.io` — nothing is proxied through this site — so it
runs against your own account and spends your own units, exactly as the same
request would from your code.

_(An interactive request builder is embedded here on the web page: pick an operation, paste an API key, and it POSTs to `https://api.upapi.io/{slug}` from the browser. The equivalent request is shown above.)_

<Callout title="It really runs">
  These are live operations, not a sandbox. A call that succeeds has genuinely scraped, fetched or
  posted something, and it is billed at the unit weight shown next to the operation.
</Callout>

## Idempotency [#idempotency]

An operation is a POST that does real work upstream — a comment posted, an
account created — and every accepted call spends units. So a request that times
out leaves you with a genuine question: did it happen?

Send an `Idempotency-Key` and the answer stops mattering.

<Tabs groupId="upapi-docs-lang" items="['cURL', 'Node', 'Python']">
  <Tab value="cURL">
    ```bash
    curl -X POST https://api.upapi.io/{slug} \
      -H 'x-api-key: upapi_YOUR_KEY' \
      -H 'idempotency-key: 8f14e45f-ea0f-4a2b-9d5a-5c4f2a1b7c30' \
      -H 'content-type: application/json' \
      -d '{ … }'
    ```
  </Tab>

  <Tab value="Node">
    ```ts
    await fetch('https://api.upapi.io/{slug}', {
      method: 'POST',
      headers: {
        'x-api-key': process.env.UPAPI_API_KEY,
        'idempotency-key': crypto.randomUUID(),
        'content-type': 'application/json',
      },
      body: JSON.stringify({}),
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os, uuid, requests

    requests.post(
        'https://api.upapi.io/{slug}',
        headers={
            'x-api-key': os.environ['UPAPI_API_KEY'],
            'idempotency-key': str(uuid.uuid4()),
            'content-type': 'application/json',
        },
        json={},
    )
    ```
  </Tab>
</Tabs>

The first request with a given key runs normally. Any later request with the
**same key and the same body** replays the first one's stored response, with
`Idempotency-Replayed: true`. Nothing runs again and nothing is billed again.

* **Format** — any string of 36–255 characters. A UUID is ideal. A key outside
  that range is `400 INVALID_INPUT`.
* **Scope** — your organization, for **24 hours**. After that the key is
  forgotten and reusing it starts a new request.
* **The body is part of the identity.** Reusing a key with a different slug or a
  different body is `409 IDEMPOTENCY_KEY_REUSED`, and nothing runs — silently
  replaying the first answer would be answering a question you did not ask. The
  raw bytes are compared, so re-serializing the same JSON differently counts as
  a different request.
* **A key whose first request is still running** answers
  `409 IDEMPOTENCY_IN_FLIGHT` with `Retry-After: 1`. Retry and you will get the
  stored response.

A replayed response carries no quota headers — nothing was consumed, so there is
no new position to report. Use [`GET /usage`](https://upapi.io/docs/usage-api) to read quota
without spending.

<Callout title="Two outcomes are deliberately not replayable">
  A `429` or a `5xx` releases the key instead of storing it: those say "this did not settle", and
  storing one would lock you out of the very retry `Retry-After` invited. Responses larger than \~100
  KB are also not stored — a repeat of that key runs as a fresh request, and bills as one.
</Callout>

The [TypeScript SDK](https://upapi.io/docs/sdk#idempotency) sends a key on every call and reuses
it across its own retries, so you get this without doing anything.

## Calling from a browser [#calling-from-a-browser]

The gateway answers CORS preflights and allows any origin, so a browser can call
it directly. `x-api-key`, `content-type`, `idempotency-key` and `x-proxy-url` are
all accepted on a cross-origin request, and the quota headers, `Retry-After`,
`x-request-id` and `Idempotency-Replayed` are all readable from one.

<Callout type="warn" title="That does not make a browser a good place for your key">
  An API key in front-end code is a key you have published. Keep the call server-side, or proxy it
  through your own backend. The CORS support exists for playgrounds, internal tools and first-party
  dashboards where the key is already the user's own.
</Callout>

## Where each operation is documented [#where-each-operation-is-documented]

This site teaches the pattern. The **marketplace** documents the operations:

| Here                                      | On the marketplace                         |
| ----------------------------------------- | ------------------------------------------ |
| The request shape, shared by everything   | One operation's input and output schema    |
| Auth, errors, quotas, retries             | Field descriptions, defaults and bounds    |
| The SDK, the MCP server, hosted functions | A worked example and a runnable playground |

Each operation's page lives at
`upapi.io/apis/{category}/{name}`, where `{name}` is the slug without its
`.get` or `.post` suffix — so
[`github-trending.get`](https://upapi.io/apis/developer-tools/github-trending)
is at that URL, and every other slug follows the same rule. The page carries the
full schema, code snippets in eight languages, and a try-it button that runs the
real operation against your account.

Note the host: browsing starts on the
[marketplace index](https://upapi.io/marketplace), which lists operations by
category and by search, and each listing links across to its detail page on the
app.

## Machine-readable sources [#machine-readable-sources]

If you would rather consume the catalog programmatically, these are served live
from the running platform:

* [`/api/catalog`](https://app.upapi.io/api/catalog) — every public operation with
  its input and output schemas, category and tags.
* [`/api/openapi.json`](https://app.upapi.io/api/openapi.json) — the same catalog
  as an OpenAPI 3.0.3 document.
* Each operation's marketplace page also offers its own OpenAPI slice and a
  Postman collection, for importing one operation rather than all of them.

That document is also enough to generate a typed client in your own language,
if the [TypeScript SDK](https://upapi.io/docs/sdk) is not what you want:

{/* Deliberately not `<Tabs groupId="upapi-docs-lang">`: this is a choice of code generator, not of request language, and a shared group whose items are a subset renders an empty panel for anyone who last picked cURL. */}

```bash title="TypeScript"
npx openapi-typescript https://app.upapi.io/api/openapi.json -o upapi.d.ts
```

```bash title="Python"
openapi-python-client generate --url https://app.upapi.io/api/openapi.json
```

A generated client is a snapshot: it is only as current as the document you
generated it from, so re-run the command after operations are added. The
TypeScript SDK is versioned and published instead, which is the difference worth
knowing about.

These come from the live platform rather than from a build artifact, so they
track the catalog far more closely than any page here. They are not instantaneous:
the catalog is cached for 30 seconds and the OpenAPI document for an hour, and
during an engine outage both fall back to a seeded mirror and say so with
`degraded: true`. Treat that flag as "this may be stale" rather than ignoring it.

## Conventions across operations [#conventions-across-operations]

**Timestamps** are ISO 8601 in UTC, usually on a `fetchedAt` field, recording
when the upstream data was read rather than when your request arrived. On a
cached response (`x-upapi-cache: HIT`) it is the original fetch time.

**Optional inputs** have server-side defaults. Sending `{}` to an operation with
no required fields is valid and uses all of them.

**Proxy control** is a header — `x-proxy-url` works on any operation. Some
operations *additionally* declare a `proxyUrl` input field; the operation's
marketplace page shows whether it does. See [Proxies](https://upapi.io/docs/platform/proxies).

**Cost** varies per operation. Each carries a unit weight that is charged
against your monthly quota — see [Rate limits](https://upapi.io/docs/rate-limits).
