---
title: "Issue a signup receipt"
description: "Sign a v1.2 Contract E signup_token with the RP key from the exact attestation, then record it through Contract F or identity_record_signup."
image: "https://docs.agentregistrar.dev/og/docs-default.png"
---

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

# Issue a signup receipt

**Goal:** after an account is created or resolved, optionally return an RP-signed
`signup_token` that proves who signed up, for whom, under which terms and when.

The receipt is optional. If you issue one, the broker records it as attribution
evidence. The claim set is defined in [spec section
11](/agent-identity/v1/specification/#11-contract-e-identity-facts--the-context-pointer);
the field reference is [Receipt and
context](/agent-identity/v1/receipt-and-context/).

A v1.2 receipt is signed by **your** relying-party key, not the broker. The key
must be present in the exact provider attestation named by
`service.attestation_jti`. There is no broker-key fallback.

## What goes in the receipt

| Claim | Value |
| :--- | :--- |
| `iss` | Your canonical RP origin, for example `https://example.com`. It must equal `service.origin`. |
| `sub` | The agent id (`agt_...`). |
| `aud` | The human broker origin, for example `https://agentregistrar.dev`. |
| `jti` | A unique receipt id (`srp_` + 26-character lowercase Crockford base32 ULID). |
| `iat`, `signed_up_at` | Unix seconds. Both must fall inside the named attestation's `[iat, exp]` window. |
| `assertion_jti` | The `jti` of the assertion presented at signup. |
| `service.audience` | Your discovery `audience`. |
| `service.origin` | Your canonical origin; must equal `iss`. |
| `service.provider_name` | Your `provider_name`. |
| `service.provider_url` | Optional canonical URL. It is not an identity binding. |
| `service.account_id` | An opaque account reference. It is not a resource inventory. |
| `service.attestation_jti` | The `jti` of the attestation that contained the signing key. |
| `service.key_id` | The key id that signs the receipt. It must equal the JWS header `kid`. |
| `agent.*` | `id`, `name`, `email`. |
| `human.*` | `id`, `name`, `email`, `verified`. |
| `tos.*` | `version`, `url`, `sha256`, optional `master_tos_version`. |
| `context` | Optional `{ url, sha256 }` pointer to your guide. |
| `exp` | **Do not add one.** A v1.2 receipt has no expiry. A legacy `exp` is ignored. |

Do not add `service.instance_sku`, `service.endpoint_url`, a free-form `metadata`
object, resource credentials or API usage. The receipt is not a resource
inventory. Vendor capability belongs in the context guide.

## Sign it

Use the RP key registered with your broker and the exact attestation that
contained it. If you use the shared implementation, `signRpSignupReceipt` builds
the frozen header and payload and rejects a missing attestation key. The
equivalent `jose` flow is below.

```bash
mkdir provider-receipt
cd provider-receipt
npm init -y
npm install jose@6.2.13
```

Save as `sign-receipt.mjs`. `rp-private.jwk.json` is your private key;
`attestation.jws` is the exact attestation that was current at signup.

```js
import { readFileSync } from "node:fs";
import { importJWK, SignJWT } from "jose";

const privateJwk = JSON.parse(readFileSync("rp-private.jwk.json", "utf8"));
const attestation = readFileSync("attestation.jws", "utf8").trim();
const payload = JSON.parse(
  Buffer.from(attestation.split(".")[1], "base64url").toString("utf8"),
);
const key = payload.keys.find((candidate) => candidate.kid === privateJwk.kid);
if (!key || !key.purposes.includes("receipt")) {
  throw new Error("the key is not a receipt key in this attestation");
}
const now = Math.floor(Date.now() / 1000);
if (now < payload.iat || now > payload.exp) {
  throw new Error("the attestation is not valid at signing time");
}

const claims = {
  iss: payload.rp.origin,
  sub: "agt_01j00000000000000000000000",
  aud: "https://agentregistrar.dev",
  jti: "srp_01j00000000000000000000000",
  iat: now,
  signed_up_at: now,
  assertion_jti: "jti_01j00000000000000000000000",
  service: {
    audience: payload.rp.audience,
    origin: payload.rp.origin,
    provider_name: "Example RP",
    provider_url: "https://example.com",
    account_id: "acct_opaque_0001",
    attestation_jti: payload.jti,
    key_id: privateJwk.kid,
  },
  agent: {
    id: "agt_01j00000000000000000000000",
    name: "opencode-agent",
    email: "opencode-agent@agents.example.com",
  },
  human: {
    id: "hum_01j00000000000000000000000",
    name: "Example Human",
    email: "human@example.com",
    verified: true,
  },
  tos: {
    version: "2026.1",
    url: "https://example.com/terms",
    sha256: "sha256:6c18126f93de8d1972589ca94633ca82e76bd9ba1f0592618b0105753f3501d6",
    master_tos_version: "2026.1",
  },
  context: {
    url: "https://example.com/docs/SKILL.md",
    sha256: "sha256:a3307e1b3904c7671f341730017f21bed656a85241b645c854b4f2887e7976ac",
  },
};

const signupToken = await new SignJWT(claims)
  .setProtectedHeader({
    alg: "EdDSA",
    typ: "agent-signup-receipt+jwt",
    kid: privateJwk.kid,
  })
  .sign(await importJWK(privateJwk, "EdDSA"));

console.log(signupToken);
```

There is no `exp` in the payload. The receipt does not expire; its validity is
the historical question "did this key and identity exist at `iat`?".

## Return it and record it

Return the token in the signup response as `signup_token`, next to the
credentials:

```json
{
  "resolution": "created",
  "account_id": "acct_opaque_0001",
  "api_key": "sk_live_<show-once>",
  "signup_token": "<compact JWS>",
  "context": {
    "url": "https://example.com/docs/SKILL.md",
    "sha256": "sha256:a3307e1b3904c7671f341730017f21bed656a85241b645c854b4f2887e7976ac"
  }
}
```

The agent records it with `identity_record_signup`, or you deliver it to the
broker through Contract F:

```bash
SIGNUP_TOKEN="<compact JWS from the previous step>"
RP_ATTESTATION="<the exact attestation JWS>"
curl -sS -X POST https://agentregistrar.dev/api/broker/signup-receipt \
  -H 'Content-Type: application/json' \
  -d "{\"signup_token\":\"$SIGNUP_TOKEN\",\"rp_attestation\":\"$RP_ATTESTATION\"}"
```

Contract F accepts exactly `{signup_token, rp_attestation}`. It verifies the
effective RP-broker trust list before any JWKS fetch, the historical attestation,
the receipt signature with the named key, the origin/audience binding, and the
receipt `sub` against the acceptance. It records `proof_source:"rp_callback"`.
A duplicate `jti` with the same body returns `deduplicated:true`; a different
body is `409 signup_conflict`.

If the attestation is unavailable, Contract F returns `attestation_unavailable`
rather than using a newer one. A transient broker metadata/JWKS outage is
`503 rp_broker_unavailable` and is retryable. An unauthenticated legacy body is
`401` with a stable code.

## Tier 2 and self-report

`identity_record_signup` accepts either:

- `{signup_token, rp_attestation?}`: the same RP-signed receipt. If the exact
  attestation is omitted, the broker fetches discovery at `service.origin` and
  requires the presented attestation's `jti` to equal `service.attestation_jti`;
  it never substitutes a newer attestation. This records `proof_source:"rp_signed"`.
- `{self_report}`: an unsigned report for an RP that issued no receipt. It has no
  proof token, assertion, verified origin, or verification row, records
  `agent_self_reported`, cannot overwrite a stronger proof, and still requires a
  real recorded acceptance.

A pre-v1.2 broker-signed receipt is rejected for a new proof. Historical rows stay
`legacy_broker_signed` and are never relabeled.

## Resolve and retry

- Create the account once. On a repeat, resolve it and return
  `resolution: "resolved"`.
- On resolve, omit the provider credential. Do not send duplicate welcome or
  legal mail.
- You may mint a fresh receipt on resolve so a dropped-response retry can still
  record the proof.
- The receipt `jti` identifies the receipt, not the account. Do not use it as the
  account key.
- `service.account_id` is opaque. It may be a database id or a handle. It must
  not encode SKUs, regions or resource state.

## Verify your work

- [ ] The JWT header is `alg: EdDSA`, `typ: agent-signup-receipt+jwt`, and
      `kid` equals `service.key_id`.
- [ ] The signing key appears in the exact attestation and has `receipt` in its
      purposes.
- [ ] The payload has no `exp`, and `iat`/`signed_up_at` are inside the
      attestation window.
- [ ] `iss` equals `service.origin` and the attestation `rp.origin`; `aud` is the
      broker origin.
- [ ] `service.attestation_jti` is the exact attestation that contained the key.
- [ ] `context.url` and `context.sha256` match the guide you serve, and the
      domain matches the signup endpoint.
- [ ] A resolve returns no credential and no duplicate mail.

## Next

- [Publish a context guide](/providers/guides/publish-context-guide/): the pointer target.
- [Receipt and context](/agent-identity/v1/receipt-and-context/): the full field reference.
- [Production checklist](/providers/guides/production-checklist/): key custody and rotation.
- [Abuse reporting](/providers/guides/abuse-reporting/): the other authenticated RP path.

Source: https://docs.agentregistrar.dev/providers/guides/issue-signup-receipt/index.mdx
