---
title: "Create a CI binding"
method: POST
path: "/ci/v1/bindings"
tags: ["CI Federation"]
---

# Create a CI binding

`POST /ci/v1/bindings`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/ci/v1/bindings`
- Auth: API key in header `SIGN-IN-WITH-X`

Create a binding linking a wallet-signed delegation to a GitHub OIDC subject pattern + project + capability scope. Optional `route_scopes` delegate narrow Web Routes changes such as `/admin` and `/admin/*`; omitted means no route authority. The body's `signed_delegation` is a SIWE/SIWX-shaped wallet-signed message verified byte-for-byte against canonical Statement and Resources builders (services/ci-bindings.ts). Position-A trust model: deployed function code runs with the project's runtime authority (RUN402_SERVICE_KEY, adminDb(), configured secrets) — disclosed verbatim in the wallet-UI consent text. Out of scope for v1: secrets_set, domain_bind, subdomain_claim, lifecycle/billing/contracts/faucet calls; non-EVM wallets; non-GitHub federation.

## Headers

- `SIGN-IN-WITH-X` string, required

## Request body

- object
  - `project_id` string, required
  - `provider` 'github-actions', required
  - `subject_match` string, required: Exact OIDC subject string OR a string ending in a single trailing `*` wildcard. Matching is string-prefix, NOT regex. Reject: empty, control chars, multiple `*`, `*` not at the end, bare `*`.
  - `allowed_actions` string[], required
  - `allowed_events` string[]: GitHub event_name values allowed. Defaults exclude `pull_request` and `pull_request_target`; opt in only deliberately.
  - `route_scopes` string[]: Optional exact or final-wildcard route patterns this CI binding may alter, e.g. [`/admin`, `/admin/*`]. Omit for no route authority. Non-null `spec.routes` in CI is allowed only when all added/removed/changed route entries are within these scopes.
  - `github_repository_id` string, nullable: GitHub stable numeric repository ID. When non-NULL, defends against the delete-then-recreate slug-reuse attack. NULL = soft-bound (logged distinctly).
  - `expires_at` string, date-time, nullable
  - `nonce` string, required: Lowercase hex 16-64 chars. Must match the nonce inside the signed delegation. Unique per (wallet, nonce) — replay protection enforced by a UNIQUE DB index.
  - `signed_delegation` string, required: Base64-encoded SIWE/SIWX-shaped wallet-signed message. The Statement and Resources fields must be verbatim equal to canonical builders' output (drift fails verification).

## Response `201`

Binding created

- CiBindingRow: A row from internal.ci_bindings — one per CI binding. Persistent forensic record; revoked rows are kept for audit.
  - `id` string: Binding id, prefixed `bnd_`.
  - `project_id` string
  - `issuer` string: OIDC issuer URL (FK to internal.trusted_oidc_issuers). v1 only: https://token.actions.githubusercontent.com.
  - `subject_match` string
  - `allowed_actions` string[]
  - `allowed_events` string[]: Sorted-deduped on insert. Default `['push','workflow_dispatch']`.
  - `route_scopes` string[]: Sorted-deduped explicit route patterns CI may alter. Empty array means CI cannot ship non-null `spec.routes`.
  - `asset_key_scopes` string[], nullable: Per-binding allowlist for the `spec.assets` slice. NULL means no asset authority (default for bindings created via `POST /ci/v1/bindings`). Each entry is an exact asset key or `prefix/*` wildcard. Mutated via `POST /ci/v1/bindings/{binding_id}/asset-scopes`.
  - `github_repository_id` string, nullable
  - `created_by` string: Lowercase EVM wallet address.
  - `nonce` string
  - `created_sig` object: Parsed delegation: { payload, raw, signer, verified_at }. Stored verbatim for forensic re-verification.
  - `created_at` string, date-time
  - `expires_at` string, date-time, nullable
  - `revoked_at` string, date-time, nullable
  - `last_used_at` string, date-time, nullable
  - `use_count` integer

## Other responses

- `400`: Validation failure. Error codes: `nonce_replay`, `delegation_statement_mismatch`, `delegation_resource_uri_mismatch`, `signer_mismatch`, `delegation_oversized`, `delegation_parse_failed`, `delegation_signature_invalid`, plus generic field validation errors.
- `403`: SIWE wallet does not own the named project_id.
- `404`: project_id does not exist.
- `409`: Duplicate active binding for (project_id, issuer, subject_match).

---

[API](https://skmtc.dev/run402/apis/run402-api.md) · [All operations](https://skmtc.dev/run402/apis/run402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/run402/apis/run402-api/revisions/893607557f50?raw)
