> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# External Calls

> Request work in external systems with durable external calls

An external call is a durable request for an external system to do something:
void an invoice, update a customer record, send an email. You submit it from an
operation's preparation, and it is persisted in the same transaction as your
local changes, before anything is sent.

```ts bijection/invoices.ts {16-19} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineOperation } from "bijection/server";
import { v } from "bijection/values";
import { invoices } from "./schema";
import { billing } from "./billing";

export const markDisputed = defineOperation({
  on: invoices,
  target: { argument: "invoice_id" },
  args: { invoice_id: v.id("billing_invoices"), reason: v.string() },
  returns: v.object({ call: v.string() }),
  prepare: async (ctx, args) => {
    await ctx.db.insert("invoice_disputes", {
      invoice_id: args.invoice_id,
      reason: args.reason,
    });
    const call = await ctx.externalCalls.submit(billing.commands.setStatus, {
      target: args.invoice_id,
      args: { status: "disputed" },
    });
    return { call };
  },
});
```

Here `billing` is an [integration](/integrations/overview) whose `setStatus`
command describes how to send the request and how to tell whether it took
effect. `billing_invoices` is the table that integration keeps in sync.

## Submitting an external call

`ctx.externalCalls.submit(command, options)` takes a command from one of your
integrations and these options:

| Option | Meaning |
| - | - |
| `target` | The local ID of the synced record the command changes, or `{ sourceId }` for a command that creates a record |
| `args` | The command's arguments, checked against the command's declared validators |
| `approval` | Optional. The ID of an approval issued for exactly this request. See [access rules](/access/overview) |

It returns the call's ID, a string you can store and use to follow the call.

Submission is part of the transaction:

* The call is recorded only if the transaction commits. If preparation throws
  after submitting, the call disappears with the rest of the transaction.
* The call can only be submitted from a mutation transaction, such as an
  operation's `prepare`. Anywhere else it is refused with `MutationRequired`.
* The command's arguments are fixed when the request is accepted. Later retries
  send those same arguments; they are never recalculated by newer code.

<Note>
  Submitting records your intent. It does not update the synced table. The
  table changes only when the external system's answer confirms the change and
  the confirmed record is published back.
</Note>

## The lifecycle of an external call

<Steps>
  <Step title="Persisted">
    The call commits with the operation, before any request is sent. From this
    point the engine owns delivering it, independently of the process that
    submitted it.
  </Step>

  <Step title="Held and sent">
    When the call's turn comes, the engine holds it and sends the command's
    request. A call can wait here for an earlier call it is ordered after, or
    for an approval.
  </Step>

  <Step title="Outcome recorded">
    The external system's response is retained and interpreted under the
    command's declared evidence contract. The resulting outcome is recorded in
    its own commit.
  </Step>

  <Step title="Published">
    Confirmed facts reach the synced table, where your queries see them: from
    the response itself when it proves the resulting record, otherwise from a
    later read of the source. Confirmation and publication are recorded
    separately.
  </Step>
</Steps>

External systems control their own transactions. No local transaction stays
open while a request is in flight, and an operation that submits several calls
does not make their effects atomic.

## Delivery outcomes

Each external call has one of these delivery states, reported by an operation's
[status](/operations/calling-operations#following-a-request):

| State | Meaning |
| - | - |
| `pending` | Persisted and not yet delivered |
| `held` | Waiting on an earlier call it is ordered after, or on an approval |
| `unknown` | A request may have reached the destination, but its outcome could not be established |
| `delivered` | The destination's answer settled it as applied |
| `refused` | The destination answered and refused it |
| `indeterminate` | Reconciliation could not establish what happened |
| `delivered_unacknowledged` | Reconciliation found it delivered, without an acknowledgement that proves it |
| `not_applied` | A completed search of the destination's own records found no trace of it |
| `duplicated` | That search found it applied more than once. This needs an operator |
| `superseded` | Replaced by a later operation |

A call's status also reports its `publication`: `null`, `pending`, `blocked`, or
`published` at a revision.

## Delivery guarantees

What the engine may do after a lost response depends on the command's declared
delivery contract, which the integration states for each command:

<Tabs>
  <Tab title="idempotent">
    The destination deduplicates requests by the call's identity for a stated
    period. Within that period, the engine can send again under the same
    identity, and the destination treats it as the same request. If the
    destination states no retention period, the engine never resends under the
    retained identity.
  </Tab>

  <Tab title="single_attempt">
    The destination offers no deduplication. The engine never resends
    automatically: an unknown outcome stays unknown, and calls ordered after it
    wait, until non-delivery is proved.
  </Tab>
</Tabs>

<Warning>
  Neither contract promises that the external effect happens exactly once. The
  engine records what the destination's evidence proves, and nothing more.
</Warning>

A successful HTTP status is not always proof. Commands declare which part of a
response carries the verdict, and a response that proves only that a request was
well formed does not settle the call as `delivered`.

## Unknown outcomes and recovery

A timeout, a crashed worker, or a missing response does not prove that a call
failed. The engine records such a call as `unknown`, and it stays `unknown`
until it is reconciled under the command's own contract, for example by reading
the destination's records for this call's identity.

When you meet an unknown outcome:

* **Do not submit a replacement.** A new request is a new business change and
  could apply the effect twice. The original call keeps its identity.
* **Recover the same call.** A deployment administrator can inspect and recover
  it from the CLI. Recovery reuses the accepted identity, interprets retained
  evidence or performs a bounded read, and never sends a new write.

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection integration command-status <external-call-id>
bijection integration command-recover <external-call-id>
```

`command-status` reports the retained outcome, the stopping reason, the attempt
and byte budgets, and the evidence still required. See also
[CLI and console](/operations/cli-and-dashboard).

<Tip>
  Undoing a completed external effect is a new business change. Write a
  separate operation for it, such as `refund` for `charge`. Cancelling a request
  cannot undo an effect that already happened.
</Tip>

## Reading a call's status in your code

<Warning>`externalCallStatus` is in beta.</Warning>

`externalCallStatus(ctx, id)` returns the delivery state of a call your
component submitted, as one of the states above. It is an ordinary tracked read,
so a query that calls it re-runs when the call settles.

```ts bijection/invoices.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { externalCallStatus, type ExternalCallId } from "bijection/server";
import { query } from "./_generated/server";
import { v } from "bijection/values";

export const disputeStatus = query({
  args: { call: v.string() },
  handler: async (ctx, args) => {
    return await externalCallStatus(ctx, args.call as ExternalCallId);
  },
});
```

It answers only for calls submitted by the calling component. Any other ID, or a
call another component submitted, is refused with `ExternalCallNotFound`.
Nothing else about the call is disclosed: not its arguments, its attempts, nor
the destination's own words for a refusal.
